Skip to main content

iri_string/percent_encode/
decode.rs

1//! Decoders for percent-encoding.
2
3use core::fmt;
4
5#[cfg(feature = "alloc")]
6use alloc::borrow::Cow;
7#[cfg(feature = "alloc")]
8use alloc::string::{FromUtf8Error, String};
9
10use crate::parser::str::strip_decode_xdigits2;
11
12/// Returns the result of [`percent-decode`] algorithm in the WHATWG URL Standard.
13///
14/// [`percent-decode`]: https://url.spec.whatwg.org/#percent-decode
15///
16/// # Examples
17///
18/// ```
19/// use iri_string::percent_encode::decode::decode_whatwg_bytes;
20///
21/// let decoded = decode_whatwg_bytes(b"hello%20world");
22///
23/// assert_eq!(decoded.not_yet_decoded(), &b"hello%20world"[..]);
24///
25/// // This requires `alloc` feature since
26/// // `into_bytes()` returns `Cow<'_, [u8]>`.
27/// # #[cfg(feature = "alloc")]
28/// assert_eq!(decoded.into_bytes(), &b"hello world"[..]);
29/// ```
30#[inline]
31#[must_use]
32pub fn decode_whatwg_bytes(bytes: &[u8]) -> PercentDecodedWhatwgBytes<'_> {
33    PercentDecodedWhatwgBytes::from_raw(bytes)
34}
35
36/// A percent-decoded string based on [`percent-decode`] algorithm of the WHATWG URL standard.
37///
38/// Note that this type does not guarantee that the string is valid
39/// percent-encoded string. The raw string may have stray percent character or
40/// the following digits may be invalid as hexadecimal digits.
41///
42/// Note that comparisons and hashing via std traits (such as `Eq` and `Hash`)
43/// will use the raw value, not the content after decoding.
44///
45/// [`percent-decode`]: https://url.spec.whatwg.org/#percent-decode
46#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
47pub struct PercentDecodedWhatwgBytes<'a> {
48    /// Not-yet-decoded string.
49    not_yet_decoded: &'a [u8],
50}
51
52impl<'a> PercentDecodedWhatwgBytes<'a> {
53    /// Creates a `PercentDecodedWhatwgBytes` from the possibly non-decoded raw bytes.
54    #[inline]
55    #[must_use]
56    fn from_raw(not_yet_decoded: &'a [u8]) -> Self {
57        Self { not_yet_decoded }
58    }
59
60    /// Returns the not-yet-decoded input bytes.
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use iri_string::percent_encode::decode::decode_whatwg_bytes;
66    ///
67    /// let decoded = decode_whatwg_bytes(b"hello%20world");
68    ///
69    /// assert_eq!(decoded.not_yet_decoded(), &b"hello%20world"[..]);
70    /// ```
71    #[inline]
72    #[must_use]
73    pub fn not_yet_decoded(&self) -> &'a [u8] {
74        self.not_yet_decoded
75    }
76
77    /// Decodes the bytes as much as possible.
78    ///
79    /// If the string contains a decodable percent-encoded triplet, returns
80    /// a tuple of:
81    ///
82    /// 1. the length of the prefix that contains no percent-encoded triplets,
83    /// 1. the first decoded byte, and
84    /// 1. the suffix after the decoded percent-encoded triplet.
85    #[must_use]
86    fn try_non_allocating_decode(&self) -> Option<(usize, u8, &'a [u8])> {
87        let mut len_before_pct;
88        let mut rest = self.not_yet_decoded;
89
90        while !rest.is_empty() {
91            #[cfg(feature = "memchr")]
92            let pct_pos = memchr::memchr(b'%', rest)?;
93            #[cfg(not(feature = "memchr"))]
94            let pct_pos = rest.iter().position(|&b| b == b'%')?;
95
96            let after_pct;
97            (len_before_pct, after_pct) = (pct_pos, &rest[(pct_pos + 1)..]);
98
99            let decoded;
100            (decoded, rest) = strip_decode_xdigits2(after_pct);
101            if let Some(decoded) = decoded {
102                return Some((len_before_pct, decoded, rest));
103            }
104            rest = after_pct;
105        }
106
107        None
108    }
109
110    /// Returns the decoded bytes as a slice if no memory allocation is needed.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use iri_string::percent_encode::decode::decode_whatwg_bytes;
116    ///
117    /// let no_alloc = decode_whatwg_bytes(b"99% unsafe");
118    /// assert_eq!(no_alloc.to_bytes(), Some(&b"99% unsafe"[..]));
119    ///
120    /// let alloc_needed = decode_whatwg_bytes(b"hello%20world");
121    /// assert_eq!(alloc_needed.to_bytes(), None);
122    /// ```
123    #[inline]
124    #[must_use]
125    pub fn to_bytes(&self) -> Option<&'a [u8]> {
126        match self.try_non_allocating_decode() {
127            None => Some(self.not_yet_decoded),
128            _ => None,
129        }
130    }
131
132    /// Decodes the bytes, based on [`percent-decode`] algorithm of the WHATWG URL standard.
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// use iri_string::percent_encode::decode::decode_whatwg_bytes;
138    ///
139    /// let decoded = decode_whatwg_bytes(b"hello%20world");
140    /// assert_eq!(decoded.into_bytes(), &b"hello world"[..]);
141    /// ```
142    ///
143    /// [`percent-decode`]: https://url.spec.whatwg.org/#percent-decode
144    #[cfg(feature = "alloc")]
145    #[inline]
146    #[must_use]
147    pub fn into_bytes(&self) -> Cow<'a, [u8]> {
148        use crate::parser::str::find_split_hole;
149
150        let (mut result, mut rest) = match self.try_non_allocating_decode() {
151            Some((prefix_len, decoded, rest)) => {
152                let mut prefix = alloc::vec::Vec::from(&self.not_yet_decoded[..prefix_len]);
153                prefix.push(decoded);
154                (prefix, rest)
155            }
156            None => return Cow::Borrowed(self.not_yet_decoded),
157        };
158
159        while !rest.is_empty() {
160            let after_pct = if let Some((no_pct, after_pct)) = find_split_hole(rest, b'%') {
161                result.extend(no_pct);
162                after_pct
163            } else {
164                result.extend(core::mem::take(&mut rest));
165                break;
166            };
167
168            let decoded;
169            (decoded, rest) = strip_decode_xdigits2(after_pct);
170            result.extend(decoded);
171        }
172
173        Cow::Owned(result)
174    }
175
176    /// Decodes the bytes into a string, based on [`percent-decode`] algorithm
177    /// of the WHATWG URL standard.
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use iri_string::percent_encode::decode::decode_whatwg_bytes;
183    ///
184    /// let decoded = decode_whatwg_bytes(b"hello%20world");
185    /// assert_eq!(
186    ///     decoded.into_string(),
187    ///     Ok("hello world".to_owned())
188    /// );
189    /// ```
190    ///
191    /// [`percent-decode`]: https://url.spec.whatwg.org/#percent-decode
192    #[cfg(feature = "alloc")]
193    #[inline]
194    pub fn into_string(&self) -> Result<String, FromUtf8Error> {
195        String::from_utf8(self.into_bytes().into_owned())
196    }
197
198    /// Returns an iterator of decoded fragments.
199    ///
200    /// # Examples
201    ///
202    /// ```
203    /// use iri_string::percent_encode::decode::{
204    ///     decode_whatwg_bytes, DecodedFragment,
205    /// };
206    ///
207    /// let mut i = decode_whatwg_bytes(b"100% hello%20world")
208    ///     .bytes_fragments();
209    ///
210    /// assert_eq!(i.next(), Some(DecodedFragment::Direct(b"100")));
211    /// assert_eq!(i.next(), Some(DecodedFragment::StrayPercent));
212    /// assert_eq!(i.next(), Some(DecodedFragment::Direct(b" hello")));
213    /// assert_eq!(i.next(), Some(DecodedFragment::DecodedByte(b' ')));
214    /// assert_eq!(i.next(), Some(DecodedFragment::Direct(b"world")));
215    /// assert_eq!(i.next(), None);
216    /// ```
217    #[inline]
218    #[must_use]
219    pub fn bytes_fragments(&self) -> PercentDecodedBytesFragments<'a> {
220        PercentDecodedBytesFragments {
221            rest: self.not_yet_decoded,
222        }
223    }
224}
225
226impl fmt::Debug for PercentDecodedWhatwgBytes<'_> {
227    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228        let mut f = f.debug_struct("PercentDecodedWhatwgBytes");
229        // Print as a string if possible.
230        match core::str::from_utf8(self.not_yet_decoded) {
231            Ok(s) => f.field("not_yet_decoded", &s),
232            Err(_) => f.field("not_yet_decoded", &self.not_yet_decoded),
233        };
234        f.finish()
235    }
236}
237
238/// Fragments in a percent-decodable byte sequence.
239#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
240pub enum DecodedFragment<'a> {
241    /// Bytes without percent characters nor percent-encoded triplets.
242    Direct(&'a [u8]),
243    /// A decoded byte from a percent-encoded triplet.
244    DecodedByte(u8),
245    /// A percent character that does not form a valid percent-encoded triplet.
246    StrayPercent,
247}
248
249/// An iterator of fragments in a percent-decodable byte sequence.
250//
251// NOTE: Do not implement `Copy` since an iterator type with `Copy` is a foot-gun.
252#[derive(Clone)]
253pub struct PercentDecodedBytesFragments<'a> {
254    /// Remaining string to decode.
255    rest: &'a [u8],
256}
257
258impl fmt::Debug for PercentDecodedBytesFragments<'_> {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        let mut f = f.debug_struct("PercentDecodedBytesFragments");
261        // Print as a string if possible.
262        match core::str::from_utf8(self.rest) {
263            Ok(s) => f.field("rest", &s),
264            Err(_) => f.field("rest", &self.rest),
265        };
266        f.finish()
267    }
268}
269
270impl<'a> PercentDecodedBytesFragments<'a> {
271    /// Returns the remaining not-yet-decoded bytes.
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// use iri_string::percent_encode::decode::{
277    ///     decode_whatwg_bytes, DecodedFragment,
278    /// };
279    ///
280    /// let mut i = decode_whatwg_bytes(b"hello%20world")
281    ///     .bytes_fragments();
282    ///
283    /// assert_eq!(i.next(), Some(DecodedFragment::Direct(b"hello")));
284    /// assert_eq!(i.not_yet_decoded(), &b"%20world"[..]);
285    /// ```
286    #[inline]
287    #[must_use]
288    pub fn not_yet_decoded(&self) -> &'a [u8] {
289        self.rest
290    }
291}
292
293impl<'a> Iterator for PercentDecodedBytesFragments<'a> {
294    type Item = DecodedFragment<'a>;
295
296    fn next(&mut self) -> Option<Self::Item> {
297        let mut rest = self.rest;
298
299        match rest {
300            [] => None,
301            [b'%', after_pct @ ..] => {
302                let decoded;
303                (decoded, rest) = strip_decode_xdigits2(after_pct);
304                if let Some(decoded) = decoded {
305                    self.rest = rest;
306                    Some(DecodedFragment::DecodedByte(decoded))
307                } else {
308                    self.rest = after_pct;
309                    Some(DecodedFragment::StrayPercent)
310                }
311            }
312            [_, after_first @ ..] => {
313                #[cfg(feature = "memchr")]
314                let pct_pos_minus_one = memchr::memchr(b'%', after_first);
315                #[cfg(not(feature = "memchr"))]
316                let pct_pos_minus_one = after_first.iter().position(|&b| b == b'%');
317
318                let before_pct;
319                (before_pct, self.rest) = match pct_pos_minus_one {
320                    None => (rest, &rest[rest.len()..]),
321                    Some(pct_pos_minus_one) => rest.split_at(pct_pos_minus_one + 1),
322                };
323                Some(DecodedFragment::Direct(before_pct))
324            }
325        }
326    }
327}