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)?).map_err(MiscCodecError::from)
71 }
72}
73
74impl ValueEncoder<str> for PercentCodec {
75 type Error = MiscCodecError;
76 type Output = String;
77
78 #[inline]
80 fn encode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
81 Ok(PercentCodec::encode(self, input))
82 }
83}
84
85impl ValueDecoder<str> for PercentCodec {
86 type Error = MiscCodecError;
87 type Output = String;
88
89 #[inline]
91 fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
92 PercentCodec::decode(self, input)
93 }
94}
95
96impl Codec for PercentCodec {
97 type Value = u8;
98 type Unit = u8;
99 type DecodeError = MiscCodecError;
100 type EncodeError = MiscCodecError;
101
102 const MIN_UNITS_PER_VALUE: usize = 1;
103 const MAX_ENCODE_UNITS_PER_VALUE: usize = 3;
104
105 const MAX_DECODE_UNITS_PER_VALUE: usize = 3;
106
107 #[inline(always)]
109 fn encode_len(&self, value: &u8) -> usize {
110 percent_encode_len(*value, PercentEncodingMode::Rfc3986)
111 }
112
113 #[inline]
119 unsafe fn decode(
120 &mut self,
121 input: &[u8],
122 input_index: usize,
123 ) -> Result<(u8, core::num::NonZeroUsize), DecodeFailure<Self::DecodeError>> {
124 debug_assert!(input_index < input.len());
125
126 let (value, consumed) = percent_decode_byte(input, input_index, false)
127 .map_err(|error| ParseError::into_decode_failure_with_consumed(error, nonzero(3)))?;
128 debug_assert!(consumed > 0);
129 let consumed = nonzero(consumed);
132 Ok((value, consumed))
133 }
134
135 #[inline]
141 unsafe fn encode(
142 &mut self,
143 value: &u8,
144 output: &mut [u8],
145 output_index: usize,
146 ) -> Result<usize, Self::EncodeError> {
147 let required = percent_encode_len(*value, PercentEncodingMode::Rfc3986);
148 debug_assert!(output_index + required <= output.len());
149
150 let written =
151 percent_encode_byte(*value, output, output_index, PercentEncodingMode::Rfc3986);
152 debug_assert_eq!(written, required);
153 Ok(required)
154 }
155
156 #[inline]
162 unsafe fn decode_eof(
163 &mut self,
164 input: &[u8],
165 input_index: usize,
166 ) -> Result<(u8, core::num::NonZeroUsize), DecodeFailure<Self::DecodeError>> {
167 debug_assert!(input_index < input.len());
168
169 let (value, consumed) =
170 percent_decode_byte_eof(input, input_index, false).map_err(|error| {
171 map_misc_decode_failure_with_consumed(
172 error,
173 percent_invalid_consumed(input, input_index),
174 )
175 })?;
176 debug_assert!(consumed > 0);
177 let consumed = nonzero(consumed);
178 Ok((value, consumed))
179 }
180}
181
182#[inline(always)]
192pub(crate) fn percent_invalid_consumed(input: &[u8], index: usize) -> NonZeroUsize {
193 NonZeroUsize::new(input.len().saturating_sub(index).clamp(1, 3))
194 .expect("percent decode errors have at least one available unit")
195}
196
197#[inline]
206pub(crate) fn percent_encode_bytes(bytes: &[u8], mode: PercentEncodingMode) -> String {
207 let capacity = bytes
208 .iter()
209 .map(|byte| percent_encode_len(*byte, mode))
210 .sum();
211 let mut output = String::with_capacity(capacity);
212 for byte in bytes {
213 if mode.is_space_plus(*byte) {
214 output.push('+');
215 } else if mode.is_unescaped(*byte) {
216 output.push(*byte as char);
217 } else {
218 output.push_str(encode_percent_byte(*byte));
219 }
220 }
221 output
222}
223
224#[inline]
236pub(crate) fn percent_decode_bytes(text: &str, plus_as_space: bool) -> MiscCodecResult<Vec<u8>> {
237 let bytes = text.as_bytes();
238 let mut output = Vec::with_capacity(bytes.len());
239 let mut index = 0;
240 while index < bytes.len() {
241 let (decoded, consumed) = percent_decode_byte(bytes, index, plus_as_space)
242 .map_err(|error| percent_parse_error_to_misc(error, bytes, index))?;
243 output.push(decoded);
244 index += consumed;
245 }
246 Ok(output)
247}
248
249#[inline]
260pub(crate) fn percent_encode_byte(
261 byte: u8,
262 output: &mut [u8],
263 index: usize,
264 mode: PercentEncodingMode,
265) -> usize {
266 if mode.is_space_plus(byte) {
267 output[index] = b'+';
268 return 1;
269 }
270 if mode.is_unescaped(byte) {
271 output[index] = byte;
272 return 1;
273 }
274 output[index..index + 3].copy_from_slice(encode_percent_byte(byte).as_bytes());
275 3
276}
277
278#[inline(always)]
280pub(crate) fn percent_encode_len(byte: u8, mode: PercentEncodingMode) -> usize {
281 if mode.is_space_plus(byte) || mode.is_unescaped(byte) {
282 1
283 } else {
284 3
285 }
286}
287
288#[inline]
301pub(crate) fn percent_decode_byte(
302 input: &[u8],
303 index: usize,
304 plus_as_space: bool,
305) -> Result<(u8, usize), ParseError> {
306 debug_assert!(index < input.len());
307 let available = input.len() - index;
308 match input[index] {
309 b'%' => {
310 if available < 3 {
311 return Err(ParseError::Incomplete {
312 required: nonzero(3),
313 });
314 }
315 let high_byte = input[index + 1];
316 let low_byte = input[index + 2];
317 let high =
318 percent_hex_value(high_byte).ok_or_else(|| invalid_percent_escape(input, index))?;
319 let low =
320 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]
330fn percent_decode_byte_eof(
331 input: &[u8],
332 index: usize,
333 plus_as_space: bool,
334) -> Result<(u8, usize), MiscCodecError> {
335 percent_decode_byte(input, index, plus_as_space)
336 .map_err(|error| percent_parse_error_to_misc(error, input, index))
337}
338
339#[inline]
341fn percent_parse_error_to_misc(error: ParseError, input: &[u8], index: usize) -> MiscCodecError {
342 match error {
343 ParseError::Incomplete { .. } => invalid_percent_escape(input, index),
344 ParseError::Invalid(error) => error,
345 }
346}
347
348fn invalid_percent_escape(input: &[u8], index: usize) -> MiscCodecError {
356 let end = index.saturating_add(3).min(input.len());
357 MiscCodecError::InvalidEscape {
358 index,
359 escape: String::from_utf8_lossy(&input[index..end]).into_owned(),
360 reason: "expected two hexadecimal digits".to_owned(),
361 }
362}
363
364#[inline(always)]
372fn percent_hex_value(byte: u8) -> Option<u8> {
373 match byte {
374 b'0'..=b'9' => Some(byte - b'0'),
375 b'a'..=b'f' => Some(byte - b'a' + 10),
376 b'A'..=b'F' => Some(byte - b'A' + 10),
377 _ => None,
378 }
379}