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 // Bytes above 0x7E and C0 controls go out encoded whatever the set says,
436 // so no encoder can emit CR, LF, or NUL.
437 !(0x20..0x7f).contains(&b) || set.contains(b)
438}
439
440/// The length of `input` once encoded with `set`.
441///
442/// # Examples
443///
444/// ```
445/// use parse_net::pct::{encoded_len, EncodeSet};
446///
447/// assert_eq!(encoded_len(b"a b", &EncodeSet::empty().add(b' ')), 5);
448/// ```
449#[must_use]
450pub fn encoded_len(input: &[u8], set: &EncodeSet) -> usize {
451 input
452 .iter()
453 .map(|&b| if must_encode(b, set) { 3 } else { 1 })
454 .fold(0usize, usize::saturating_add)
455}
456
457/// Writes `input` with every byte in `set`, and every byte above 0x7F, as
458/// `%XX`.
459///
460/// # Errors
461///
462/// [`WriteError::Full`]; nothing is written when the sink reports its room.
463///
464/// # Examples
465///
466/// ```
467/// use parse_net::{pct::{encode, EncodeSet, HexCase}, SliceSink};
468///
469/// let mut buf = [0u8; 16];
470/// let mut out = SliceSink::new(&mut buf);
471/// encode("caf\u{e9}".as_bytes(), &EncodeSet::SF_DISPLAY, HexCase::Lower, &mut out).expect("room");
472/// assert_eq!(out.written(), b"caf%c3%a9");
473/// ```
474pub fn encode(
475 input: &[u8],
476 set: &EncodeSet,
477 case: HexCase,
478 out: &mut impl Sink,
479) -> core::result::Result<(), WriteError> {
480 reserve(out, encoded_len(input, set))?;
481 let digits: &[u8; 16] = match case {
482 HexCase::Upper => b"0123456789ABCDEF",
483 HexCase::Lower => b"0123456789abcdef",
484 };
485 let mut rest = input;
486 while !rest.is_empty() {
487 let plain = rest.iter().take_while(|&&b| !must_encode(b, set)).count();
488 let (literal, tail) = rest.split_at(plain);
489 out.put(literal)?;
490 if let Some((&b, after)) = tail.split_first() {
491 let hi = digits.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
492 let lo = digits.get(usize::from(b & 15)).copied().unwrap_or(b'0');
493 out.put(&[b'%', hi, lo])?;
494 rest = after;
495 } else {
496 rest = &[];
497 }
498 }
499 Ok(())
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505 use crate::write::SliceSink;
506
507 #[test]
508 fn test_pct_edge_cases() {
509 let mut buf = [0u8; 8];
510 for bad in [&b"%"[..], b"%4", b"%4g", b"a%", b"%g4"] {
511 assert_eq!(
512 validate(bad).unwrap_err().kind(),
513 ErrorKind::InvalidPercentEncoding
514 );
515 assert_eq!(
516 decode_into(bad, &mut buf).unwrap_err().kind(),
517 ErrorKind::InvalidPercentEncoding
518 );
519 }
520 assert_eq!(decode_into(b"%41", &mut buf).unwrap(), b"A");
521 assert_eq!(decode_into(b"%c3%a9", &mut buf).unwrap(), b"\xc3\xa9");
522 assert_eq!(decode_into(b"%C3%A9", &mut buf).unwrap(), b"\xc3\xa9");
523 assert_eq!(decode_into(b"%00", &mut buf).unwrap(), b"\x00");
524 assert_eq!(
525 decode_utf8_into(b"%c3", &mut buf).unwrap_err().kind(),
526 ErrorKind::InvalidUtf8
527 );
528 assert_eq!(
529 decode_utf8_into(b"%ed%a0%80", &mut buf).unwrap_err().kind(),
530 ErrorKind::InvalidUtf8
531 );
532 let e = decode_lower_only_into(b"ab%2F", &mut buf).unwrap_err();
533 assert_eq!(
534 (e.kind(), e.offset()),
535 (ErrorKind::InvalidPercentEncoding, 2)
536 );
537 }
538
539 #[test]
540 fn test_pct_decode_buffer_one_short_writes_nothing() {
541 let mut buf = [0xaau8; 2];
542 assert_eq!(
543 decode_into(b"a%20b", &mut buf).unwrap_err().kind(),
544 ErrorKind::BufferTooSmall
545 );
546 assert_eq!(buf, [0xaa, 0xaa]);
547 let mut buf = [0xaau8; 3];
548 assert_eq!(
549 decode_utf8_into(b"a%c3", &mut buf).unwrap_err().kind(),
550 ErrorKind::InvalidUtf8
551 );
552 assert_eq!(buf, [0xaa; 3]);
553 }
554
555 #[test]
556 fn test_validate_with_target_matches_http_net() {
557 assert!(validate_with(b"/a/b?c=d&e=%20", Class::URI_TARGET).is_ok());
558 assert!(validate_with(b"/a%2", Class::URI_TARGET).is_err());
559 assert!(validate_with(b"/a%zz", Class::URI_TARGET).is_err());
560 assert!(validate_with(b"/a b", Class::URI_TARGET).is_err());
561 assert!(validate_with(b"/a#frag", Class::URI_TARGET).is_err());
562 assert_eq!(
563 validate_with(b"/a\x7f", Class::URI_TARGET)
564 .unwrap_err()
565 .kind(),
566 ErrorKind::Control
567 );
568 }
569
570 #[test]
571 fn test_encode_sets_and_round_trip() {
572 for b in 0u8..=255 {
573 assert_eq!(
574 EncodeSet::ATTR_CHAR_COMPLEMENT.contains(b),
575 !class::is_attr_char(b)
576 );
577 assert_eq!(
578 EncodeSet::CONTROLS_AND_NON_ASCII.contains(b),
579 !(0x20..0x7f).contains(&b)
580 );
581 assert!(EncodeSet::empty().add(b).contains(b));
582 assert!(!EncodeSet::SF_DISPLAY.remove(b).contains(b));
583 }
584 let all: std::vec::Vec<u8> = (0u8..=255).collect();
585 let mut buf = [0u8; 1024];
586 let mut out = SliceSink::new(&mut buf);
587 encode(
588 &all,
589 &EncodeSet::ATTR_CHAR_COMPLEMENT,
590 HexCase::Upper,
591 &mut out,
592 )
593 .unwrap();
594 assert_eq!(
595 out.len(),
596 encoded_len(&all, &EncodeSet::ATTR_CHAR_COMPLEMENT)
597 );
598 let mut back = [0u8; 256];
599 assert_eq!(decode_into(out.written(), &mut back).unwrap(), &all[..]);
600 }
601
602 #[test]
603 fn test_encode_full_writes_nothing() {
604 let mut buf = [0u8; 4];
605 let mut out = SliceSink::new(&mut buf);
606 assert_eq!(
607 encode(
608 b"a b",
609 &EncodeSet::empty().add(b' '),
610 HexCase::Upper,
611 &mut out
612 ),
613 Err(WriteError::Full)
614 );
615 assert!(out.is_empty());
616 }
617}