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    /// 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}