iri_string/
convert.rs

1//! Conversion between URI/IRI types.
2
3use core::fmt;
4
5#[cfg(feature = "alloc")]
6use alloc::collections::TryReserveError;
7#[cfg(all(feature = "alloc", not(feature = "std")))]
8use alloc::string::String;
9
10#[cfg(feature = "alloc")]
11use crate::format::{ToDedicatedString, ToStringFallible};
12use crate::spec::Spec;
13use crate::types::{
14    RiAbsoluteStr, RiFragmentStr, RiQueryStr, RiReferenceStr, RiRelativeStr, RiStr,
15};
16#[cfg(feature = "alloc")]
17use crate::types::{
18    RiAbsoluteString, RiFragmentString, RiQueryString, RiReferenceString, RiRelativeString,
19    RiString,
20};
21#[cfg(feature = "alloc")]
22use crate::types::{
23    UriAbsoluteString, UriFragmentString, UriQueryString, UriReferenceString, UriRelativeString,
24    UriString,
25};
26
27/// Hexadecimal digits for a nibble.
28const HEXDIGITS: [u8; 16] = [
29    b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'A', b'B', b'C', b'D', b'E', b'F',
30];
31
32/// A resource identifier mapped to a URI of some kind.
33///
34/// Supported `Src` type are:
35///
36/// * IRIs:
37///     + [`IriAbsoluteStr`] (alias of `RiAbsoluteStr<IriSpec>`)
38///     + [`IriReferenceStr`] (alias of `RiReferenceStr<IriSpec>`)
39///     + [`IriRelativeStr`] (alias of `RiRelativeStr<IriSpec>`)
40///     + [`IriStr`] (alias of `RiStr<IriSpec>`)
41/// * URIs:
42///     + [`UriAbsoluteStr`] (alias of `RiAbsoluteStr<UriSpec>`)
43///     + [`UriReferenceStr`] (alias of `RiReferenceStr<UriSpec>`)
44///     + [`UriRelativeStr`] (alias of `RiRelativeStr<UriSpec>`)
45///     + [`UriStr`] (alias of `RiStr<UriSpec>`)
46///
47/// # Examples
48///
49/// ```
50/// use iri_string::convert::MappedToUri;
51/// use iri_string::types::{IriStr, UriStr};
52///
53/// let src = IriStr::new("http://example.com/?alpha=\u{03B1}")?;
54/// // The type is `MappedToUri<IriStr>`, but you usually don't need to specify.
55/// let mapped = MappedToUri::from(src).to_string();
56/// assert_eq!(mapped, "http://example.com/?alpha=%CE%B1");
57/// # Ok::<_, iri_string::validate::Error>(())
58/// ```
59///
60/// [`IriAbsoluteStr`]: crate::types::IriAbsoluteStr
61/// [`IriReferenceStr`]: crate::types::IriReferenceStr
62/// [`IriRelativeStr`]: crate::types::IriRelativeStr
63/// [`IriStr`]: crate::types::IriStr
64/// [`UriAbsoluteStr`]: crate::types::UriAbsoluteStr
65/// [`UriReferenceStr`]: crate::types::UriReferenceStr
66/// [`UriRelativeStr`]: crate::types::UriRelativeStr
67/// [`UriStr`]: crate::types::UriStr
68#[derive(Debug, Clone, Copy)]
69pub struct MappedToUri<'a, Src: ?Sized>(&'a Src);
70
71/// Implement conversions for an IRI string type.
72macro_rules! impl_for_iri {
73    ($borrowed:ident, $owned:ident, $owned_uri:ident) => {
74        impl<S: Spec> fmt::Display for MappedToUri<'_, $borrowed<S>> {
75            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76                write_percent_encoded(f, self.0.as_str())
77            }
78        }
79
80        #[cfg(feature = "alloc")]
81        impl<S: Spec> ToDedicatedString for MappedToUri<'_, $borrowed<S>> {
82            type Target = $owned_uri;
83
84            fn try_to_dedicated_string(&self) -> Result<Self::Target, TryReserveError> {
85                let s = self.try_to_string()?;
86                Ok(TryFrom::try_from(s)
87                    .expect("[validity] the IRI must be encoded into a valid URI"))
88            }
89        }
90
91        impl<'a, S: Spec> From<&'a $borrowed<S>> for MappedToUri<'a, $borrowed<S>> {
92            #[inline]
93            fn from(iri: &'a $borrowed<S>) -> Self {
94                Self(iri)
95            }
96        }
97
98        #[cfg(feature = "alloc")]
99        impl<'a, S: Spec> From<&'a $owned<S>> for MappedToUri<'a, $borrowed<S>> {
100            #[inline]
101            fn from(iri: &'a $owned<S>) -> Self {
102                Self(iri.as_slice())
103            }
104        }
105    };
106}
107
108impl_for_iri!(RiReferenceStr, RiReferenceString, UriReferenceString);
109impl_for_iri!(RiStr, RiString, UriString);
110impl_for_iri!(RiAbsoluteStr, RiAbsoluteString, UriAbsoluteString);
111impl_for_iri!(RiRelativeStr, RiRelativeString, UriRelativeString);
112impl_for_iri!(RiQueryStr, RiQueryString, UriQueryString);
113impl_for_iri!(RiFragmentStr, RiFragmentString, UriFragmentString);
114
115/// Percent-encodes and writes the IRI string using the given buffer.
116fn write_percent_encoded(f: &mut fmt::Formatter<'_>, mut s: &str) -> fmt::Result {
117    while !s.is_empty() {
118        // Skip ASCII characters.
119        let non_ascii_pos = s.bytes().position(|b| !b.is_ascii()).unwrap_or(s.len());
120        let (ascii, rest) = s.split_at(non_ascii_pos);
121        if !ascii.is_empty() {
122            f.write_str(ascii)?;
123            s = rest;
124        }
125
126        if s.is_empty() {
127            return Ok(());
128        }
129
130        // Search for the next ASCII character.
131        let nonascii_end = s.bytes().position(|b| b.is_ascii()).unwrap_or(s.len());
132        let (nonasciis, rest) = s.split_at(nonascii_end);
133        debug_assert!(
134            !nonasciis.is_empty(),
135            "string without non-ASCII characters should have caused early return"
136        );
137        s = rest;
138
139        // Escape non-ASCII characters as percent-encoded bytes.
140        //
141        // RFC 3987 (section 3.1 step 2) says "for each character in
142        // 'ucschar' or 'iprivate'", but this simply means "for each
143        // non-ASCII characters" since any non-ASCII characters that can
144        // appear in an IRI match `ucschar` or `iprivate`.
145        /// Number of source bytes to encode at once.
146        const NUM_BYTES_AT_ONCE: usize = 21;
147        percent_encode_bytes(f, nonasciis, &mut [0_u8; NUM_BYTES_AT_ONCE * 3])?;
148    }
149
150    Ok(())
151}
152
153/// Percent-encode the string and pass the encoded chunks to the given function.
154///
155/// `buf` is used as a temporary working buffer. It is initialized by this
156/// function, so users can pass any mutable byte slice with enough size.
157///
158/// # Precondition
159///
160/// The length of `buf` must be 3 bytes or more.
161fn percent_encode_bytes(f: &mut fmt::Formatter<'_>, s: &str, buf: &mut [u8]) -> fmt::Result {
162    /// Fill the buffer by percent-encoded bytes.
163    ///
164    /// Note that this function applies percent-encoding to every characters,
165    /// even if it is ASCII alphabet.
166    ///
167    /// # Precondition
168    ///
169    /// * The length of `buf` must be 3 bytes or more.
170    /// * All of the `buf[i * 3]` elements should already be set to `b'%'`.
171    // This function have many preconditions and I don't want checks for them
172    // to be mandatory, so make this nested inner function.
173    fn fill_by_percent_encoded<'a>(buf: &'a mut [u8], bytes: &mut core::str::Bytes<'_>) -> &'a str {
174        let src_len = bytes.len();
175        // `<[u8; N]>::array_chunks_mut` is unstable as of Rust 1.58.1.
176        for (dest, byte) in buf.chunks_exact_mut(3).zip(bytes.by_ref()) {
177            debug_assert_eq!(
178                dest.len(),
179                3,
180                "[validity] `chunks_exact()` must return a slice with the exact length"
181            );
182            debug_assert_eq!(
183                dest[0], b'%',
184                "[precondition] the buffer must be properly initialized"
185            );
186
187            let upper = byte >> 4;
188            let lower = byte & 0b1111;
189            dest[1] = HEXDIGITS[usize::from(upper)];
190            dest[2] = HEXDIGITS[usize::from(lower)];
191        }
192        let num_dest_written = (src_len - bytes.len()) * 3;
193        let buf_filled = &buf[..num_dest_written];
194        // SAFETY: `b'%'` and `HEXDIGITS[_]` are all ASCII characters, so
195        // `buf_filled` is filled with ASCII characters and is valid UTF-8 bytes.
196        unsafe {
197            debug_assert!(core::str::from_utf8(buf_filled).is_ok());
198            core::str::from_utf8_unchecked(buf_filled)
199        }
200    }
201
202    assert!(
203        buf.len() >= 3,
204        "[precondition] length of `buf` must be 3 bytes or more"
205    );
206
207    // Drop the elements that will never be used.
208    // The length to be used is always a multiple of three.
209    let buf_len = buf.len() / 3 * 3;
210    let buf = &mut buf[..buf_len];
211
212    // Fill some bytes with `%`.
213    // This will be vectorized by optimization (especially for long buffers),
214    // so no need to selectively set `buf[i * 3]`.
215    buf.fill(b'%');
216
217    let mut bytes = s.bytes();
218    // `<core::str::Bytes as ExactSizeIterator>::is_empty` is unstable as of Rust 1.58.1.
219    while bytes.len() != 0 {
220        let encoded = fill_by_percent_encoded(buf, &mut bytes);
221        f.write_str(encoded)?;
222    }
223
224    Ok(())
225}
226
227/// Percent-encodes the given IRI using the given buffer.
228#[cfg(feature = "alloc")]
229pub(crate) fn try_percent_encode_iri_inline(
230    iri: &mut String,
231) -> Result<(), alloc::collections::TryReserveError> {
232    // Calculate the result length and extend the buffer.
233    let num_nonascii = count_nonascii(iri);
234    if num_nonascii == 0 {
235        // No need to escape.
236        return Ok(());
237    }
238    let additional = num_nonascii * 2;
239    iri.try_reserve(additional)?;
240    let src_len = iri.len();
241
242    // Temporarily take the ownership of the internal buffer.
243    let mut buf = core::mem::take(iri).into_bytes();
244    // `b'\0'` cannot appear in a valid IRI, so this default value would be
245    // useful in case of debugging.
246    buf.extend(core::iter::repeat(b'\0').take(additional));
247
248    // Fill the buffer from the tail to the head.
249    let mut dest_end = buf.len();
250    let mut src_end = src_len;
251    let mut rest_nonascii = num_nonascii;
252    while rest_nonascii > 0 {
253        debug_assert!(
254            src_end > 0,
255            "[validity] the source position should not overrun"
256        );
257        debug_assert!(
258            dest_end > 0,
259            "[validity] the destination position should not overrun"
260        );
261        src_end -= 1;
262        dest_end -= 1;
263        let byte = buf[src_end];
264        if byte.is_ascii() {
265            buf[dest_end] = byte;
266            // Use the ASCII character directly.
267        } else {
268            // Percent-encode the byte.
269            dest_end -= 2;
270            buf[dest_end] = b'%';
271            let upper = byte >> 4;
272            let lower = byte & 0b1111;
273            buf[dest_end + 1] = HEXDIGITS[usize::from(upper)];
274            buf[dest_end + 2] = HEXDIGITS[usize::from(lower)];
275            rest_nonascii -= 1;
276        }
277    }
278
279    // Move the result from the temporary buffer to the destination.
280    let s = String::from_utf8(buf).expect("[consistency] the encoding result is an ASCII string");
281    *iri = s;
282    Ok(())
283}
284
285/// Returns the number of non-ASCII characters.
286#[cfg(feature = "alloc")]
287#[inline]
288#[must_use]
289fn count_nonascii(s: &str) -> usize {
290    s.bytes().filter(|b| !b.is_ascii()).count()
291}