use core::num::NonZeroUsize;
use percent_encoding::percent_encode_byte as encode_percent_byte;
use qubit_codec::Codec;
use qubit_codec::DecodeFailure;
use qubit_codec::ValueDecoder;
use qubit_codec::ValueEncoder;
use qubit_utils::nonzero;
use crate::MiscCodecError;
use crate::MiscCodecResult;
use crate::internal::ParseError;
use crate::internal::PercentEncodingMode;
use crate::misc_codec_error::map_misc_decode_failure_with_consumed;
#[must_use]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PercentCodec;
impl PercentCodec {
#[inline]
pub fn new() -> Self {
Self
}
#[inline]
#[must_use]
pub fn encode(&self, text: &str) -> String {
percent_encode_bytes(text.as_bytes(), PercentEncodingMode::Rfc3986)
}
#[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(&mut 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(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
PercentCodec::decode(self, input)
}
}
impl Codec for PercentCodec {
type Value = u8;
type Unit = u8;
type DecodeError = MiscCodecError;
type EncodeError = MiscCodecError;
const MIN_UNITS_PER_VALUE: usize = 1;
const MAX_ENCODE_UNITS_PER_VALUE: usize = 3;
const MAX_DECODE_UNITS_PER_VALUE: usize = 3;
#[inline]
fn encode_len(&self, value: &u8) -> usize {
percent_encode_len(*value, PercentEncodingMode::Rfc3986)
}
#[inline]
unsafe fn decode(
&mut self,
input: &[u8],
input_index: usize,
) -> Result<(u8, core::num::NonZeroUsize), DecodeFailure<Self::DecodeError>> {
debug_assert!(input_index < input.len());
let (value, consumed) = percent_decode_byte(input, input_index, false)
.map_err(|error| ParseError::into_decode_failure_with_consumed(error, nonzero(3)))?;
debug_assert!(consumed > 0);
let consumed = nonzero(consumed);
Ok((value, consumed))
}
#[inline]
unsafe fn encode(
&mut self,
value: &u8,
output: &mut [u8],
output_index: usize,
) -> Result<usize, Self::EncodeError> {
let required = percent_encode_len(*value, PercentEncodingMode::Rfc3986);
debug_assert!(output_index + required <= output.len());
let written = percent_encode_byte(*value, output, output_index, PercentEncodingMode::Rfc3986);
debug_assert_eq!(written, required);
Ok(required)
}
#[inline]
unsafe fn decode_eof(
&mut self,
input: &[u8],
input_index: usize,
) -> Result<(u8, core::num::NonZeroUsize), DecodeFailure<Self::DecodeError>> {
debug_assert!(input_index < input.len());
let (value, consumed) = percent_decode_byte_eof(input, input_index, false).map_err(|error| {
map_misc_decode_failure_with_consumed(error, percent_invalid_consumed(input, input_index))
})?;
debug_assert!(consumed > 0);
let consumed = nonzero(consumed);
Ok((value, consumed))
}
}
#[inline]
pub(crate) fn percent_invalid_consumed(input: &[u8], index: usize) -> NonZeroUsize {
NonZeroUsize::new(input.len().saturating_sub(index).clamp(1, 3))
.expect("percent decode errors have at least one available unit")
}
#[inline]
pub(crate) fn percent_encode_bytes(bytes: &[u8], mode: PercentEncodingMode) -> String {
let capacity = bytes.iter().map(|byte| percent_encode_len(*byte, mode)).sum();
let mut output = String::with_capacity(capacity);
for byte in bytes {
if mode.is_space_plus(*byte) {
output.push('+');
} else if mode.is_unescaped(*byte) {
output.push(*byte as char);
} else {
output.push_str(encode_percent_byte(*byte));
}
}
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)
.map_err(|error| percent_parse_error_to_misc(error, bytes, index))?;
output.push(decoded);
index += consumed;
}
Ok(output)
}
#[inline]
pub(crate) fn percent_encode_byte(byte: u8, output: &mut [u8], index: usize, mode: PercentEncodingMode) -> usize {
if mode.is_space_plus(byte) {
output[index] = b'+';
return 1;
}
if mode.is_unescaped(byte) {
output[index] = byte;
return 1;
}
output[index..index + 3].copy_from_slice(encode_percent_byte(byte).as_bytes());
3
}
#[inline]
pub(crate) fn percent_encode_len(byte: u8, mode: PercentEncodingMode) -> usize {
if mode.is_space_plus(byte) || mode.is_unescaped(byte) {
1
} else {
3
}
}
#[inline]
pub(crate) fn percent_decode_byte(input: &[u8], index: usize, plus_as_space: bool) -> Result<(u8, usize), ParseError> {
debug_assert!(index < input.len());
let available = input.len() - index;
match input[index] {
b'%' => {
if available < 3 {
return Err(ParseError::Incomplete { required: nonzero(3) });
}
let high_byte = input[index + 1];
let low_byte = input[index + 2];
let high = percent_hex_value(high_byte).ok_or_else(|| invalid_percent_escape(input, index))?;
let low = percent_hex_value(low_byte).ok_or_else(|| invalid_percent_escape(input, index))?;
Ok(((high << 4) | low, 3))
}
b'+' if plus_as_space => Ok((b' ', 1)),
byte => Ok((byte, 1)),
}
}
#[inline]
fn percent_decode_byte_eof(input: &[u8], index: usize, plus_as_space: bool) -> Result<(u8, usize), MiscCodecError> {
percent_decode_byte(input, index, plus_as_space).map_err(|error| percent_parse_error_to_misc(error, input, index))
}
#[inline]
fn percent_parse_error_to_misc(error: ParseError, input: &[u8], index: usize) -> MiscCodecError {
match error {
ParseError::Incomplete { .. } => invalid_percent_escape(input, index),
ParseError::Invalid(error) => error,
}
}
fn invalid_percent_escape(input: &[u8], index: usize) -> MiscCodecError {
let end = index.saturating_add(3).min(input.len());
MiscCodecError::InvalidEscape {
index,
escape: String::from_utf8_lossy(&input[index..end]).into_owned(),
reason: "expected two hexadecimal digits".to_owned(),
}
}
#[inline]
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,
}
}