Skip to main content

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    type Error = MiscCodecError;
95    type Output = String;
96
97    /// Encodes text, using `+` for spaces.
98    ///
99    /// # Parameters
100    /// - `input`: Text to encode.
101    ///
102    /// # Returns
103    /// The form-url-encoded text.
104    ///
105    /// # Errors
106    /// This implementation does not fail for UTF-8 text.
107    #[inline]
108    fn encode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
109        Ok(FormUrlencodedCodec::encode(self, input))
110    }
111}
112
113impl ValueDecoder<str> for FormUrlencodedCodec {
114    type Error = MiscCodecError;
115    type Output = String;
116
117    /// Decodes form-url-encoded text.
118    ///
119    /// # Parameters
120    /// - `input`: Form-url-encoded text to decode.
121    ///
122    /// # Returns
123    /// The decoded UTF-8 text.
124    ///
125    /// # Errors
126    /// Returns an error when an escape is malformed or decoded bytes are not
127    /// valid UTF-8.
128    #[inline]
129    fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
130        FormUrlencodedCodec::decode(self, input)
131    }
132}
133
134impl Codec for FormUrlencodedCodec {
135    type Value = u8;
136    type Unit = u8;
137    type DecodeError = MiscCodecError;
138    type EncodeError = MiscCodecError;
139
140    const MIN_UNITS_PER_VALUE: usize = 1;
141    const MAX_ENCODE_UNITS_PER_VALUE: usize = 3;
142
143    const MAX_DECODE_UNITS_PER_VALUE: usize = 3;
144
145    /// Returns the exact form-url-encoded width for one byte.
146    ///
147    /// # Parameters
148    /// - `value`: Byte whose encoded width is queried.
149    ///
150    /// # Returns
151    /// The number of output bytes required to encode `value`.
152    #[inline]
153    fn encode_len(&self, value: &u8) -> usize {
154        percent_encode_len(*value, PercentEncodingMode::FormUrlencoded)
155    }
156
157    /// Decodes one raw byte, `+`, or `%XX` escape.
158    ///
159    /// # Parameters
160    /// - `input`: Encoded input bytes.
161    /// - `input_index`: Index of the value to decode.
162    ///
163    /// # Returns
164    /// The decoded byte and the non-zero number of consumed input units.
165    ///
166    /// # Errors
167    /// Returns a decode failure when the input is malformed or incomplete.
168    ///
169    /// # Safety
170    /// The caller must provide an input index with at least one readable unit
171    /// and a slice satisfying the codec trait's decode preconditions.
172    #[inline]
173    unsafe fn decode(
174        &mut self,
175        input: &[u8],
176        input_index: usize,
177    ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
178        debug_assert!(input_index < input.len());
179
180        let (value, consumed) = percent_decode_byte(input, input_index, true).map_err(|error| {
181            ParseError::into_decode_failure_with_consumed(error, percent_invalid_consumed(input, input_index))
182        })?;
183        debug_assert!(consumed > 0);
184        // SAFETY: `percent_decode_byte` returns a non-zero width for every
185        // successful raw byte, `+`, or escape.
186        let consumed = nonzero(consumed);
187        Ok((value, consumed))
188    }
189
190    /// Encodes one byte using form URL encoding.
191    ///
192    /// # Parameters
193    /// - `value`: Byte to encode.
194    /// - `output`: Destination buffer.
195    /// - `output_index`: Index at which encoding begins.
196    ///
197    /// # Returns
198    /// The number of output bytes written.
199    ///
200    /// # Errors
201    /// This implementation does not fail when the caller satisfies the safety
202    /// preconditions.
203    ///
204    /// # Safety
205    /// The caller must provide enough writable output units for
206    /// [`Self::encode_len`].
207    #[inline]
208    unsafe fn encode(
209        &mut self,
210        value: &u8,
211        output: &mut [u8],
212        output_index: usize,
213    ) -> Result<usize, Self::EncodeError> {
214        let required = percent_encode_len(*value, PercentEncodingMode::FormUrlencoded);
215        debug_assert!(output_index + required <= output.len());
216
217        let written = percent_encode_byte(*value, output, output_index, PercentEncodingMode::FormUrlencoded);
218        debug_assert_eq!(written, required);
219        Ok(required)
220    }
221
222    /// Decodes one value after end of input has been confirmed.
223    ///
224    /// # Parameters
225    /// - `input`: Encoded input bytes.
226    /// - `input_index`: Index of the value to decode.
227    ///
228    /// # Returns
229    /// The decoded byte and the non-zero number of consumed input units.
230    ///
231    /// # Errors
232    /// Returns a decode failure when the input is malformed or incomplete at
233    /// end of input.
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(input, input_index, true).map_err(|error| match error {
247            ParseError::Incomplete { .. } => map_misc_decode_failure_with_consumed(
248                MiscCodecError::InvalidEscape {
249                    index: input_index,
250                    escape: String::from_utf8_lossy(&input[input_index..]).into_owned(),
251                    reason: "expected two hexadecimal digits".to_owned(),
252                },
253                percent_invalid_consumed(input, input_index),
254            ),
255            ParseError::Invalid(error) => {
256                map_misc_decode_failure_with_consumed(error, percent_invalid_consumed(input, input_index))
257            }
258        })?;
259        debug_assert!(consumed > 0);
260        let consumed = nonzero(consumed);
261        Ok((value, consumed))
262    }
263}