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}