Skip to main content

qubit_codec_misc/percent/
percent_codec.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Percent codec type and trait implementations.
9
10use core::num::NonZeroUsize;
11
12use qubit_codec::Codec;
13use qubit_codec::DecodeFailure;
14use qubit_codec::ValueDecoder;
15use qubit_codec::ValueEncoder;
16use qubit_utils::nonzero;
17
18use super::internal::percent_decode_byte_eof;
19use super::percent_decode_byte;
20use super::percent_decode_bytes;
21use super::percent_encode_byte;
22use super::percent_encode_bytes;
23use super::percent_encode_len;
24use super::percent_invalid_consumed;
25use crate::MiscCodecError;
26use crate::MiscCodecResult;
27use crate::internal::ParseError;
28use crate::internal::PercentEncodingMode;
29use crate::misc_error::map_misc_decode_failure_with_consumed;
30
31/// Encodes and decodes percent-encoded UTF-8 text.
32///
33/// Its low-level [`Codec<Value = u8, Unit = u8>`] implementation converts one
34/// byte to either one unreserved ASCII unit or a `%XX` escape. UTF-8 validation
35/// remains part of the owned [`decode`](Self::decode) helper.
36/// Streaming skip/replace hooks may consume the malformed span visible in the
37/// current input chunk, so recovery output for malformed input can depend on
38/// chunk boundaries. This does not affect valid input or strict decoding.
39///
40/// # Examples
41///
42/// ```
43/// use qubit_codec_misc::PercentCodec;
44///
45/// let codec = PercentCodec::new();
46/// let encoded = codec.encode("hello world");
47/// assert_eq!(encoded, "hello%20world");
48/// assert_eq!(codec.decode(&encoded).expect("valid encoded text"), "hello world");
49/// ```
50#[must_use]
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
52pub struct PercentCodec;
53
54impl PercentCodec {
55    /// Creates a percent codec.
56    ///
57    /// # Returns
58    /// Percent codec.
59    #[inline]
60    pub fn new() -> Self {
61        Self
62    }
63
64    /// Encodes text using percent encoding.
65    ///
66    /// # Parameters
67    /// - `text`: UTF-8 text to encode.
68    ///
69    /// # Returns
70    /// Percent-encoded text.
71    #[inline]
72    #[must_use]
73    pub fn encode(&self, text: &str) -> String {
74        percent_encode_bytes(text.as_bytes(), PercentEncodingMode::Rfc3986)
75    }
76
77    /// Decodes percent-encoded UTF-8 text.
78    ///
79    /// # Parameters
80    /// - `text`: Percent-encoded text.
81    ///
82    /// # Returns
83    /// Decoded UTF-8 text.
84    ///
85    /// # Errors
86    /// Returns [`MiscCodecError`] when a percent escape is malformed or decoded
87    /// bytes are not valid UTF-8.
88    #[inline]
89    pub fn decode(&self, text: &str) -> MiscCodecResult<String> {
90        String::from_utf8(percent_decode_bytes(text, false)?).map_err(MiscCodecError::from)
91    }
92}
93
94impl ValueEncoder<str> for PercentCodec {
95    /// Error type used when encoding a text value.
96    ///
97    /// Percent encoding accepts every UTF-8 input, so this implementation
98    /// does not produce an encoding error.
99    type Error = MiscCodecError;
100
101    /// Owned percent-encoded text produced from the input.
102    type Output = String;
103
104    /// Encodes one text input using RFC 3986 percent encoding.
105    ///
106    /// # Parameters
107    /// - `input`: UTF-8 text to encode.
108    ///
109    /// # Returns
110    /// The percent-encoded text.
111    ///
112    /// # Errors
113    /// This implementation does not reject text and therefore always succeeds.
114    #[inline]
115    fn encode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
116        Ok(PercentCodec::encode(self, input))
117    }
118}
119
120impl ValueDecoder<str> for PercentCodec {
121    /// Error type used when decoding percent-encoded text.
122    ///
123    /// Errors report malformed escapes or decoded bytes that are not valid
124    /// UTF-8.
125    type Error = MiscCodecError;
126
127    /// Owned UTF-8 text produced by a successful decode.
128    type Output = String;
129
130    /// Decodes one percent-encoded UTF-8 text input.
131    ///
132    /// # Parameters
133    /// - `input`: Percent-encoded text to decode.
134    ///
135    /// # Returns
136    /// The decoded UTF-8 text.
137    ///
138    /// # Errors
139    /// Returns an error for malformed escapes or decoded bytes that are not
140    /// valid UTF-8.
141    #[inline]
142    fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
143        PercentCodec::decode(self, input)
144    }
145}
146
147impl Codec for PercentCodec {
148    /// Logical byte value represented by each decoded codec value.
149    type Value = u8;
150
151    /// Byte unit used by the encoded representation.
152    type Unit = u8;
153
154    /// Error reported when encoded bytes cannot be decoded as one value.
155    type DecodeError = MiscCodecError;
156
157    /// Error type for encoding; this codec can encode every byte.
158    type EncodeError = MiscCodecError;
159
160    const MIN_UNITS_PER_VALUE: usize = 1;
161    const MAX_ENCODE_UNITS_PER_VALUE: usize = 3;
162
163    const MAX_DECODE_UNITS_PER_VALUE: usize = 3;
164
165    /// Returns the exact RFC 3986 encoded width for one byte.
166    ///
167    /// # Parameters
168    /// - `value`: Byte whose encoded width is measured.
169    ///
170    /// # Returns
171    /// One for an unreserved byte, or three for an escaped byte.
172    #[inline]
173    fn encode_len(&self, value: &u8) -> usize {
174        percent_encode_len(*value, PercentEncodingMode::Rfc3986)
175    }
176
177    /// Decodes one raw byte or `%XX` escape.
178    ///
179    /// # Parameters
180    /// - `input`: Encoded byte units.
181    /// - `input_index`: Index of the byte or escape to decode.
182    ///
183    /// # Returns
184    /// The decoded byte and non-zero number of consumed units.
185    ///
186    /// # Errors
187    /// Returns a decode failure when the visible escape is malformed or
188    /// incomplete.
189    ///
190    /// # Safety
191    /// The caller must provide an input index with at least one readable unit
192    /// and a slice satisfying the codec trait's decode preconditions.
193    #[inline]
194    unsafe fn decode(
195        &mut self,
196        input: &[u8],
197        input_index: usize,
198    ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
199        debug_assert!(input_index < input.len());
200
201        let (value, consumed) = percent_decode_byte(input, input_index, false).map_err(|error| {
202            ParseError::into_decode_failure_with_consumed(error, percent_invalid_consumed(input, input_index))
203        })?;
204        debug_assert!(consumed > 0);
205        // SAFETY: `percent_decode_byte` returns a non-zero width for every
206        // successful raw byte or escape.
207        let consumed = nonzero(consumed);
208        Ok((value, consumed))
209    }
210
211    /// Encodes one byte using percent encoding.
212    ///
213    /// # Parameters
214    /// - `value`: Byte to encode.
215    /// - `output`: Destination unit buffer.
216    /// - `output_index`: Start index in `output`.
217    ///
218    /// # Returns
219    /// Number of units written.
220    ///
221    /// # Errors
222    /// This implementation accepts every byte and therefore always succeeds.
223    ///
224    /// # Safety
225    /// The caller must provide enough writable output units for
226    /// [`Self::encode_len`].
227    #[inline]
228    unsafe fn encode(
229        &mut self,
230        value: &u8,
231        output: &mut [u8],
232        output_index: usize,
233    ) -> Result<usize, Self::EncodeError> {
234        let required = percent_encode_len(*value, PercentEncodingMode::Rfc3986);
235        debug_assert!(output_index + required <= output.len());
236
237        let written = percent_encode_byte(*value, output, output_index, PercentEncodingMode::Rfc3986);
238        debug_assert_eq!(written, required);
239        Ok(required)
240    }
241
242    /// Decodes one value after end of input has been confirmed.
243    ///
244    /// # Parameters
245    /// - `input`: Complete encoded byte units.
246    /// - `input_index`: Index of the byte or escape to decode.
247    ///
248    /// # Returns
249    /// The decoded byte and non-zero number of consumed units.
250    ///
251    /// # Errors
252    /// Returns a decode failure when the escape is malformed or incomplete.
253    ///
254    /// # Safety
255    /// The caller must provide an input index with at least one readable unit
256    /// and a slice satisfying the codec trait's EOF decode preconditions.
257    #[inline]
258    unsafe fn decode_eof(
259        &mut self,
260        input: &[u8],
261        input_index: usize,
262    ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
263        debug_assert!(input_index < input.len());
264
265        let (value, consumed) = percent_decode_byte_eof(input, input_index, false).map_err(|error| {
266            map_misc_decode_failure_with_consumed(error, percent_invalid_consumed(input, input_index))
267        })?;
268        debug_assert!(consumed > 0);
269        let consumed = nonzero(consumed);
270        Ok((value, consumed))
271    }
272}