use crate::{
class::{self, Class},
error::{ErrorKind, Result, WriteError, err},
scan::control_kind,
utf8::Utf8,
write::{Sink, reserve},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct EncodeSet([u64; 4]);
#[derive(Clone, Copy)]
enum Named {
ControlsAndNonAscii,
AttrCharComplement,
SfDisplay,
}
const fn in_named(b: u8, which: Named) -> bool {
match which {
Named::ControlsAndNonAscii => b < 0x20 || b >= 0x7f,
Named::AttrCharComplement => !class::is_attr_char(b),
Named::SfDisplay => b == b'%' || b == b'"' || b < 0x20 || b > 0x7e,
}
}
const fn set_of(which: Named) -> [u64; 4] {
let mut bits = [0u64; 4];
let mut b: u8 = 0;
loop {
if in_named(b, which) {
let word = (b >> 6) as usize;
#[allow(clippy::indexing_slicing)]
{
bits[word] |= 1 << (b & 63);
}
}
if b == u8::MAX {
break;
}
b += 1;
}
bits
}
impl EncodeSet {
pub const CONTROLS_AND_NON_ASCII: Self = Self(set_of(Named::ControlsAndNonAscii));
pub const ATTR_CHAR_COMPLEMENT: Self = Self(set_of(Named::AttrCharComplement));
pub const SF_DISPLAY: Self = Self(set_of(Named::SfDisplay));
#[must_use]
pub const fn empty() -> Self {
Self([0; 4])
}
#[must_use]
pub const fn add(self, b: u8) -> Self {
let mut bits = self.0;
let word = (b >> 6) as usize;
#[allow(clippy::indexing_slicing)]
{
bits[word] |= 1 << (b & 63);
}
Self(bits)
}
#[must_use]
pub const fn remove(self, b: u8) -> Self {
let mut bits = self.0;
let word = (b >> 6) as usize;
#[allow(clippy::indexing_slicing)]
{
bits[word] &= !(1 << (b & 63));
}
Self(bits)
}
#[must_use]
pub const fn contains(&self, b: u8) -> bool {
let word = (b >> 6) as usize;
#[allow(clippy::indexing_slicing)]
let w = self.0[word];
w & (1 << (b & 63)) != 0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum HexCase {
Upper,
Lower,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Accept {
AnyCase,
LowerOnly,
}
#[inline]
fn walk(input: &[u8], accept: Accept, mut f: impl FnMut(u8, usize) -> Result<()>) -> Result<()> {
let mut i = 0usize;
while let Some(&b) = input.get(i) {
if b == b'%' {
let hi = input.get(i + 1).copied().and_then(|h| hex(h, accept));
let lo = input.get(i + 2).copied().and_then(|h| hex(h, accept));
let (Some(hi), Some(lo)) = (hi, lo) else {
return err(ErrorKind::InvalidPercentEncoding, i);
};
f((hi << 4) | lo, i)?;
i += 3;
} else {
f(b, i)?;
i += 1;
}
}
Ok(())
}
#[inline]
const fn hex(b: u8, accept: Accept) -> Option<u8> {
match (b, accept) {
(b'A'..=b'F', Accept::LowerOnly) => None,
_ => class::hex_value(b),
}
}
pub fn validate(input: &[u8]) -> Result<()> {
walk(input, Accept::AnyCase, |_, _| Ok(()))
}
pub fn validate_with(input: &[u8], allowed: Class) -> Result<()> {
let mut i = 0usize;
while let Some(&b) = input.get(i) {
if b == b'%' {
match (input.get(i + 1), input.get(i + 2)) {
(Some(&h), Some(&l)) if class::is_hexdig(h) && class::is_hexdig(l) => i += 3,
_ => return err(ErrorKind::InvalidPercentEncoding, i),
}
} else if allowed.contains(b) {
i += 1;
} else if crate::scan::is_control(b) {
return err(control_kind(b), i);
} else {
return err(ErrorKind::InvalidByte, i);
}
}
Ok(())
}
pub fn decoded_len(input: &[u8]) -> Result<usize> {
let mut n = 0usize;
walk(input, Accept::AnyCase, |_, _| {
n += 1;
Ok(())
})?;
Ok(n)
}
fn decode_with<'b>(
input: &[u8],
out: &'b mut [u8],
accept: Accept,
utf8: bool,
) -> Result<&'b [u8]> {
let mut n = 0usize;
let mut v = Utf8::new();
walk(input, accept, |b, at| {
if utf8 && !v.push(b) {
return err(ErrorKind::InvalidUtf8, at);
}
n += 1;
Ok(())
})?;
if utf8 && !v.is_complete() {
return err(ErrorKind::InvalidUtf8, input.len());
}
let Some(dst) = out.get_mut(..n) else {
return err(ErrorKind::BufferTooSmall, 0);
};
let mut slots = dst.iter_mut();
walk(input, accept, |b, _| {
if let Some(slot) = slots.next() {
*slot = b;
}
Ok(())
})?;
Ok(dst)
}
pub fn decode_into<'b>(input: &[u8], out: &'b mut [u8]) -> Result<&'b [u8]> {
decode_with(input, out, Accept::AnyCase, false)
}
pub fn decode_utf8_into<'b>(input: &[u8], out: &'b mut [u8]) -> Result<&'b str> {
let bytes = decode_with(input, out, Accept::AnyCase, true)?;
core::str::from_utf8(bytes)
.map_err(|e| crate::ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
}
pub fn decode_lower_only_into<'b>(input: &[u8], out: &'b mut [u8]) -> Result<&'b [u8]> {
decode_with(input, out, Accept::LowerOnly, false)
}
#[inline]
fn must_encode(b: u8, set: &EncodeSet) -> bool {
!(0x20..0x7f).contains(&b) || set.contains(b)
}
#[must_use]
pub fn encoded_len(input: &[u8], set: &EncodeSet) -> usize {
input
.iter()
.map(|&b| if must_encode(b, set) { 3 } else { 1 })
.fold(0usize, usize::saturating_add)
}
pub fn encode(
input: &[u8],
set: &EncodeSet,
case: HexCase,
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
reserve(out, encoded_len(input, set))?;
let digits: &[u8; 16] = match case {
HexCase::Upper => b"0123456789ABCDEF",
HexCase::Lower => b"0123456789abcdef",
};
let mut rest = input;
while !rest.is_empty() {
let plain = rest.iter().take_while(|&&b| !must_encode(b, set)).count();
let (literal, tail) = rest.split_at(plain);
out.put(literal)?;
if let Some((&b, after)) = tail.split_first() {
let hi = digits.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
let lo = digits.get(usize::from(b & 15)).copied().unwrap_or(b'0');
out.put(&[b'%', hi, lo])?;
rest = after;
} else {
rest = &[];
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::write::SliceSink;
#[test]
fn test_pct_edge_cases() {
let mut buf = [0u8; 8];
for bad in [&b"%"[..], b"%4", b"%4g", b"a%", b"%g4"] {
assert_eq!(
validate(bad).unwrap_err().kind(),
ErrorKind::InvalidPercentEncoding
);
assert_eq!(
decode_into(bad, &mut buf).unwrap_err().kind(),
ErrorKind::InvalidPercentEncoding
);
}
assert_eq!(decode_into(b"%41", &mut buf).unwrap(), b"A");
assert_eq!(decode_into(b"%c3%a9", &mut buf).unwrap(), b"\xc3\xa9");
assert_eq!(decode_into(b"%C3%A9", &mut buf).unwrap(), b"\xc3\xa9");
assert_eq!(decode_into(b"%00", &mut buf).unwrap(), b"\x00");
assert_eq!(
decode_utf8_into(b"%c3", &mut buf).unwrap_err().kind(),
ErrorKind::InvalidUtf8
);
assert_eq!(
decode_utf8_into(b"%ed%a0%80", &mut buf).unwrap_err().kind(),
ErrorKind::InvalidUtf8
);
let e = decode_lower_only_into(b"ab%2F", &mut buf).unwrap_err();
assert_eq!(
(e.kind(), e.offset()),
(ErrorKind::InvalidPercentEncoding, 2)
);
}
#[test]
fn test_pct_decode_buffer_one_short_writes_nothing() {
let mut buf = [0xaau8; 2];
assert_eq!(
decode_into(b"a%20b", &mut buf).unwrap_err().kind(),
ErrorKind::BufferTooSmall
);
assert_eq!(buf, [0xaa, 0xaa]);
let mut buf = [0xaau8; 3];
assert_eq!(
decode_utf8_into(b"a%c3", &mut buf).unwrap_err().kind(),
ErrorKind::InvalidUtf8
);
assert_eq!(buf, [0xaa; 3]);
}
#[test]
fn test_validate_with_target_matches_http_net() {
assert!(validate_with(b"/a/b?c=d&e=%20", Class::URI_TARGET).is_ok());
assert!(validate_with(b"/a%2", Class::URI_TARGET).is_err());
assert!(validate_with(b"/a%zz", Class::URI_TARGET).is_err());
assert!(validate_with(b"/a b", Class::URI_TARGET).is_err());
assert!(validate_with(b"/a#frag", Class::URI_TARGET).is_err());
assert_eq!(
validate_with(b"/a\x7f", Class::URI_TARGET)
.unwrap_err()
.kind(),
ErrorKind::Control
);
}
#[test]
fn test_encode_sets_and_round_trip() {
for b in 0u8..=255 {
assert_eq!(
EncodeSet::ATTR_CHAR_COMPLEMENT.contains(b),
!class::is_attr_char(b)
);
assert_eq!(
EncodeSet::CONTROLS_AND_NON_ASCII.contains(b),
!(0x20..0x7f).contains(&b)
);
assert!(EncodeSet::empty().add(b).contains(b));
assert!(!EncodeSet::SF_DISPLAY.remove(b).contains(b));
}
let all: std::vec::Vec<u8> = (0u8..=255).collect();
let mut buf = [0u8; 1024];
let mut out = SliceSink::new(&mut buf);
encode(
&all,
&EncodeSet::ATTR_CHAR_COMPLEMENT,
HexCase::Upper,
&mut out,
)
.unwrap();
assert_eq!(
out.len(),
encoded_len(&all, &EncodeSet::ATTR_CHAR_COMPLEMENT)
);
let mut back = [0u8; 256];
assert_eq!(decode_into(out.written(), &mut back).unwrap(), &all[..]);
}
#[test]
fn test_encode_full_writes_nothing() {
let mut buf = [0u8; 4];
let mut out = SliceSink::new(&mut buf);
assert_eq!(
encode(
b"a b",
&EncodeSet::empty().add(b' '),
HexCase::Upper,
&mut out
),
Err(WriteError::Full)
);
assert!(out.is_empty());
}
}