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    pub const CONTROLS_AND_NON_ASCII: Self = Self(set_of(Named::ControlsAndNonAscii));
88    /// Every byte that is not an `attr-char` (RFC 8187 section 3.2.1).
89    pub const ATTR_CHAR_COMPLEMENT: Self = Self(set_of(Named::AttrCharComplement));
90    /// `%`, `"`, and every byte outside 0x20 to 0x7E (RFC 9651 section 4.1.11).
91    pub const SF_DISPLAY: Self = Self(set_of(Named::SfDisplay));
92
93    /// The empty set (only bytes above 0x7F are then encoded).
94    #[must_use]
95    pub const fn empty() -> Self {
96        Self([0; 4])
97    }
98
99    /// The set with `b` added.
100    #[must_use]
101    pub const fn add(self, b: u8) -> Self {
102        let mut bits = self.0;
103        let word = (b >> 6) as usize;
104        // `word` is 0 to 3.
105        #[allow(clippy::indexing_slicing)]
106        {
107            bits[word] |= 1 << (b & 63);
108        }
109        Self(bits)
110    }
111
112    /// The set with `b` removed.
113    #[must_use]
114    pub const fn remove(self, b: u8) -> Self {
115        let mut bits = self.0;
116        let word = (b >> 6) as usize;
117        // `word` is 0 to 3.
118        #[allow(clippy::indexing_slicing)]
119        {
120            bits[word] &= !(1 << (b & 63));
121        }
122        Self(bits)
123    }
124
125    /// Whether `b` is in the set.
126    #[must_use]
127    pub const fn contains(&self, b: u8) -> bool {
128        let word = (b >> 6) as usize;
129        // `word` is 0 to 3.
130        #[allow(clippy::indexing_slicing)]
131        let w = self.0[word];
132        w & (1 << (b & 63)) != 0
133    }
134}
135
136/// The hex case [`encode`] writes.
137#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
138pub enum HexCase {
139    /// `%2F` (RFC 3986 section 2.1 recommends uppercase).
140    Upper,
141    /// `%2f` (required by RFC 9651 Display Strings).
142    Lower,
143}
144
145/// How the hex digits of a triple may be written.
146#[derive(Clone, Copy, PartialEq, Eq)]
147enum Accept {
148    AnyCase,
149    LowerOnly,
150}
151
152/// Walks `input`, calling `f` with each decoded byte and its offset.
153#[inline]
154fn walk(input: &[u8], accept: Accept, mut f: impl FnMut(u8, usize) -> Result<()>) -> Result<()> {
155    let mut i = 0usize;
156    while let Some(&b) = input.get(i) {
157        if b == b'%' {
158            let hi = input.get(i + 1).copied().and_then(|h| hex(h, accept));
159            let lo = input.get(i + 2).copied().and_then(|h| hex(h, accept));
160            let (Some(hi), Some(lo)) = (hi, lo) else {
161                return err(ErrorKind::InvalidPercentEncoding, i);
162            };
163            f((hi << 4) | lo, i)?;
164            i += 3;
165        } else {
166            f(b, i)?;
167            i += 1;
168        }
169    }
170    Ok(())
171}
172
173#[inline]
174const fn hex(b: u8, accept: Accept) -> Option<u8> {
175    match (b, accept) {
176        (b'A'..=b'F', Accept::LowerOnly) => None,
177        _ => class::hex_value(b),
178    }
179}
180
181/// Checks that every `%` is followed by two hex digits (either case).
182///
183/// # Errors
184///
185/// [`ErrorKind::InvalidPercentEncoding`] at the `%`.
186///
187/// # Examples
188///
189/// ```
190/// use parse_net::pct::validate;
191///
192/// assert!(validate(b"a%20b").is_ok());
193/// assert_eq!(validate(b"a%2").unwrap_err().offset(), 1);
194/// ```
195pub fn validate(input: &[u8]) -> Result<()> {
196    walk(input, Accept::AnyCase, |_, _| Ok(()))
197}
198
199/// As [`validate`], and every byte other than a `%XX` triple is in `allowed`.
200///
201/// `validate_with(target, Class::URI_TARGET)` checks the characters of a
202/// request target.
203///
204/// # Errors
205///
206/// [`ErrorKind::InvalidPercentEncoding`]; a control kind for a control byte;
207/// [`ErrorKind::InvalidByte`] for another byte outside `allowed`.
208///
209/// # Examples
210///
211/// ```
212/// use parse_net::{pct::validate_with, class::Class, ErrorKind};
213///
214/// assert!(validate_with(b"/a/b?c=d&e=%20", Class::URI_TARGET).is_ok());
215/// assert_eq!(validate_with(b"/a#frag", Class::URI_TARGET).unwrap_err().kind(), ErrorKind::InvalidByte);
216/// ```
217pub fn validate_with(input: &[u8], allowed: Class) -> Result<()> {
218    let mut i = 0usize;
219    while let Some(&b) = input.get(i) {
220        if b == b'%' {
221            match (input.get(i + 1), input.get(i + 2)) {
222                (Some(&h), Some(&l)) if class::is_hexdig(h) && class::is_hexdig(l) => i += 3,
223                _ => return err(ErrorKind::InvalidPercentEncoding, i),
224            }
225        } else if allowed.contains(b) {
226            i += 1;
227        } else if crate::scan::is_control(b) {
228            return err(control_kind(b), i);
229        } else {
230            return err(ErrorKind::InvalidByte, i);
231        }
232    }
233    Ok(())
234}
235
236/// Validates `input` and returns its decoded length.
237///
238/// # Errors
239///
240/// [`ErrorKind::InvalidPercentEncoding`].
241///
242/// # Examples
243///
244/// ```
245/// assert_eq!(parse_net::pct::decoded_len(b"a%20b")?, 3);
246/// # Ok::<(), parse_net::ParseError>(())
247/// ```
248pub fn decoded_len(input: &[u8]) -> Result<usize> {
249    let mut n = 0usize;
250    walk(input, Accept::AnyCase, |_, _| {
251        n += 1;
252        Ok(())
253    })?;
254    Ok(n)
255}
256
257fn decode_with<'b>(
258    input: &[u8],
259    out: &'b mut [u8],
260    accept: Accept,
261    utf8: bool,
262) -> Result<&'b [u8]> {
263    // First pass: validate everything and size the output, writing nothing.
264    let mut n = 0usize;
265    let mut v = Utf8::new();
266    walk(input, accept, |b, at| {
267        if utf8 && !v.push(b) {
268            return err(ErrorKind::InvalidUtf8, at);
269        }
270        n += 1;
271        Ok(())
272    })?;
273    if utf8 && !v.is_complete() {
274        return err(ErrorKind::InvalidUtf8, input.len());
275    }
276    let Some(dst) = out.get_mut(..n) else {
277        return err(ErrorKind::BufferTooSmall, 0);
278    };
279    let mut slots = dst.iter_mut();
280    walk(input, accept, |b, _| {
281        if let Some(slot) = slots.next() {
282            *slot = b;
283        }
284        Ok(())
285    })?;
286    Ok(dst)
287}
288
289/// Decodes `%XX` triples (either case) into `out`, copying every other byte,
290/// and returns the written part.
291///
292/// # Errors
293///
294/// [`ErrorKind::InvalidPercentEncoding`]; [`ErrorKind::BufferTooSmall`].
295/// Nothing is written on error.
296///
297/// # Examples
298///
299/// ```
300/// let mut buf = [0u8; 8];
301/// assert_eq!(parse_net::pct::decode_into(b"a%2Fb", &mut buf)?, b"a/b");
302/// # Ok::<(), parse_net::ParseError>(())
303/// ```
304pub fn decode_into<'b>(input: &[u8], out: &'b mut [u8]) -> Result<&'b [u8]> {
305    decode_with(input, out, Accept::AnyCase, false)
306}
307
308/// As [`decode_into`], and the decoded bytes must be UTF-8 (overlong forms
309/// and surrogates refused).
310///
311/// # Errors
312///
313/// [`ErrorKind::InvalidUtf8`], and as [`decode_into`]. Nothing is written on
314/// error.
315///
316/// # Examples
317///
318/// ```
319/// use parse_net::{pct::decode_utf8_into, ErrorKind};
320///
321/// let mut buf = [0u8; 8];
322/// assert_eq!(decode_utf8_into(b"%E2%82%AC", &mut buf)?, "\u{20ac}");
323/// assert_eq!(decode_utf8_into(b"%C0%AF", &mut buf).unwrap_err().kind(), ErrorKind::InvalidUtf8);
324/// # Ok::<(), parse_net::ParseError>(())
325/// ```
326pub fn decode_utf8_into<'b>(input: &[u8], out: &'b mut [u8]) -> Result<&'b str> {
327    let bytes = decode_with(input, out, Accept::AnyCase, true)?;
328    core::str::from_utf8(bytes)
329        .map_err(|e| crate::ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
330}
331
332/// As [`decode_into`], refusing uppercase hex (RFC 9651 section 4.2.10).
333///
334/// # Errors
335///
336/// [`ErrorKind::InvalidPercentEncoding`] for uppercase hex or a broken
337/// triple; [`ErrorKind::BufferTooSmall`]. Nothing is written on error.
338///
339/// # Examples
340///
341/// ```
342/// use parse_net::pct::decode_lower_only_into;
343///
344/// let mut buf = [0u8; 4];
345/// assert_eq!(decode_lower_only_into(b"%c3%a9", &mut buf)?, b"\xc3\xa9");
346/// assert!(decode_lower_only_into(b"%C3%A9", &mut buf).is_err());
347/// # Ok::<(), parse_net::ParseError>(())
348/// ```
349pub fn decode_lower_only_into<'b>(input: &[u8], out: &'b mut [u8]) -> Result<&'b [u8]> {
350    decode_with(input, out, Accept::LowerOnly, false)
351}
352
353#[inline]
354fn must_encode(b: u8, set: &EncodeSet) -> bool {
355    b >= 0x80 || set.contains(b)
356}
357
358/// The length of `input` once encoded with `set`.
359///
360/// # Examples
361///
362/// ```
363/// use parse_net::pct::{encoded_len, EncodeSet};
364///
365/// assert_eq!(encoded_len(b"a b", &EncodeSet::empty().add(b' ')), 5);
366/// ```
367#[must_use]
368pub fn encoded_len(input: &[u8], set: &EncodeSet) -> usize {
369    input
370        .iter()
371        .map(|&b| if must_encode(b, set) { 3 } else { 1 })
372        .fold(0usize, usize::saturating_add)
373}
374
375/// Writes `input` with every byte in `set`, and every byte above 0x7F, as
376/// `%XX`.
377///
378/// # Errors
379///
380/// [`WriteError::Full`]; nothing is written when the sink reports its room.
381///
382/// # Examples
383///
384/// ```
385/// use parse_net::{pct::{encode, EncodeSet, HexCase}, SliceSink};
386///
387/// let mut buf = [0u8; 16];
388/// let mut out = SliceSink::new(&mut buf);
389/// encode("caf\u{e9}".as_bytes(), &EncodeSet::SF_DISPLAY, HexCase::Lower, &mut out).expect("room");
390/// assert_eq!(out.written(), b"caf%c3%a9");
391/// ```
392pub fn encode(
393    input: &[u8],
394    set: &EncodeSet,
395    case: HexCase,
396    out: &mut impl Sink,
397) -> core::result::Result<(), WriteError> {
398    reserve(out, encoded_len(input, set))?;
399    let digits: &[u8; 16] = match case {
400        HexCase::Upper => b"0123456789ABCDEF",
401        HexCase::Lower => b"0123456789abcdef",
402    };
403    let mut rest = input;
404    while !rest.is_empty() {
405        let plain = rest.iter().take_while(|&&b| !must_encode(b, set)).count();
406        let (literal, tail) = rest.split_at(plain);
407        out.put(literal)?;
408        if let Some((&b, after)) = tail.split_first() {
409            let hi = digits.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
410            let lo = digits.get(usize::from(b & 15)).copied().unwrap_or(b'0');
411            out.put(&[b'%', hi, lo])?;
412            rest = after;
413        } else {
414            rest = &[];
415        }
416    }
417    Ok(())
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use crate::write::SliceSink;
424
425    #[test]
426    fn test_pct_edge_cases() {
427        let mut buf = [0u8; 8];
428        for bad in [&b"%"[..], b"%4", b"%4g", b"a%", b"%g4"] {
429            assert_eq!(
430                validate(bad).unwrap_err().kind(),
431                ErrorKind::InvalidPercentEncoding
432            );
433            assert_eq!(
434                decode_into(bad, &mut buf).unwrap_err().kind(),
435                ErrorKind::InvalidPercentEncoding
436            );
437        }
438        assert_eq!(decode_into(b"%41", &mut buf).unwrap(), b"A");
439        assert_eq!(decode_into(b"%c3%a9", &mut buf).unwrap(), b"\xc3\xa9");
440        assert_eq!(decode_into(b"%C3%A9", &mut buf).unwrap(), b"\xc3\xa9");
441        assert_eq!(decode_into(b"%00", &mut buf).unwrap(), b"\x00");
442        assert_eq!(
443            decode_utf8_into(b"%c3", &mut buf).unwrap_err().kind(),
444            ErrorKind::InvalidUtf8
445        );
446        assert_eq!(
447            decode_utf8_into(b"%ed%a0%80", &mut buf).unwrap_err().kind(),
448            ErrorKind::InvalidUtf8
449        );
450        let e = decode_lower_only_into(b"ab%2F", &mut buf).unwrap_err();
451        assert_eq!(
452            (e.kind(), e.offset()),
453            (ErrorKind::InvalidPercentEncoding, 2)
454        );
455    }
456
457    #[test]
458    fn test_pct_decode_buffer_one_short_writes_nothing() {
459        let mut buf = [0xaau8; 2];
460        assert_eq!(
461            decode_into(b"a%20b", &mut buf).unwrap_err().kind(),
462            ErrorKind::BufferTooSmall
463        );
464        assert_eq!(buf, [0xaa, 0xaa]);
465        let mut buf = [0xaau8; 3];
466        assert_eq!(
467            decode_utf8_into(b"a%c3", &mut buf).unwrap_err().kind(),
468            ErrorKind::InvalidUtf8
469        );
470        assert_eq!(buf, [0xaa; 3]);
471    }
472
473    #[test]
474    fn test_validate_with_target_matches_http_net() {
475        assert!(validate_with(b"/a/b?c=d&e=%20", Class::URI_TARGET).is_ok());
476        assert!(validate_with(b"/a%2", Class::URI_TARGET).is_err());
477        assert!(validate_with(b"/a%zz", Class::URI_TARGET).is_err());
478        assert!(validate_with(b"/a b", Class::URI_TARGET).is_err());
479        assert!(validate_with(b"/a#frag", Class::URI_TARGET).is_err());
480        assert_eq!(
481            validate_with(b"/a\x7f", Class::URI_TARGET)
482                .unwrap_err()
483                .kind(),
484            ErrorKind::Control
485        );
486    }
487
488    #[test]
489    fn test_encode_sets_and_round_trip() {
490        for b in 0u8..=255 {
491            assert_eq!(
492                EncodeSet::ATTR_CHAR_COMPLEMENT.contains(b),
493                !class::is_attr_char(b)
494            );
495            assert_eq!(
496                EncodeSet::CONTROLS_AND_NON_ASCII.contains(b),
497                !(0x20..0x7f).contains(&b)
498            );
499            assert!(EncodeSet::empty().add(b).contains(b));
500            assert!(!EncodeSet::SF_DISPLAY.remove(b).contains(b));
501        }
502        let all: std::vec::Vec<u8> = (0u8..=255).collect();
503        let mut buf = [0u8; 1024];
504        let mut out = SliceSink::new(&mut buf);
505        encode(
506            &all,
507            &EncodeSet::ATTR_CHAR_COMPLEMENT,
508            HexCase::Upper,
509            &mut out,
510        )
511        .unwrap();
512        assert_eq!(
513            out.len(),
514            encoded_len(&all, &EncodeSet::ATTR_CHAR_COMPLEMENT)
515        );
516        let mut back = [0u8; 256];
517        assert_eq!(decode_into(out.written(), &mut back).unwrap(), &all[..]);
518    }
519
520    #[test]
521    fn test_encode_full_writes_nothing() {
522        let mut buf = [0u8; 4];
523        let mut out = SliceSink::new(&mut buf);
524        assert_eq!(
525            encode(
526                b"a b",
527                &EncodeSet::empty().add(b' '),
528                HexCase::Upper,
529                &mut out
530            ),
531            Err(WriteError::Full)
532        );
533        assert!(out.is_empty());
534    }
535}