Skip to main content

parse_net/
pct.rs

1//! The percent-encoding primitive: validate, decode into a caller buffer, and
2//! encode with a named set and hex case.
3//!
4//! These are the operations parse-net's own grammars need (RFC 8187 values,
5//! RFC 9651 Display Strings, RFC 7578 file names). Everything URL-specific
6//! (component encode sets, `+` as space, normalization) belongs to url-lib,
7//! which builds its sets with [`EncodeSet::add`] and [`EncodeSet::remove`].
8//!
9//! # Examples
10//!
11//! ```
12//! use parse_net::{self as pn, pct::{self, EncodeSet, HexCase}, class::Class};
13//!
14//! let mut buf = [0u8; 16];
15//! assert_eq!(pct::decode_utf8_into(b"caf%C3%A9", &mut buf)?, "caf\u{e9}");
16//! assert_eq!(pct::validate(b"%4").unwrap_err().kind(), pn::ErrorKind::InvalidPercentEncoding);
17//! assert!(pct::validate_with(b"/a/b?c=%20", Class::URI_TARGET).is_ok());
18//!
19//! let mut out = pn::SliceSink::new(&mut buf);
20//! pct::encode(b"a b/c", &EncodeSet::ATTR_CHAR_COMPLEMENT, HexCase::Upper, &mut out).expect("room");
21//! assert_eq!(out.written(), b"a%20b%2Fc");
22//! # Ok::<(), pn::ParseError>(())
23//! ```
24
25use crate::{
26    class::{self, Class},
27    error::{ErrorKind, Result, WriteError, err},
28    scan::control_kind,
29    utf8::Utf8,
30    write::{Sink, reserve},
31};
32
33/// A set of bytes that must be percent-encoded: a 256-bit bitmap.
34///
35/// [`encode`] always encodes bytes above 0x7F, whatever the set says.
36///
37/// # Examples
38///
39/// ```
40/// use parse_net::pct::EncodeSet;
41///
42/// let path = EncodeSet::CONTROLS_AND_NON_ASCII.add(b' ').add(b'#').add(b'?');
43/// assert!(path.contains(b'#') && !path.contains(b'/'));
44/// assert!(!path.remove(b'#').contains(b'#'));
45/// ```
46#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
47pub struct EncodeSet([u64; 4]);
48
49/// Which named set [`set_of`] builds.
50#[derive(Clone, Copy)]
51enum Named {
52    ControlsAndNonAscii,
53    AttrCharComplement,
54    SfDisplay,
55}
56
57const fn in_named(b: u8, which: Named) -> bool {
58    match which {
59        Named::ControlsAndNonAscii => b < 0x20 || b >= 0x7f,
60        Named::AttrCharComplement => !class::is_attr_char(b),
61        Named::SfDisplay => b == b'%' || b == b'"' || b < 0x20 || b > 0x7e,
62    }
63}
64
65const fn set_of(which: Named) -> [u64; 4] {
66    let mut bits = [0u64; 4];
67    let mut b: u8 = 0;
68    loop {
69        if in_named(b, which) {
70            let word = (b >> 6) as usize;
71            // `word` is 0 to 3.
72            #[allow(clippy::indexing_slicing)]
73            {
74                bits[word] |= 1 << (b & 63);
75            }
76        }
77        if b == u8::MAX {
78            break;
79        }
80        b += 1;
81    }
82    bits
83}
84
85impl EncodeSet {
86    /// C0 controls, DEL, and 0x80 to 0xFF.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use parse_net::pct::EncodeSet;
92    ///
93    /// let s = EncodeSet::CONTROLS_AND_NON_ASCII;
94    /// assert!(s.contains(b'\n') && s.contains(0x7f) && s.contains(0xe9));
95    /// assert!(!s.contains(b' ') && !s.contains(b'%'));
96    /// ```
97    pub const CONTROLS_AND_NON_ASCII: Self = Self(set_of(Named::ControlsAndNonAscii));
98    /// Every byte that is not an `attr-char` (RFC 8187 section 3.2.1).
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use parse_net::pct::EncodeSet;
104    ///
105    /// let s = EncodeSet::ATTR_CHAR_COMPLEMENT;
106    /// assert!(s.contains(b' ') && s.contains(b'%') && s.contains(b'\''));
107    /// assert!(!s.contains(b'a') && !s.contains(b'~'));
108    /// ```
109    pub const ATTR_CHAR_COMPLEMENT: Self = Self(set_of(Named::AttrCharComplement));
110    /// `%`, `"`, and every byte outside 0x20 to 0x7E (RFC 9651 section 4.1.11).
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use parse_net::pct::EncodeSet;
116    ///
117    /// let s = EncodeSet::SF_DISPLAY;
118    /// assert!(s.contains(b'%') && s.contains(b'"') && s.contains(0x7f));
119    /// assert!(!s.contains(b' ') && !s.contains(b'~'));
120    /// ```
121    pub const SF_DISPLAY: Self = Self(set_of(Named::SfDisplay));
122
123    /// The empty set (only bytes above 0x7F are then encoded).
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use parse_net::pct::{encoded_len, EncodeSet};
129    ///
130    /// let none = EncodeSet::empty();
131    /// assert!(!none.contains(b'%'));
132    /// assert_eq!(encoded_len(b"a b", &none), 3);
133    /// assert_eq!(encoded_len(&[0xff], &none), 3);
134    /// ```
135    #[must_use]
136    pub const fn empty() -> Self {
137        Self([0; 4])
138    }
139
140    /// The set with `b` added.
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use parse_net::pct::EncodeSet;
146    ///
147    /// let s = EncodeSet::empty().add(b'/');
148    /// assert!(s.contains(b'/') && !s.contains(b'?'));
149    /// ```
150    #[must_use]
151    pub const fn add(self, b: u8) -> Self {
152        let mut bits = self.0;
153        let word = (b >> 6) as usize;
154        // `word` is 0 to 3.
155        #[allow(clippy::indexing_slicing)]
156        {
157            bits[word] |= 1 << (b & 63);
158        }
159        Self(bits)
160    }
161
162    /// The set with `b` removed.
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// use parse_net::pct::EncodeSet;
168    ///
169    /// let s = EncodeSet::ATTR_CHAR_COMPLEMENT.remove(b'%');
170    /// assert!(!s.contains(b'%') && s.contains(b' '));
171    /// ```
172    #[must_use]
173    pub const fn remove(self, b: u8) -> Self {
174        let mut bits = self.0;
175        let word = (b >> 6) as usize;
176        // `word` is 0 to 3.
177        #[allow(clippy::indexing_slicing)]
178        {
179            bits[word] &= !(1 << (b & 63));
180        }
181        Self(bits)
182    }
183
184    /// Whether `b` is in the set.
185    ///
186    /// # Examples
187    ///
188    /// ```
189    /// use parse_net::pct::EncodeSet;
190    ///
191    /// assert!(EncodeSet::SF_DISPLAY.contains(b'"'));
192    /// assert!(!EncodeSet::SF_DISPLAY.contains(b'a'));
193    /// ```
194    #[must_use]
195    pub const fn contains(&self, b: u8) -> bool {
196        let word = (b >> 6) as usize;
197        // `word` is 0 to 3.
198        #[allow(clippy::indexing_slicing)]
199        let w = self.0[word];
200        w & (1 << (b & 63)) != 0
201    }
202}
203
204/// The hex case [`encode`] writes.
205///
206/// # Examples
207///
208/// ```
209/// use parse_net::{pct::{encode, EncodeSet, HexCase}, SliceSink};
210///
211/// let mut buf = [0u8; 8];
212/// let mut out = SliceSink::new(&mut buf);
213/// encode(b"/", &EncodeSet::ATTR_CHAR_COMPLEMENT, HexCase::Lower, &mut out)?;
214/// assert_eq!(out.written(), b"%2f");
215/// # Ok::<(), parse_net::WriteError>(())
216/// ```
217#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
218pub enum HexCase {
219    /// `%2F` (RFC 3986 section 2.1 recommends uppercase).
220    Upper,
221    /// `%2f` (required by RFC 9651 Display Strings).
222    Lower,
223}
224
225/// How the hex digits of a triple may be written.
226#[derive(Clone, Copy, PartialEq, Eq)]
227enum Accept {
228    AnyCase,
229    LowerOnly,
230}
231
232/// Walks `input`, calling `f` with each decoded byte and its offset.
233#[inline]
234fn walk(input: &[u8], accept: Accept, mut f: impl FnMut(u8, usize) -> Result<()>) -> Result<()> {
235    let mut i = 0usize;
236    while let Some(&b) = input.get(i) {
237        if b == b'%' {
238            let hi = input.get(i + 1).copied().and_then(|h| hex(h, accept));
239            let lo = input.get(i + 2).copied().and_then(|h| hex(h, accept));
240            let (Some(hi), Some(lo)) = (hi, lo) else {
241                return err(ErrorKind::InvalidPercentEncoding, i);
242            };
243            f((hi << 4) | lo, i)?;
244            i += 3;
245        } else {
246            f(b, i)?;
247            i += 1;
248        }
249    }
250    Ok(())
251}
252
253#[inline]
254const fn hex(b: u8, accept: Accept) -> Option<u8> {
255    match (b, accept) {
256        (b'A'..=b'F', Accept::LowerOnly) => None,
257        _ => class::hex_value(b),
258    }
259}
260
261/// Checks that every `%` is followed by two hex digits (either case).
262///
263/// # Errors
264///
265/// [`ErrorKind::InvalidPercentEncoding`] at the `%`.
266///
267/// # Examples
268///
269/// ```
270/// use parse_net::pct::validate;
271///
272/// assert!(validate(b"a%20b").is_ok());
273/// assert_eq!(validate(b"a%2").unwrap_err().offset(), 1);
274/// ```
275pub fn validate(input: &[u8]) -> Result<()> {
276    walk(input, Accept::AnyCase, |_, _| Ok(()))
277}
278
279/// As [`validate`], and every byte other than a `%XX` triple is in `allowed`.
280///
281/// `validate_with(target, Class::URI_TARGET)` checks the characters of a
282/// request target.
283///
284/// # Errors
285///
286/// [`ErrorKind::InvalidPercentEncoding`]; a control kind for a control byte;
287/// [`ErrorKind::InvalidByte`] for another byte outside `allowed`.
288///
289/// # Examples
290///
291/// ```
292/// use parse_net::{pct::validate_with, class::Class, ErrorKind};
293///
294/// assert!(validate_with(b"/a/b?c=d&e=%20", Class::URI_TARGET).is_ok());
295/// assert_eq!(validate_with(b"/a#frag", Class::URI_TARGET).unwrap_err().kind(), ErrorKind::InvalidByte);
296/// ```
297pub fn validate_with(input: &[u8], allowed: Class) -> Result<()> {
298    let mut i = 0usize;
299    while let Some(&b) = input.get(i) {
300        if b == b'%' {
301            match (input.get(i + 1), input.get(i + 2)) {
302                (Some(&h), Some(&l)) if class::is_hexdig(h) && class::is_hexdig(l) => i += 3,
303                _ => return err(ErrorKind::InvalidPercentEncoding, i),
304            }
305        } else if allowed.contains(b) {
306            i += 1;
307        } else if crate::scan::is_control(b) {
308            return err(control_kind(b), i);
309        } else {
310            return err(ErrorKind::InvalidByte, i);
311        }
312    }
313    Ok(())
314}
315
316/// Validates `input` and returns its decoded length.
317///
318/// # Errors
319///
320/// [`ErrorKind::InvalidPercentEncoding`].
321///
322/// # Examples
323///
324/// ```
325/// assert_eq!(parse_net::pct::decoded_len(b"a%20b")?, 3);
326/// # Ok::<(), parse_net::ParseError>(())
327/// ```
328pub fn decoded_len(input: &[u8]) -> Result<usize> {
329    let mut n = 0usize;
330    walk(input, Accept::AnyCase, |_, _| {
331        n += 1;
332        Ok(())
333    })?;
334    Ok(n)
335}
336
337fn decode_with<'b>(
338    input: &[u8],
339    out: &'b mut [u8],
340    accept: Accept,
341    utf8: bool,
342) -> Result<&'b [u8]> {
343    // First pass: validate everything and size the output, writing nothing.
344    let mut n = 0usize;
345    let mut v = Utf8::new();
346    walk(input, accept, |b, at| {
347        if utf8 && !v.push(b) {
348            return err(ErrorKind::InvalidUtf8, at);
349        }
350        n += 1;
351        Ok(())
352    })?;
353    if utf8 && !v.is_complete() {
354        return err(ErrorKind::InvalidUtf8, input.len());
355    }
356    let Some(dst) = out.get_mut(..n) else {
357        return err(ErrorKind::BufferTooSmall, 0);
358    };
359    let mut slots = dst.iter_mut();
360    walk(input, accept, |b, _| {
361        if let Some(slot) = slots.next() {
362            *slot = b;
363        }
364        Ok(())
365    })?;
366    Ok(dst)
367}
368
369/// Decodes `%XX` triples (either case) into `out`, copying every other byte,
370/// and returns the written part.
371///
372/// # Errors
373///
374/// [`ErrorKind::InvalidPercentEncoding`]; [`ErrorKind::BufferTooSmall`].
375/// Nothing is written on error.
376///
377/// # Examples
378///
379/// ```
380/// let mut buf = [0u8; 8];
381/// assert_eq!(parse_net::pct::decode_into(b"a%2Fb", &mut buf)?, b"a/b");
382/// # Ok::<(), parse_net::ParseError>(())
383/// ```
384pub fn decode_into<'b>(input: &[u8], out: &'b mut [u8]) -> Result<&'b [u8]> {
385    decode_with(input, out, Accept::AnyCase, false)
386}
387
388/// As [`decode_into`], and the decoded bytes must be UTF-8 (overlong forms
389/// and surrogates refused).
390///
391/// # Errors
392///
393/// [`ErrorKind::InvalidUtf8`], and as [`decode_into`]. Nothing is written on
394/// error.
395///
396/// # Examples
397///
398/// ```
399/// use parse_net::{pct::decode_utf8_into, ErrorKind};
400///
401/// let mut buf = [0u8; 8];
402/// assert_eq!(decode_utf8_into(b"%E2%82%AC", &mut buf)?, "\u{20ac}");
403/// assert_eq!(decode_utf8_into(b"%C0%AF", &mut buf).unwrap_err().kind(), ErrorKind::InvalidUtf8);
404/// # Ok::<(), parse_net::ParseError>(())
405/// ```
406pub fn decode_utf8_into<'b>(input: &[u8], out: &'b mut [u8]) -> Result<&'b str> {
407    let bytes = decode_with(input, out, Accept::AnyCase, true)?;
408    core::str::from_utf8(bytes)
409        .map_err(|e| crate::ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
410}
411
412/// As [`decode_into`], refusing uppercase hex (RFC 9651 section 4.2.10).
413///
414/// # Errors
415///
416/// [`ErrorKind::InvalidPercentEncoding`] for uppercase hex or a broken
417/// triple; [`ErrorKind::BufferTooSmall`]. Nothing is written on error.
418///
419/// # Examples
420///
421/// ```
422/// use parse_net::pct::decode_lower_only_into;
423///
424/// let mut buf = [0u8; 4];
425/// assert_eq!(decode_lower_only_into(b"%c3%a9", &mut buf)?, b"\xc3\xa9");
426/// assert!(decode_lower_only_into(b"%C3%A9", &mut buf).is_err());
427/// # Ok::<(), parse_net::ParseError>(())
428/// ```
429pub fn decode_lower_only_into<'b>(input: &[u8], out: &'b mut [u8]) -> Result<&'b [u8]> {
430    decode_with(input, out, Accept::LowerOnly, false)
431}
432
433#[inline]
434fn must_encode(b: u8, set: &EncodeSet) -> bool {
435    b >= 0x80 || set.contains(b)
436}
437
438/// The length of `input` once encoded with `set`.
439///
440/// # Examples
441///
442/// ```
443/// use parse_net::pct::{encoded_len, EncodeSet};
444///
445/// assert_eq!(encoded_len(b"a b", &EncodeSet::empty().add(b' ')), 5);
446/// ```
447#[must_use]
448pub fn encoded_len(input: &[u8], set: &EncodeSet) -> usize {
449    input
450        .iter()
451        .map(|&b| if must_encode(b, set) { 3 } else { 1 })
452        .fold(0usize, usize::saturating_add)
453}
454
455/// Writes `input` with every byte in `set`, and every byte above 0x7F, as
456/// `%XX`.
457///
458/// # Errors
459///
460/// [`WriteError::Full`]; nothing is written when the sink reports its room.
461///
462/// # Examples
463///
464/// ```
465/// use parse_net::{pct::{encode, EncodeSet, HexCase}, SliceSink};
466///
467/// let mut buf = [0u8; 16];
468/// let mut out = SliceSink::new(&mut buf);
469/// encode("caf\u{e9}".as_bytes(), &EncodeSet::SF_DISPLAY, HexCase::Lower, &mut out).expect("room");
470/// assert_eq!(out.written(), b"caf%c3%a9");
471/// ```
472pub fn encode(
473    input: &[u8],
474    set: &EncodeSet,
475    case: HexCase,
476    out: &mut impl Sink,
477) -> core::result::Result<(), WriteError> {
478    reserve(out, encoded_len(input, set))?;
479    let digits: &[u8; 16] = match case {
480        HexCase::Upper => b"0123456789ABCDEF",
481        HexCase::Lower => b"0123456789abcdef",
482    };
483    let mut rest = input;
484    while !rest.is_empty() {
485        let plain = rest.iter().take_while(|&&b| !must_encode(b, set)).count();
486        let (literal, tail) = rest.split_at(plain);
487        out.put(literal)?;
488        if let Some((&b, after)) = tail.split_first() {
489            let hi = digits.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
490            let lo = digits.get(usize::from(b & 15)).copied().unwrap_or(b'0');
491            out.put(&[b'%', hi, lo])?;
492            rest = after;
493        } else {
494            rest = &[];
495        }
496    }
497    Ok(())
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503    use crate::write::SliceSink;
504
505    #[test]
506    fn test_pct_edge_cases() {
507        let mut buf = [0u8; 8];
508        for bad in [&b"%"[..], b"%4", b"%4g", b"a%", b"%g4"] {
509            assert_eq!(
510                validate(bad).unwrap_err().kind(),
511                ErrorKind::InvalidPercentEncoding
512            );
513            assert_eq!(
514                decode_into(bad, &mut buf).unwrap_err().kind(),
515                ErrorKind::InvalidPercentEncoding
516            );
517        }
518        assert_eq!(decode_into(b"%41", &mut buf).unwrap(), b"A");
519        assert_eq!(decode_into(b"%c3%a9", &mut buf).unwrap(), b"\xc3\xa9");
520        assert_eq!(decode_into(b"%C3%A9", &mut buf).unwrap(), b"\xc3\xa9");
521        assert_eq!(decode_into(b"%00", &mut buf).unwrap(), b"\x00");
522        assert_eq!(
523            decode_utf8_into(b"%c3", &mut buf).unwrap_err().kind(),
524            ErrorKind::InvalidUtf8
525        );
526        assert_eq!(
527            decode_utf8_into(b"%ed%a0%80", &mut buf).unwrap_err().kind(),
528            ErrorKind::InvalidUtf8
529        );
530        let e = decode_lower_only_into(b"ab%2F", &mut buf).unwrap_err();
531        assert_eq!(
532            (e.kind(), e.offset()),
533            (ErrorKind::InvalidPercentEncoding, 2)
534        );
535    }
536
537    #[test]
538    fn test_pct_decode_buffer_one_short_writes_nothing() {
539        let mut buf = [0xaau8; 2];
540        assert_eq!(
541            decode_into(b"a%20b", &mut buf).unwrap_err().kind(),
542            ErrorKind::BufferTooSmall
543        );
544        assert_eq!(buf, [0xaa, 0xaa]);
545        let mut buf = [0xaau8; 3];
546        assert_eq!(
547            decode_utf8_into(b"a%c3", &mut buf).unwrap_err().kind(),
548            ErrorKind::InvalidUtf8
549        );
550        assert_eq!(buf, [0xaa; 3]);
551    }
552
553    #[test]
554    fn test_validate_with_target_matches_http_net() {
555        assert!(validate_with(b"/a/b?c=d&e=%20", Class::URI_TARGET).is_ok());
556        assert!(validate_with(b"/a%2", Class::URI_TARGET).is_err());
557        assert!(validate_with(b"/a%zz", Class::URI_TARGET).is_err());
558        assert!(validate_with(b"/a b", Class::URI_TARGET).is_err());
559        assert!(validate_with(b"/a#frag", Class::URI_TARGET).is_err());
560        assert_eq!(
561            validate_with(b"/a\x7f", Class::URI_TARGET)
562                .unwrap_err()
563                .kind(),
564            ErrorKind::Control
565        );
566    }
567
568    #[test]
569    fn test_encode_sets_and_round_trip() {
570        for b in 0u8..=255 {
571            assert_eq!(
572                EncodeSet::ATTR_CHAR_COMPLEMENT.contains(b),
573                !class::is_attr_char(b)
574            );
575            assert_eq!(
576                EncodeSet::CONTROLS_AND_NON_ASCII.contains(b),
577                !(0x20..0x7f).contains(&b)
578            );
579            assert!(EncodeSet::empty().add(b).contains(b));
580            assert!(!EncodeSet::SF_DISPLAY.remove(b).contains(b));
581        }
582        let all: std::vec::Vec<u8> = (0u8..=255).collect();
583        let mut buf = [0u8; 1024];
584        let mut out = SliceSink::new(&mut buf);
585        encode(
586            &all,
587            &EncodeSet::ATTR_CHAR_COMPLEMENT,
588            HexCase::Upper,
589            &mut out,
590        )
591        .unwrap();
592        assert_eq!(
593            out.len(),
594            encoded_len(&all, &EncodeSet::ATTR_CHAR_COMPLEMENT)
595        );
596        let mut back = [0u8; 256];
597        assert_eq!(decode_into(out.written(), &mut back).unwrap(), &all[..]);
598    }
599
600    #[test]
601    fn test_encode_full_writes_nothing() {
602        let mut buf = [0u8; 4];
603        let mut out = SliceSink::new(&mut buf);
604        assert_eq!(
605            encode(
606                b"a b",
607                &EncodeSet::empty().add(b' '),
608                HexCase::Upper,
609                &mut out
610            ),
611            Err(WriteError::Full)
612        );
613        assert!(out.is_empty());
614    }
615}