use crate::{
Codec,
MiscCodecError,
MiscCodecResult,
ValueDecoder,
ValueEncoder,
};
const UPPER_HEX_DIGITS: [char; 16] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
'F',
];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PercentCodec;
impl PercentCodec {
#[inline]
pub fn new() -> Self {
Self
}
#[inline]
pub fn encode(&self, text: &str) -> String {
percent_encode_bytes(text.as_bytes(), false)
}
#[inline]
pub fn decode(&self, text: &str) -> MiscCodecResult<String> {
String::from_utf8(percent_decode_bytes(text, false)?)
.map_err(MiscCodecError::from)
}
}
impl ValueEncoder<str> for PercentCodec {
type Error = MiscCodecError;
type Output = String;
#[inline]
fn encode(&self, input: &str) -> Result<Self::Output, Self::Error> {
Ok(PercentCodec::encode(self, input))
}
}
impl ValueDecoder<str> for PercentCodec {
type Error = MiscCodecError;
type Output = String;
#[inline]
fn decode(&self, input: &str) -> Result<Self::Output, Self::Error> {
PercentCodec::decode(self, input)
}
}
unsafe impl Codec for PercentCodec {
type Value = u8;
type Unit = u8;
type DecodeError = MiscCodecError;
type EncodeError = MiscCodecError;
#[inline(always)]
fn min_units_per_value(&self) -> core::num::NonZeroUsize {
core::num::NonZeroUsize::MIN
}
#[inline(always)]
fn max_units_per_value(&self) -> core::num::NonZeroUsize {
unsafe { core::num::NonZeroUsize::new_unchecked(3) }
}
#[inline]
unsafe fn decode_unchecked(
&self,
input: &[u8],
index: usize,
) -> Result<(u8, core::num::NonZeroUsize), Self::DecodeError> {
debug_assert!(index < input.len());
let (value, consumed) = percent_decode_byte(input, index, false)?;
debug_assert!(consumed > 0);
let consumed =
unsafe { core::num::NonZeroUsize::new_unchecked(consumed) };
Ok((value, consumed))
}
#[inline]
unsafe fn encode_unchecked(
&self,
value: &u8,
output: &mut [u8],
index: usize,
) -> Result<usize, Self::EncodeError> {
debug_assert!(
index + if is_unreserved(*value) { 1 } else { 3 } <= output.len()
);
Ok(percent_encode_byte(*value, output, index, false))
}
}
#[inline]
pub(crate) fn percent_encode_bytes(
bytes: &[u8],
space_as_plus: bool,
) -> String {
let mut output = String::with_capacity(bytes.len());
for byte in bytes {
if *byte == b' ' && space_as_plus {
output.push('+');
} else if is_unreserved(*byte) {
output.push(*byte as char);
} else {
output.push('%');
output.push(percent_hex_digit(byte >> 4));
output.push(percent_hex_digit(byte & 0x0f));
}
}
output
}
#[inline]
pub(crate) fn percent_decode_bytes(
text: &str,
plus_as_space: bool,
) -> MiscCodecResult<Vec<u8>> {
let bytes = text.as_bytes();
let mut output = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
let (decoded, consumed) =
percent_decode_byte(bytes, index, plus_as_space)?;
output.push(decoded);
index += consumed;
}
Ok(output)
}
#[inline]
pub(crate) fn percent_encode_byte(
byte: u8,
output: &mut [u8],
index: usize,
space_as_plus: bool,
) -> usize {
if byte == b' ' && space_as_plus {
output[index] = b'+';
return 1;
}
if is_unreserved(byte) {
output[index] = byte;
return 1;
}
output[index] = b'%';
output[index + 1] = percent_hex_digit(byte >> 4) as u8;
output[index + 2] = percent_hex_digit(byte & 0x0f) as u8;
3
}
#[inline]
pub(crate) fn percent_decode_byte(
input: &[u8],
index: usize,
plus_as_space: bool,
) -> MiscCodecResult<(u8, usize)> {
let available = input.len().saturating_sub(index);
if available == 0 {
return Err(MiscCodecError::Incomplete {
required: 1,
available,
});
}
match input[index] {
b'%' => {
if available < 3 {
return Err(MiscCodecError::Incomplete {
required: 3,
available,
});
}
let (Some(&high_byte), Some(&low_byte)) =
(input.get(index + 1), input.get(index + 2))
else {
return Err(invalid_percent_escape(index));
};
let high = percent_hex_value(high_byte)
.ok_or_else(|| invalid_percent_escape(index))?;
let low = percent_hex_value(low_byte)
.ok_or_else(|| invalid_percent_escape(index))?;
Ok(((high << 4) | low, 3))
}
b'+' if plus_as_space => Ok((b' ', 1)),
byte => Ok((byte, 1)),
}
}
fn invalid_percent_escape(index: usize) -> MiscCodecError {
MiscCodecError::InvalidEscape {
index,
escape: "%".to_owned(),
reason: "expected two hexadecimal digits".to_owned(),
}
}
#[inline(always)]
fn is_unreserved(byte: u8) -> bool {
matches!(
byte,
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~'
)
}
#[inline(always)]
fn percent_hex_value(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
#[inline(always)]
fn percent_hex_digit(value: u8) -> char {
UPPER_HEX_DIGITS[(value & 0x0f) as usize]
}