1use crate::{
11 MiscCodecError,
12 MiscCodecResult,
13 internal::{
14 ParseError,
15 PercentEncodingMode,
16 },
17 misc_codec_error::map_misc_decode_failure_with_consumed,
18};
19use core::num::NonZeroUsize;
20use percent_encoding::percent_encode_byte as encode_percent_byte;
21use qubit_codec::{
22 Codec,
23 ValueDecoder,
24 ValueEncoder,
25};
26
27#[must_use]
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
34pub struct PercentCodec;
35
36impl PercentCodec {
37 #[inline]
42 pub fn new() -> Self {
43 Self
44 }
45
46 #[inline]
54 #[must_use]
55 pub fn encode(&self, text: &str) -> String {
56 percent_encode_bytes(text.as_bytes(), PercentEncodingMode::Rfc3986)
57 }
58
59 #[inline]
71 pub fn decode(&self, text: &str) -> MiscCodecResult<String> {
72 String::from_utf8(percent_decode_bytes(text, false)?)
73 .map_err(MiscCodecError::from)
74 }
75}
76
77impl ValueEncoder<str> for PercentCodec {
78 type Error = MiscCodecError;
79 type Output = String;
80
81 #[inline]
83 fn encode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
84 Ok(PercentCodec::encode(self, input))
85 }
86}
87
88impl ValueDecoder<str> for PercentCodec {
89 type Error = MiscCodecError;
90 type Output = String;
91
92 #[inline]
94 fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
95 PercentCodec::decode(self, input)
96 }
97}
98
99impl Codec for PercentCodec {
100 type Value = u8;
101 type Unit = u8;
102 type DecodeError = MiscCodecError;
103 type EncodeError = MiscCodecError;
104
105 const MIN_UNITS_PER_VALUE: usize = 1;
106 const MAX_ENCODE_UNITS_PER_VALUE: usize = 3;
107
108 const MAX_DECODE_UNITS_PER_VALUE: usize = 3;
109
110 #[inline(always)]
112 fn encode_len(&self, value: &u8) -> usize {
113 percent_encode_len(*value, PercentEncodingMode::Rfc3986)
114 }
115
116 #[inline]
122 unsafe fn decode(
123 &mut self,
124 input: &[u8],
125 input_index: usize,
126 ) -> Result<
127 (u8, core::num::NonZeroUsize),
128 qubit_codec::DecodeFailure<Self::DecodeError>,
129 > {
130 debug_assert!(input_index < input.len());
131
132 let (value, consumed) = percent_decode_byte(input, input_index, false)
133 .map_err(|error| {
134 ParseError::into_decode_failure_with_consumed(
135 error,
136 qubit_codec::nz!(3),
137 )
138 })?;
139 debug_assert!(consumed > 0);
140 let consumed = qubit_codec::nz!(consumed);
143 Ok((value, consumed))
144 }
145
146 #[inline]
152 unsafe fn encode(
153 &mut self,
154 value: &u8,
155 output: &mut [u8],
156 output_index: usize,
157 ) -> Result<usize, Self::EncodeError> {
158 let required = percent_encode_len(*value, PercentEncodingMode::Rfc3986);
159 debug_assert!(output_index + required <= output.len());
160
161 let written = percent_encode_byte(
162 *value,
163 output,
164 output_index,
165 PercentEncodingMode::Rfc3986,
166 );
167 debug_assert_eq!(written, required);
168 Ok(required)
169 }
170
171 #[inline]
177 unsafe fn decode_eof(
178 &mut self,
179 input: &[u8],
180 input_index: usize,
181 ) -> Result<
182 (u8, core::num::NonZeroUsize),
183 qubit_codec::DecodeFailure<Self::DecodeError>,
184 > {
185 debug_assert!(input_index < input.len());
186
187 let (value, consumed) =
188 percent_decode_byte_eof(input, input_index, false).map_err(
189 |error| {
190 map_misc_decode_failure_with_consumed(
191 error,
192 percent_invalid_consumed(input, input_index),
193 )
194 },
195 )?;
196 debug_assert!(consumed > 0);
197 let consumed = qubit_codec::nz!(consumed);
198 Ok((value, consumed))
199 }
200}
201
202#[inline(always)]
212pub(crate) fn percent_invalid_consumed(
213 input: &[u8],
214 index: usize,
215) -> NonZeroUsize {
216 NonZeroUsize::new(input.len().saturating_sub(index).clamp(1, 3))
217 .expect("percent decode errors have at least one available unit")
218}
219
220#[inline]
229pub(crate) fn percent_encode_bytes(
230 bytes: &[u8],
231 mode: PercentEncodingMode,
232) -> String {
233 let capacity = bytes
234 .iter()
235 .map(|byte| percent_encode_len(*byte, mode))
236 .sum();
237 let mut output = String::with_capacity(capacity);
238 for byte in bytes {
239 if mode.is_space_plus(*byte) {
240 output.push('+');
241 } else if mode.is_unescaped(*byte) {
242 output.push(*byte as char);
243 } else {
244 output.push_str(encode_percent_byte(*byte));
245 }
246 }
247 output
248}
249
250#[inline]
262pub(crate) fn percent_decode_bytes(
263 text: &str,
264 plus_as_space: bool,
265) -> MiscCodecResult<Vec<u8>> {
266 let bytes = text.as_bytes();
267 let mut output = Vec::with_capacity(bytes.len());
268 let mut index = 0;
269 while index < bytes.len() {
270 let (decoded, consumed) =
271 percent_decode_byte(bytes, index, plus_as_space).map_err(
272 |error| percent_parse_error_to_misc(error, bytes, index),
273 )?;
274 output.push(decoded);
275 index += consumed;
276 }
277 Ok(output)
278}
279
280#[inline]
291pub(crate) fn percent_encode_byte(
292 byte: u8,
293 output: &mut [u8],
294 index: usize,
295 mode: PercentEncodingMode,
296) -> usize {
297 if mode.is_space_plus(byte) {
298 output[index] = b'+';
299 return 1;
300 }
301 if mode.is_unescaped(byte) {
302 output[index] = byte;
303 return 1;
304 }
305 output[index..index + 3]
306 .copy_from_slice(encode_percent_byte(byte).as_bytes());
307 3
308}
309
310#[inline(always)]
312pub(crate) fn percent_encode_len(byte: u8, mode: PercentEncodingMode) -> usize {
313 if mode.is_space_plus(byte) || mode.is_unescaped(byte) {
314 1
315 } else {
316 3
317 }
318}
319
320#[inline]
333pub(crate) fn percent_decode_byte(
334 input: &[u8],
335 index: usize,
336 plus_as_space: bool,
337) -> Result<(u8, usize), ParseError> {
338 debug_assert!(index < input.len());
339 let available = input.len() - index;
340 match input[index] {
341 b'%' => {
342 if available < 3 {
343 return Err(ParseError::Incomplete {
344 required: qubit_codec::nz!(3),
345 });
346 }
347 let high_byte = input[index + 1];
348 let low_byte = input[index + 2];
349 let high = percent_hex_value(high_byte)
350 .ok_or_else(|| invalid_percent_escape(input, index))?;
351 let low = percent_hex_value(low_byte)
352 .ok_or_else(|| invalid_percent_escape(input, index))?;
353 Ok(((high << 4) | low, 3))
354 }
355 b'+' if plus_as_space => Ok((b' ', 1)),
356 byte => Ok((byte, 1)),
357 }
358}
359
360#[inline]
362fn percent_decode_byte_eof(
363 input: &[u8],
364 index: usize,
365 plus_as_space: bool,
366) -> Result<(u8, usize), MiscCodecError> {
367 percent_decode_byte(input, index, plus_as_space)
368 .map_err(|error| percent_parse_error_to_misc(error, input, index))
369}
370
371#[inline]
373fn percent_parse_error_to_misc(
374 error: ParseError,
375 input: &[u8],
376 index: usize,
377) -> MiscCodecError {
378 match error {
379 ParseError::Incomplete { .. } => invalid_percent_escape(input, index),
380 ParseError::Invalid(error) => error,
381 }
382}
383
384fn invalid_percent_escape(input: &[u8], index: usize) -> MiscCodecError {
392 let end = index.saturating_add(3).min(input.len());
393 MiscCodecError::InvalidEscape {
394 index,
395 escape: String::from_utf8_lossy(&input[index..end]).into_owned(),
396 reason: "expected two hexadecimal digits".to_owned(),
397 }
398}
399
400#[inline(always)]
408fn percent_hex_value(byte: u8) -> Option<u8> {
409 match byte {
410 b'0'..=b'9' => Some(byte - b'0'),
411 b'a'..=b'f' => Some(byte - b'a' + 10),
412 b'A'..=b'F' => Some(byte - b'A' + 10),
413 _ => None,
414 }
415}