Skip to main content

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