use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PercentEncoded {
pub byte: u8,
}
impl PercentEncoded {
pub const BYTES_LENGTH: usize = 3;
#[cfg(test)]
pub const fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidPercentEncoded> {
Self::from_bytes_at_index(bytes, 0)
}
pub const fn from_bytes_at_index(
bytes: &[u8],
index: usize,
) -> Result<Self, InvalidPercentEncoded> {
if index + 2 >= bytes.len() {
return Err(InvalidPercentEncoded { hex_digits: None });
}
Self::from_hex_digits([bytes[index + 1], bytes[index + 2]])
}
pub const fn from_hex_digits(hex_digits: [u8; 2]) -> Result<Self, InvalidPercentEncoded> {
let invalid = Err(InvalidPercentEncoded {
hex_digits: Some(hex_digits),
});
let hex_byte_0 = match hex_digit_to_byte(hex_digits[0]) {
Some(b) => b,
None => return invalid,
};
let hex_byte_1 = match hex_digit_to_byte(hex_digits[1]) {
Some(b) => b,
None => return invalid,
};
Ok(Self {
byte: hex_byte_0 * 16 + hex_byte_1,
})
}
}
const fn hex_digit_to_byte(hex_digit: u8) -> Option<u8> {
Some(match hex_digit {
b'0' => 0,
b'1' => 1,
b'2' => 2,
b'3' => 3,
b'4' => 4,
b'5' => 5,
b'6' => 6,
b'7' => 7,
b'8' => 8,
b'9' => 9,
b'A' => 10,
b'B' => 11,
b'C' => 12,
b'D' => 13,
b'E' => 14,
b'F' => 15,
_ => return None,
})
}
#[derive(Debug, Clone)]
pub(crate) struct InvalidPercentEncoded {
hex_digits: Option<[u8; 2]>,
}
impl fmt::Display for InvalidPercentEncoded {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "percent encoding ")?;
match self.hex_digits {
Some([a, b]) => write!(f, "%{}{}", char::from(a), char::from(b))?,
None => write!(f, "%")?,
};
write!(f, " is not valid")
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
#[test]
fn percent_1_g() {
assert_matches!(
PercentEncoded::from_bytes(b"%1G"),
Err(InvalidPercentEncoded {
hex_digits: Some([b'1', b'G']),
})
);
}
#[test]
fn space_percent_1_g() {
assert_matches!(
PercentEncoded::from_bytes_at_index(b" %1G", 1),
Err(InvalidPercentEncoded {
hex_digits: Some([b'1', b'G']),
})
);
}
#[test]
fn percent_f_f() {
let actual = PercentEncoded::from_bytes(b"%FF").unwrap().byte;
assert_eq!(actual, u8::MAX);
}
#[test]
fn percent_1_2() {
let actual = PercentEncoded::from_bytes(b"%12").unwrap().byte;
assert_eq!(actual, 18);
}
#[test]
fn space_percent_f_f() {
let actual = PercentEncoded::from_bytes_at_index(b" %FF", 1)
.unwrap()
.byte;
assert_eq!(actual, u8::MAX);
}
#[test]
fn percent_f_f_lower_case() {
assert_matches!(
PercentEncoded::from_bytes(b"%ff"),
Err(InvalidPercentEncoded {
hex_digits: Some([b'f', b'f']),
})
);
}
#[test]
fn negative_sign_is_error() {
assert_matches!(
PercentEncoded::from_bytes(b"%-A"),
Err(InvalidPercentEncoded {
hex_digits: Some([b'-', b'A']),
})
);
}
#[test]
fn positive_sign_is_error() {
assert_matches!(
PercentEncoded::from_bytes(b"%+B"),
Err(InvalidPercentEncoded {
hex_digits: Some([b'+', b'B']),
})
);
}
#[test]
fn qc_byte_equals_from_str_radix() {
quickcheck::quickcheck(test_byte_equals_from_str_radix as fn(u8, u8))
}
fn test_byte_equals_from_str_radix(byte_a: u8, byte_b: u8) {
if matches!(char::from(byte_a), '+' | '-')
|| byte_a.is_ascii_lowercase()
|| byte_b.is_ascii_lowercase()
{
return;
}
let bytes = [byte_a, byte_b];
let percent_encoded_bytes = Vec::from([b'%', byte_a, byte_b]);
let expected = std::str::from_utf8(&bytes)
.ok()
.and_then(|s| u8::from_str_radix(s, 16).ok());
let actual = PercentEncoded::from_bytes(&percent_encoded_bytes)
.ok()
.map(|x| x.byte);
assert_eq!(actual, expected);
}
}