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    type Error = MiscCodecError;
96    type Output = String;
97
98    /// Encodes one text input using RFC 3986 percent encoding.
99    ///
100    /// # Parameters
101    /// - `input`: UTF-8 text to encode.
102    ///
103    /// # Returns
104    /// The percent-encoded text.
105    ///
106    /// # Errors
107    /// This implementation does not reject text and therefore always succeeds.
108    #[inline]
109    fn encode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
110        Ok(PercentCodec::encode(self, input))
111    }
112}
113
114impl ValueDecoder<str> for PercentCodec {
115    type Error = MiscCodecError;
116    type Output = String;
117
118    /// Decodes one percent-encoded UTF-8 text input.
119    ///
120    /// # Parameters
121    /// - `input`: Percent-encoded text to decode.
122    ///
123    /// # Returns
124    /// The decoded UTF-8 text.
125    ///
126    /// # Errors
127    /// Returns an error for malformed escapes or decoded bytes that are not
128    /// valid UTF-8.
129    #[inline]
130    fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
131        PercentCodec::decode(self, input)
132    }
133}
134
135impl Codec for PercentCodec {
136    type Value = u8;
137    type Unit = u8;
138    type DecodeError = MiscCodecError;
139    type EncodeError = MiscCodecError;
140
141    const MIN_UNITS_PER_VALUE: usize = 1;
142    const MAX_ENCODE_UNITS_PER_VALUE: usize = 3;
143
144    const MAX_DECODE_UNITS_PER_VALUE: usize = 3;
145
146    /// Returns the exact RFC 3986 encoded width for one byte.
147    ///
148    /// # Parameters
149    /// - `value`: Byte whose encoded width is measured.
150    ///
151    /// # Returns
152    /// One for an unreserved byte, or three for an escaped byte.
153    #[inline]
154    fn encode_len(&self, value: &u8) -> usize {
155        percent_encode_len(*value, PercentEncodingMode::Rfc3986)
156    }
157
158    /// Decodes one raw byte or `%XX` escape.
159    ///
160    /// # Parameters
161    /// - `input`: Encoded byte units.
162    /// - `input_index`: Index of the byte or escape to decode.
163    ///
164    /// # Returns
165    /// The decoded byte and non-zero number of consumed units.
166    ///
167    /// # Errors
168    /// Returns a decode failure when the visible escape is malformed or
169    /// incomplete.
170    ///
171    /// # Safety
172    /// The caller must provide an input index with at least one readable unit
173    /// and a slice satisfying the codec trait's decode preconditions.
174    #[inline]
175    unsafe fn decode(
176        &mut self,
177        input: &[u8],
178        input_index: usize,
179    ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
180        debug_assert!(input_index < input.len());
181
182        let (value, consumed) = percent_decode_byte(input, input_index, false).map_err(|error| {
183            ParseError::into_decode_failure_with_consumed(error, percent_invalid_consumed(input, input_index))
184        })?;
185        debug_assert!(consumed > 0);
186        // SAFETY: `percent_decode_byte` returns a non-zero width for every
187        // successful raw byte or escape.
188        let consumed = nonzero(consumed);
189        Ok((value, consumed))
190    }
191
192    /// Encodes one byte using percent encoding.
193    ///
194    /// # Parameters
195    /// - `value`: Byte to encode.
196    /// - `output`: Destination unit buffer.
197    /// - `output_index`: Start index in `output`.
198    ///
199    /// # Returns
200    /// Number of units written.
201    ///
202    /// # Errors
203    /// This implementation accepts every byte and therefore always succeeds.
204    ///
205    /// # Safety
206    /// The caller must provide enough writable output units for
207    /// [`Self::encode_len`].
208    #[inline]
209    unsafe fn encode(
210        &mut self,
211        value: &u8,
212        output: &mut [u8],
213        output_index: usize,
214    ) -> Result<usize, Self::EncodeError> {
215        let required = percent_encode_len(*value, PercentEncodingMode::Rfc3986);
216        debug_assert!(output_index + required <= output.len());
217
218        let written = percent_encode_byte(*value, output, output_index, PercentEncodingMode::Rfc3986);
219        debug_assert_eq!(written, required);
220        Ok(required)
221    }
222
223    /// Decodes one value after end of input has been confirmed.
224    ///
225    /// # Parameters
226    /// - `input`: Complete encoded byte units.
227    /// - `input_index`: Index of the byte or escape to decode.
228    ///
229    /// # Returns
230    /// The decoded byte and non-zero number of consumed units.
231    ///
232    /// # Errors
233    /// Returns a decode failure when the escape is malformed or incomplete.
234    ///
235    /// # Safety
236    /// The caller must provide an input index with at least one readable unit
237    /// and a slice satisfying the codec trait's EOF decode preconditions.
238    #[inline]
239    unsafe fn decode_eof(
240        &mut self,
241        input: &[u8],
242        input_index: usize,
243    ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
244        debug_assert!(input_index < input.len());
245
246        let (value, consumed) = percent_decode_byte_eof(input, input_index, false).map_err(|error| {
247            map_misc_decode_failure_with_consumed(error, percent_invalid_consumed(input, input_index))
248        })?;
249        debug_assert!(consumed > 0);
250        let consumed = nonzero(consumed);
251        Ok((value, consumed))
252    }
253}