1use crate::{
11 Codec,
12 MiscCodecError,
13 MiscCodecResult,
14 ValueDecoder,
15 ValueEncoder,
16};
17
18const UPPER_HEX_DIGITS: [char; 16] = [
19 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
20 'F',
21];
22
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub struct PercentCodec;
30
31impl PercentCodec {
32 #[inline]
37 pub fn new() -> Self {
38 Self
39 }
40
41 #[inline]
49 pub fn encode(&self, text: &str) -> String {
50 percent_encode_bytes(text.as_bytes(), false)
51 }
52
53 #[inline]
65 pub fn decode(&self, text: &str) -> MiscCodecResult<String> {
66 String::from_utf8(percent_decode_bytes(text, false)?)
67 .map_err(MiscCodecError::from)
68 }
69}
70
71impl ValueEncoder<str> for PercentCodec {
72 type Error = MiscCodecError;
73 type Output = String;
74
75 #[inline]
77 fn encode(&self, input: &str) -> Result<Self::Output, Self::Error> {
78 Ok(PercentCodec::encode(self, input))
79 }
80}
81
82impl ValueDecoder<str> for PercentCodec {
83 type Error = MiscCodecError;
84 type Output = String;
85
86 #[inline]
88 fn decode(&self, input: &str) -> Result<Self::Output, Self::Error> {
89 PercentCodec::decode(self, input)
90 }
91}
92
93unsafe impl Codec for PercentCodec {
94 type Value = u8;
95 type Unit = u8;
96 type DecodeError = MiscCodecError;
97 type EncodeError = MiscCodecError;
98
99 #[inline(always)]
101 fn min_units_per_value(&self) -> core::num::NonZeroUsize {
102 core::num::NonZeroUsize::MIN
103 }
104
105 #[inline(always)]
107 fn max_units_per_value(&self) -> core::num::NonZeroUsize {
108 unsafe { core::num::NonZeroUsize::new_unchecked(3) }
109 }
110
111 #[inline]
113 unsafe fn decode_unchecked(
114 &self,
115 input: &[u8],
116 index: usize,
117 ) -> Result<(u8, core::num::NonZeroUsize), Self::DecodeError> {
118 debug_assert!(index < input.len());
119
120 let (value, consumed) = percent_decode_byte(input, index, false)?;
121 debug_assert!(consumed > 0);
122 let consumed =
125 unsafe { core::num::NonZeroUsize::new_unchecked(consumed) };
126 Ok((value, consumed))
127 }
128
129 #[inline]
131 unsafe fn encode_unchecked(
132 &self,
133 value: &u8,
134 output: &mut [u8],
135 index: usize,
136 ) -> Result<usize, Self::EncodeError> {
137 debug_assert!(
138 index + if is_unreserved(*value) { 1 } else { 3 } <= output.len()
139 );
140
141 Ok(percent_encode_byte(*value, output, index, false))
142 }
143}
144
145#[inline]
154pub(crate) fn percent_encode_bytes(
155 bytes: &[u8],
156 space_as_plus: bool,
157) -> String {
158 let mut output = String::with_capacity(bytes.len());
159 for byte in bytes {
160 if *byte == b' ' && space_as_plus {
161 output.push('+');
162 } else if is_unreserved(*byte) {
163 output.push(*byte as char);
164 } else {
165 output.push('%');
166 output.push(percent_hex_digit(byte >> 4));
167 output.push(percent_hex_digit(byte & 0x0f));
168 }
169 }
170 output
171}
172
173#[inline]
185pub(crate) fn percent_decode_bytes(
186 text: &str,
187 plus_as_space: bool,
188) -> MiscCodecResult<Vec<u8>> {
189 let bytes = text.as_bytes();
190 let mut output = Vec::with_capacity(bytes.len());
191 let mut index = 0;
192 while index < bytes.len() {
193 let (decoded, consumed) =
194 percent_decode_byte(bytes, index, plus_as_space)?;
195 output.push(decoded);
196 index += consumed;
197 }
198 Ok(output)
199}
200
201#[inline]
212pub(crate) fn percent_encode_byte(
213 byte: u8,
214 output: &mut [u8],
215 index: usize,
216 space_as_plus: bool,
217) -> usize {
218 if byte == b' ' && space_as_plus {
219 output[index] = b'+';
220 return 1;
221 }
222 if is_unreserved(byte) {
223 output[index] = byte;
224 return 1;
225 }
226 output[index] = b'%';
227 output[index + 1] = percent_hex_digit(byte >> 4) as u8;
228 output[index + 2] = percent_hex_digit(byte & 0x0f) as u8;
229 3
230}
231
232#[inline]
245pub(crate) fn percent_decode_byte(
246 input: &[u8],
247 index: usize,
248 plus_as_space: bool,
249) -> MiscCodecResult<(u8, usize)> {
250 let available = input.len().saturating_sub(index);
251 if available == 0 {
252 return Err(MiscCodecError::Incomplete {
253 required: 1,
254 available,
255 });
256 }
257 match input[index] {
258 b'%' => {
259 if available < 3 {
260 return Err(MiscCodecError::Incomplete {
261 required: 3,
262 available,
263 });
264 }
265 let (Some(&high_byte), Some(&low_byte)) =
266 (input.get(index + 1), input.get(index + 2))
267 else {
268 return Err(invalid_percent_escape(index));
269 };
270 let high = percent_hex_value(high_byte)
271 .ok_or_else(|| invalid_percent_escape(index))?;
272 let low = percent_hex_value(low_byte)
273 .ok_or_else(|| invalid_percent_escape(index))?;
274 Ok(((high << 4) | low, 3))
275 }
276 b'+' if plus_as_space => Ok((b' ', 1)),
277 byte => Ok((byte, 1)),
278 }
279}
280
281fn invalid_percent_escape(index: usize) -> MiscCodecError {
289 MiscCodecError::InvalidEscape {
290 index,
291 escape: "%".to_owned(),
292 reason: "expected two hexadecimal digits".to_owned(),
293 }
294}
295
296#[inline(always)]
304fn is_unreserved(byte: u8) -> bool {
305 matches!(
306 byte,
307 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~'
308 )
309}
310
311#[inline(always)]
319fn percent_hex_value(byte: u8) -> Option<u8> {
320 match byte {
321 b'0'..=b'9' => Some(byte - b'0'),
322 b'a'..=b'f' => Some(byte - b'a' + 10),
323 b'A'..=b'F' => Some(byte - b'A' + 10),
324 _ => None,
325 }
326}
327
328#[inline(always)]
337fn percent_hex_digit(value: u8) -> char {
338 UPPER_HEX_DIGITS[(value & 0x0f) as usize]
339}