qubit_codec_misc/form_urlencoded_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//! `application/x-www-form-urlencoded` text codec.
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 crate::MiscCodecError;
19use crate::MiscCodecResult;
20use crate::internal::ParseError;
21use crate::internal::PercentEncodingMode;
22use crate::misc_error::map_misc_decode_failure_with_consumed;
23use crate::percent::percent_decode_byte;
24use crate::percent::percent_decode_bytes;
25use crate::percent::percent_encode_byte;
26use crate::percent::percent_encode_bytes;
27use crate::percent::percent_encode_len;
28use crate::percent::percent_invalid_consumed;
29
30/// Encodes and decodes `application/x-www-form-urlencoded` text fragments.
31///
32/// Its low-level [`Codec<Value = u8, Unit = u8>`] implementation converts one
33/// byte at a time, including the form-specific space and `+` mapping. UTF-8
34/// validation remains part of the owned [`decode`](Self::decode) helper.
35/// Streaming skip/replace hooks may consume the malformed span visible in the
36/// current input chunk, so recovery output for malformed input can depend on
37/// chunk boundaries. This does not affect valid input or strict decoding.
38///
39/// # Examples
40///
41/// ```
42/// use qubit_codec_misc::FormUrlencodedCodec;
43///
44/// let codec = FormUrlencodedCodec::new();
45/// let encoded = codec.encode("hello world");
46/// assert_eq!(encoded, "hello+world");
47/// assert_eq!(codec.decode(&encoded).unwrap(), "hello world");
48/// ```
49#[must_use]
50#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
51pub struct FormUrlencodedCodec;
52
53impl FormUrlencodedCodec {
54 /// Creates a form-url-encoded codec.
55 ///
56 /// # Returns
57 /// Form URL encoded codec.
58 #[inline]
59 pub fn new() -> Self {
60 Self
61 }
62
63 /// Encodes text, using `+` for spaces.
64 ///
65 /// # Parameters
66 /// - `text`: Text to encode.
67 ///
68 /// # Returns
69 /// Form-url-encoded text.
70 #[must_use]
71 #[inline]
72 pub fn encode(&self, text: &str) -> String {
73 percent_encode_bytes(text.as_bytes(), PercentEncodingMode::FormUrlencoded)
74 }
75
76 /// Decodes text, treating `+` as space.
77 ///
78 /// # Parameters
79 /// - `text`: Text to decode.
80 ///
81 /// # Returns
82 /// Decoded UTF-8 text.
83 ///
84 /// # Errors
85 /// Returns [`MiscCodecError`] when an escape is malformed or decoded bytes
86 /// are not valid UTF-8.
87 #[inline]
88 pub fn decode(&self, text: &str) -> MiscCodecResult<String> {
89 String::from_utf8(percent_decode_bytes(text, true)?).map_err(MiscCodecError::from)
90 }
91}
92
93impl ValueEncoder<str> for FormUrlencodedCodec {
94 /// Errors produced while converting an encoded value.
95 ///
96 /// Form URL encoding accepts every UTF-8 string, so this implementation
97 /// does not produce an error.
98 type Error = MiscCodecError;
99
100 /// Owned form-url-encoded text produced from the input string.
101 type Output = String;
102
103 /// Encodes text, using `+` for spaces.
104 ///
105 /// # Parameters
106 /// - `input`: Text to encode.
107 ///
108 /// # Returns
109 /// The form-url-encoded text.
110 ///
111 /// # Errors
112 /// This implementation does not fail for UTF-8 text.
113 #[inline]
114 fn encode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
115 Ok(FormUrlencodedCodec::encode(self, input))
116 }
117}
118
119impl ValueDecoder<str> for FormUrlencodedCodec {
120 /// Errors produced when the encoded value is malformed or not valid UTF-8.
121 type Error = MiscCodecError;
122
123 /// Owned UTF-8 text produced from a successfully decoded input string.
124 type Output = String;
125
126 /// Decodes form-url-encoded text.
127 ///
128 /// # Parameters
129 /// - `input`: Form-url-encoded text to decode.
130 ///
131 /// # Returns
132 /// The decoded UTF-8 text.
133 ///
134 /// # Errors
135 /// Returns an error when an escape is malformed or decoded bytes are not
136 /// valid UTF-8.
137 #[inline]
138 fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
139 FormUrlencodedCodec::decode(self, input)
140 }
141}
142
143impl Codec for FormUrlencodedCodec {
144 /// A decoded or encoded value is one input byte.
145 type Value = u8;
146
147 /// The encoded representation is stored in byte units.
148 type Unit = u8;
149
150 /// Errors raised when an encoded byte sequence cannot be decoded.
151 type DecodeError = MiscCodecError;
152
153 /// Errors raised when a value cannot be encoded.
154 type EncodeError = MiscCodecError;
155
156 const MIN_UNITS_PER_VALUE: usize = 1;
157 const MAX_ENCODE_UNITS_PER_VALUE: usize = 3;
158
159 const MAX_DECODE_UNITS_PER_VALUE: usize = 3;
160
161 /// Returns the exact form-url-encoded width for one byte.
162 ///
163 /// # Parameters
164 /// - `value`: Byte whose encoded width is queried.
165 ///
166 /// # Returns
167 /// The number of output bytes required to encode `value`.
168 #[inline]
169 fn encode_len(&self, value: &u8) -> usize {
170 percent_encode_len(*value, PercentEncodingMode::FormUrlencoded)
171 }
172
173 /// Decodes one raw byte, `+`, or `%XX` escape.
174 ///
175 /// # Parameters
176 /// - `input`: Encoded input bytes.
177 /// - `input_index`: Index of the value to decode.
178 ///
179 /// # Returns
180 /// The decoded byte and the non-zero number of consumed input units.
181 ///
182 /// # Errors
183 /// Returns a decode failure when the input is malformed or incomplete.
184 ///
185 /// # Safety
186 /// The caller must provide an input index with at least one readable unit
187 /// and a slice satisfying the codec trait's decode preconditions.
188 #[inline]
189 unsafe fn decode(
190 &mut self,
191 input: &[u8],
192 input_index: usize,
193 ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
194 debug_assert!(input_index < input.len());
195
196 let (value, consumed) = percent_decode_byte(input, input_index, true).map_err(|error| {
197 ParseError::into_decode_failure_with_consumed(error, percent_invalid_consumed(input, input_index))
198 })?;
199 debug_assert!(consumed > 0);
200 // SAFETY: `percent_decode_byte` returns a non-zero width for every
201 // successful raw byte, `+`, or escape.
202 let consumed = nonzero(consumed);
203 Ok((value, consumed))
204 }
205
206 /// Encodes one byte using form URL encoding.
207 ///
208 /// # Parameters
209 /// - `value`: Byte to encode.
210 /// - `output`: Destination buffer.
211 /// - `output_index`: Index at which encoding begins.
212 ///
213 /// # Returns
214 /// The number of output bytes written.
215 ///
216 /// # Errors
217 /// This implementation does not fail when the caller satisfies the safety
218 /// preconditions.
219 ///
220 /// # Safety
221 /// The caller must provide enough writable output units for
222 /// [`Self::encode_len`].
223 #[inline]
224 unsafe fn encode(
225 &mut self,
226 value: &u8,
227 output: &mut [u8],
228 output_index: usize,
229 ) -> Result<usize, Self::EncodeError> {
230 let required = percent_encode_len(*value, PercentEncodingMode::FormUrlencoded);
231 debug_assert!(output_index + required <= output.len());
232
233 let written = percent_encode_byte(*value, output, output_index, PercentEncodingMode::FormUrlencoded);
234 debug_assert_eq!(written, required);
235 Ok(required)
236 }
237
238 /// Decodes one value after end of input has been confirmed.
239 ///
240 /// # Parameters
241 /// - `input`: Encoded input bytes.
242 /// - `input_index`: Index of the value to decode.
243 ///
244 /// # Returns
245 /// The decoded byte and the non-zero number of consumed input units.
246 ///
247 /// # Errors
248 /// Returns a decode failure when the input is malformed or incomplete at
249 /// end of input.
250 ///
251 /// # Safety
252 /// The caller must provide an input index with at least one readable unit
253 /// and a slice satisfying the codec trait's EOF decode preconditions.
254 #[inline]
255 unsafe fn decode_eof(
256 &mut self,
257 input: &[u8],
258 input_index: usize,
259 ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
260 debug_assert!(input_index < input.len());
261
262 let (value, consumed) = percent_decode_byte(input, input_index, true).map_err(|error| match error {
263 ParseError::Incomplete { .. } => map_misc_decode_failure_with_consumed(
264 MiscCodecError::InvalidEscape {
265 index: input_index,
266 escape: String::from_utf8_lossy(&input[input_index..]).into_owned(),
267 reason: "expected two hexadecimal digits".to_owned(),
268 },
269 percent_invalid_consumed(input, input_index),
270 ),
271 ParseError::Invalid(error) => {
272 map_misc_decode_failure_with_consumed(error, percent_invalid_consumed(input, input_index))
273 }
274 })?;
275 debug_assert!(consumed > 0);
276 let consumed = nonzero(consumed);
277 Ok((value, consumed))
278 }
279}