1use core::num::NonZeroUsize;
11
12use percent_encoding::percent_encode_byte as encode_percent_byte;
13use qubit_codec::Codec;
14use qubit_codec::DecodeFailure;
15use qubit_codec::ValueDecoder;
16use qubit_codec::ValueEncoder;
17use qubit_utils::nonzero;
18
19use crate::MiscCodecError;
20use crate::MiscCodecResult;
21use crate::internal::ParseError;
22use crate::internal::PercentEncodingMode;
23use crate::misc_codec_error::map_misc_decode_failure_with_consumed;
24
25#[must_use]
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
32pub struct PercentCodec;
33
34impl PercentCodec {
35 #[inline]
40 pub fn new() -> Self {
41 Self
42 }
43
44 #[inline]
52 #[must_use]
53 pub fn encode(&self, text: &str) -> String {
54 percent_encode_bytes(text.as_bytes(), PercentEncodingMode::Rfc3986)
55 }
56
57 #[inline]
69 pub fn decode(&self, text: &str) -> MiscCodecResult<String> {
70 String::from_utf8(percent_decode_bytes(text, false)?)
71 .map_err(MiscCodecError::from)
72 }
73}
74
75impl ValueEncoder<str> for PercentCodec {
76 type Error = MiscCodecError;
77 type Output = String;
78
79 #[inline]
81 fn encode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
82 Ok(PercentCodec::encode(self, input))
83 }
84}
85
86impl ValueDecoder<str> for PercentCodec {
87 type Error = MiscCodecError;
88 type Output = String;
89
90 #[inline]
92 fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
93 PercentCodec::decode(self, input)
94 }
95}
96
97impl Codec for PercentCodec {
98 type Value = u8;
99 type Unit = u8;
100 type DecodeError = MiscCodecError;
101 type EncodeError = MiscCodecError;
102
103 const MIN_UNITS_PER_VALUE: usize = 1;
104 const MAX_ENCODE_UNITS_PER_VALUE: usize = 3;
105
106 const MAX_DECODE_UNITS_PER_VALUE: usize = 3;
107
108 #[inline(always)]
110 fn encode_len(&self, value: &u8) -> usize {
111 percent_encode_len(*value, PercentEncodingMode::Rfc3986)
112 }
113
114 #[inline]
120 unsafe fn decode(
121 &mut self,
122 input: &[u8],
123 input_index: usize,
124 ) -> Result<(u8, core::num::NonZeroUsize), DecodeFailure<Self::DecodeError>>
125 {
126 debug_assert!(input_index < input.len());
127
128 let (value, consumed) = percent_decode_byte(input, input_index, false)
129 .map_err(|error| {
130 ParseError::into_decode_failure_with_consumed(error, nonzero(3))
131 })?;
132 debug_assert!(consumed > 0);
133 let consumed = nonzero(consumed);
136 Ok((value, consumed))
137 }
138
139 #[inline]
145 unsafe fn encode(
146 &mut self,
147 value: &u8,
148 output: &mut [u8],
149 output_index: usize,
150 ) -> Result<usize, Self::EncodeError> {
151 let required = percent_encode_len(*value, PercentEncodingMode::Rfc3986);
152 debug_assert!(output_index + required <= output.len());
153
154 let written = percent_encode_byte(
155 *value,
156 output,
157 output_index,
158 PercentEncodingMode::Rfc3986,
159 );
160 debug_assert_eq!(written, required);
161 Ok(required)
162 }
163
164 #[inline]
170 unsafe fn decode_eof(
171 &mut self,
172 input: &[u8],
173 input_index: usize,
174 ) -> Result<(u8, core::num::NonZeroUsize), DecodeFailure<Self::DecodeError>>
175 {
176 debug_assert!(input_index < input.len());
177
178 let (value, consumed) =
179 percent_decode_byte_eof(input, input_index, false).map_err(
180 |error| {
181 map_misc_decode_failure_with_consumed(
182 error,
183 percent_invalid_consumed(input, input_index),
184 )
185 },
186 )?;
187 debug_assert!(consumed > 0);
188 let consumed = nonzero(consumed);
189 Ok((value, consumed))
190 }
191}
192
193#[inline(always)]
203pub(crate) fn percent_invalid_consumed(
204 input: &[u8],
205 index: usize,
206) -> NonZeroUsize {
207 NonZeroUsize::new(input.len().saturating_sub(index).clamp(1, 3))
208 .expect("percent decode errors have at least one available unit")
209}
210
211#[inline]
220pub(crate) fn percent_encode_bytes(
221 bytes: &[u8],
222 mode: PercentEncodingMode,
223) -> String {
224 let capacity = bytes
225 .iter()
226 .map(|byte| percent_encode_len(*byte, mode))
227 .sum();
228 let mut output = String::with_capacity(capacity);
229 for byte in bytes {
230 if mode.is_space_plus(*byte) {
231 output.push('+');
232 } else if mode.is_unescaped(*byte) {
233 output.push(*byte as char);
234 } else {
235 output.push_str(encode_percent_byte(*byte));
236 }
237 }
238 output
239}
240
241#[inline]
253pub(crate) fn percent_decode_bytes(
254 text: &str,
255 plus_as_space: bool,
256) -> MiscCodecResult<Vec<u8>> {
257 let bytes = text.as_bytes();
258 let mut output = Vec::with_capacity(bytes.len());
259 let mut index = 0;
260 while index < bytes.len() {
261 let (decoded, consumed) =
262 percent_decode_byte(bytes, index, plus_as_space).map_err(
263 |error| percent_parse_error_to_misc(error, bytes, index),
264 )?;
265 output.push(decoded);
266 index += consumed;
267 }
268 Ok(output)
269}
270
271#[inline]
282pub(crate) fn percent_encode_byte(
283 byte: u8,
284 output: &mut [u8],
285 index: usize,
286 mode: PercentEncodingMode,
287) -> usize {
288 if mode.is_space_plus(byte) {
289 output[index] = b'+';
290 return 1;
291 }
292 if mode.is_unescaped(byte) {
293 output[index] = byte;
294 return 1;
295 }
296 output[index..index + 3]
297 .copy_from_slice(encode_percent_byte(byte).as_bytes());
298 3
299}
300
301#[inline(always)]
303pub(crate) fn percent_encode_len(byte: u8, mode: PercentEncodingMode) -> usize {
304 if mode.is_space_plus(byte) || mode.is_unescaped(byte) {
305 1
306 } else {
307 3
308 }
309}
310
311#[inline]
324pub(crate) fn percent_decode_byte(
325 input: &[u8],
326 index: usize,
327 plus_as_space: bool,
328) -> Result<(u8, usize), ParseError> {
329 debug_assert!(index < input.len());
330 let available = input.len() - index;
331 match input[index] {
332 b'%' => {
333 if available < 3 {
334 return Err(ParseError::Incomplete {
335 required: nonzero(3),
336 });
337 }
338 let high_byte = input[index + 1];
339 let low_byte = input[index + 2];
340 let high = percent_hex_value(high_byte)
341 .ok_or_else(|| invalid_percent_escape(input, index))?;
342 let low = percent_hex_value(low_byte)
343 .ok_or_else(|| invalid_percent_escape(input, index))?;
344 Ok(((high << 4) | low, 3))
345 }
346 b'+' if plus_as_space => Ok((b' ', 1)),
347 byte => Ok((byte, 1)),
348 }
349}
350
351#[inline]
353fn percent_decode_byte_eof(
354 input: &[u8],
355 index: usize,
356 plus_as_space: bool,
357) -> Result<(u8, usize), MiscCodecError> {
358 percent_decode_byte(input, index, plus_as_space)
359 .map_err(|error| percent_parse_error_to_misc(error, input, index))
360}
361
362#[inline]
364fn percent_parse_error_to_misc(
365 error: ParseError,
366 input: &[u8],
367 index: usize,
368) -> MiscCodecError {
369 match error {
370 ParseError::Incomplete { .. } => invalid_percent_escape(input, index),
371 ParseError::Invalid(error) => error,
372 }
373}
374
375fn invalid_percent_escape(input: &[u8], index: usize) -> MiscCodecError {
383 let end = index.saturating_add(3).min(input.len());
384 MiscCodecError::InvalidEscape {
385 index,
386 escape: String::from_utf8_lossy(&input[index..end]).into_owned(),
387 reason: "expected two hexadecimal digits".to_owned(),
388 }
389}
390
391#[inline(always)]
399fn percent_hex_value(byte: u8) -> Option<u8> {
400 match byte {
401 b'0'..=b'9' => Some(byte - b'0'),
402 b'a'..=b'f' => Some(byte - b'a' + 10),
403 b'A'..=b'F' => Some(byte - b'A' + 10),
404 _ => None,
405 }
406}