pub struct Quoter {
protected_table: AsciiBitmap,
}
impl Quoter {
pub fn new(_: &[u8], protected: &[u8]) -> Quoter {
let mut protected_table = AsciiBitmap::default();
for &ch in protected {
protected_table.set_bit(ch);
}
Quoter { protected_table }
}
#[inline(always)]
fn decode_next<'a>(&self, val: &mut &'a [u8]) -> Option<(&'a [u8], u8)> {
for i in 0..val.len() {
if let (prev, [b'%', p1, p2, rem @ ..]) = val.split_at(i) {
if let Some(ch) = hex_pair_to_char(*p1, *p2)
.filter(|&ch| !(ch < 128 && self.protected_table.bit_at(ch)))
{
*val = rem;
return Some((prev, ch));
}
}
}
None
}
pub fn requote(&self, val: &[u8]) -> Option<Vec<u8>> {
let mut remaining = val;
let (pre, decoded_char) = self.decode_next(&mut remaining)?;
let mut decoded = Vec::<u8>::with_capacity(val.len());
decoded.extend_from_slice(pre);
decoded.push(decoded_char);
while let Some((prev, ch)) = self.decode_next(&mut remaining) {
if !prev.is_empty() {
decoded.extend_from_slice(prev);
}
decoded.push(ch);
}
decoded.extend_from_slice(remaining);
Some(decoded)
}
pub(crate) fn requote_str_lossy(&self, val: &str) -> Option<String> {
self.requote(val.as_bytes())
.map(|data| String::from_utf8_lossy(&data).into_owned())
}
}
#[inline(always)]
fn hex_pair_to_char(d1: u8, d2: u8) -> Option<u8> {
let d_high = char::from(d1).to_digit(16)?;
let d_low = char::from(d2).to_digit(16)?;
Some(((d_high as u8) << 4) | (d_low as u8))
}
#[derive(Debug, Default, Clone)]
struct AsciiBitmap {
array: [u8; 16],
}
impl AsciiBitmap {
fn set_bit(&mut self, ch: u8) {
self.array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111)
}
fn bit_at(&self, ch: u8) -> bool {
self.array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn custom_quoter() {
let q = Quoter::new(b"", b"+");
assert_eq!(q.requote(b"/a%25c").unwrap(), b"/a%c");
assert_eq!(q.requote(b"/a%2Bc"), None);
let q = Quoter::new(b"%+", b"/");
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), b"/a%b+c");
assert_eq!(q.requote(b"/a%2fb"), None);
assert_eq!(q.requote(b"/a%2Fb"), None);
assert_eq!(q.requote(b"/a%0Ab").unwrap(), b"/a\nb");
assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb");
assert_eq!(q.requote(b"/a\xfe\xffb"), None);
}
#[test]
fn non_ascii() {
let q = Quoter::new(b"%+", b"/");
assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb");
assert_eq!(q.requote(b"/a\xfe\xffb"), None);
}
#[test]
fn invalid_sequences() {
let q = Quoter::new(b"%+", b"/");
assert_eq!(q.requote(b"/a%2x%2X%%"), None);
assert_eq!(q.requote(b"/a%20%2X%%").unwrap(), b"/a %2X%%");
}
#[test]
fn quoter_no_modification() {
let q = Quoter::new(b"", b"");
assert_eq!(q.requote(b"/abc/../efg"), None);
}
}