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]
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub struct PercentCodec;
44
45impl PercentCodec {
46 #[inline]
51 pub fn new() -> Self {
52 Self
53 }
54
55 #[inline]
63 #[must_use]
64 pub fn encode(&self, text: &str) -> String {
65 percent_encode_bytes(text.as_bytes(), PercentEncodingMode::Rfc3986)
66 }
67
68 #[inline]
80 pub fn decode(&self, text: &str) -> MiscCodecResult<String> {
81 String::from_utf8(percent_decode_bytes(text, false)?).map_err(MiscCodecError::from)
82 }
83}
84
85impl ValueEncoder<str> for PercentCodec {
86 type Error = MiscCodecError;
87 type Output = String;
88
89 #[inline]
91 fn encode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
92 Ok(PercentCodec::encode(self, input))
93 }
94}
95
96impl ValueDecoder<str> for PercentCodec {
97 type Error = MiscCodecError;
98 type Output = String;
99
100 #[inline]
102 fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
103 PercentCodec::decode(self, input)
104 }
105}
106
107impl Codec for PercentCodec {
108 type Value = u8;
109 type Unit = u8;
110 type DecodeError = MiscCodecError;
111 type EncodeError = MiscCodecError;
112
113 const MIN_UNITS_PER_VALUE: usize = 1;
114 const MAX_ENCODE_UNITS_PER_VALUE: usize = 3;
115
116 const MAX_DECODE_UNITS_PER_VALUE: usize = 3;
117
118 #[inline]
120 fn encode_len(&self, value: &u8) -> usize {
121 percent_encode_len(*value, PercentEncodingMode::Rfc3986)
122 }
123
124 #[inline]
130 unsafe fn decode(
131 &mut self,
132 input: &[u8],
133 input_index: usize,
134 ) -> Result<(u8, core::num::NonZeroUsize), DecodeFailure<Self::DecodeError>> {
135 debug_assert!(input_index < input.len());
136
137 let (value, consumed) = percent_decode_byte(input, input_index, false)
138 .map_err(|error| ParseError::into_decode_failure_with_consumed(error, nonzero(3)))?;
139 debug_assert!(consumed > 0);
140 let consumed = nonzero(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(*value, output, output_index, PercentEncodingMode::Rfc3986);
162 debug_assert_eq!(written, required);
163 Ok(required)
164 }
165
166 #[inline]
172 unsafe fn decode_eof(
173 &mut self,
174 input: &[u8],
175 input_index: usize,
176 ) -> Result<(u8, core::num::NonZeroUsize), DecodeFailure<Self::DecodeError>> {
177 debug_assert!(input_index < input.len());
178
179 let (value, consumed) = percent_decode_byte_eof(input, input_index, false).map_err(|error| {
180 map_misc_decode_failure_with_consumed(error, percent_invalid_consumed(input, input_index))
181 })?;
182 debug_assert!(consumed > 0);
183 let consumed = nonzero(consumed);
184 Ok((value, consumed))
185 }
186}
187
188#[inline]
198pub(crate) fn percent_invalid_consumed(input: &[u8], index: usize) -> NonZeroUsize {
199 NonZeroUsize::new(input.len().saturating_sub(index).clamp(1, 3))
200 .expect("percent decode errors have at least one available unit")
201}
202
203#[inline]
212pub(crate) fn percent_encode_bytes(bytes: &[u8], mode: PercentEncodingMode) -> String {
213 let capacity = bytes.iter().map(|byte| percent_encode_len(*byte, mode)).sum();
214 let mut output = String::with_capacity(capacity);
215 for byte in bytes {
216 if mode.is_space_plus(*byte) {
217 output.push('+');
218 } else if mode.is_unescaped(*byte) {
219 output.push(*byte as char);
220 } else {
221 output.push_str(encode_percent_byte(*byte));
222 }
223 }
224 output
225}
226
227#[inline]
239pub(crate) fn percent_decode_bytes(text: &str, plus_as_space: bool) -> MiscCodecResult<Vec<u8>> {
240 let bytes = text.as_bytes();
241 let mut output = Vec::with_capacity(bytes.len());
242 let mut index = 0;
243 while index < bytes.len() {
244 let (decoded, consumed) = percent_decode_byte(bytes, index, plus_as_space)
245 .map_err(|error| percent_parse_error_to_misc(error, bytes, index))?;
246 output.push(decoded);
247 index += consumed;
248 }
249 Ok(output)
250}
251
252#[inline]
263pub(crate) fn percent_encode_byte(byte: u8, output: &mut [u8], index: usize, mode: PercentEncodingMode) -> usize {
264 if mode.is_space_plus(byte) {
265 output[index] = b'+';
266 return 1;
267 }
268 if mode.is_unescaped(byte) {
269 output[index] = byte;
270 return 1;
271 }
272 output[index..index + 3].copy_from_slice(encode_percent_byte(byte).as_bytes());
273 3
274}
275
276#[inline]
287pub(crate) fn percent_encode_len(byte: u8, mode: PercentEncodingMode) -> usize {
288 if mode.is_space_plus(byte) || mode.is_unescaped(byte) {
289 1
290 } else {
291 3
292 }
293}
294
295#[inline]
309pub(crate) fn percent_decode_byte(input: &[u8], index: usize, plus_as_space: bool) -> Result<(u8, usize), ParseError> {
310 debug_assert!(index < input.len());
311 let available = input.len() - index;
312 match input[index] {
313 b'%' => {
314 if available < 3 {
315 return Err(ParseError::Incomplete { required: nonzero(3) });
316 }
317 let high_byte = input[index + 1];
318 let low_byte = input[index + 2];
319 let high = percent_hex_value(high_byte).ok_or_else(|| invalid_percent_escape(input, index))?;
320 let low = percent_hex_value(low_byte).ok_or_else(|| invalid_percent_escape(input, index))?;
321 Ok(((high << 4) | low, 3))
322 }
323 b'+' if plus_as_space => Ok((b' ', 1)),
324 byte => Ok((byte, 1)),
325 }
326}
327
328#[inline]
342fn percent_decode_byte_eof(input: &[u8], index: usize, plus_as_space: bool) -> Result<(u8, usize), MiscCodecError> {
343 percent_decode_byte(input, index, plus_as_space).map_err(|error| percent_parse_error_to_misc(error, input, index))
344}
345
346#[inline]
357fn percent_parse_error_to_misc(error: ParseError, input: &[u8], index: usize) -> MiscCodecError {
358 match error {
359 ParseError::Incomplete { .. } => invalid_percent_escape(input, index),
360 ParseError::Invalid(error) => error,
361 }
362}
363
364fn invalid_percent_escape(input: &[u8], index: usize) -> MiscCodecError {
373 let end = index.saturating_add(3).min(input.len());
374 MiscCodecError::InvalidEscape {
375 index,
376 escape: String::from_utf8_lossy(&input[index..end]).into_owned(),
377 reason: "expected two hexadecimal digits".to_owned(),
378 }
379}
380
381#[inline]
389fn percent_hex_value(byte: u8) -> Option<u8> {
390 match byte {
391 b'0'..=b'9' => Some(byte - b'0'),
392 b'a'..=b'f' => Some(byte - b'a' + 10),
393 b'A'..=b'F' => Some(byte - b'A' + 10),
394 _ => None,
395 }
396}