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 crate::percent_codec::{
11    percent_decode_byte,
12    percent_decode_bytes,
13    percent_encode_byte,
14    percent_encode_bytes,
15};
16use crate::{
17    Codec,
18    MiscCodecError,
19    MiscCodecResult,
20    ValueDecoder,
21    ValueEncoder,
22};
23
24/// Encodes and decodes `application/x-www-form-urlencoded` text fragments.
25///
26/// Its low-level [`Codec<Value = u8, Unit = u8>`] implementation converts one
27/// byte at a time, including the form-specific space and `+` mapping. UTF-8
28/// validation remains part of the owned [`decode`](Self::decode) helper.
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30pub struct FormUrlencodedCodec;
31
32impl FormUrlencodedCodec {
33    /// Creates a form-url-encoded codec.
34    ///
35    /// # Returns
36    /// Form URL encoded codec.
37    #[inline]
38    pub fn new() -> Self {
39        Self
40    }
41
42    /// Encodes text, using `+` for spaces.
43    ///
44    /// # Parameters
45    /// - `text`: Text to encode.
46    ///
47    /// # Returns
48    /// Form-url-encoded text.
49    #[inline]
50    pub fn encode(&self, text: &str) -> String {
51        percent_encode_bytes(text.as_bytes(), true)
52    }
53
54    /// Decodes text, treating `+` as space.
55    ///
56    /// # Parameters
57    /// - `text`: Text to decode.
58    ///
59    /// # Returns
60    /// Decoded UTF-8 text.
61    ///
62    /// # Errors
63    /// Returns [`MiscCodecError`] when an escape is malformed or decoded bytes
64    /// are not valid UTF-8.
65    #[inline]
66    pub fn decode(&self, text: &str) -> MiscCodecResult<String> {
67        String::from_utf8(percent_decode_bytes(text, true)?)
68            .map_err(MiscCodecError::from)
69    }
70}
71
72impl ValueEncoder<str> for FormUrlencodedCodec {
73    type Error = MiscCodecError;
74    type Output = String;
75
76    /// Encodes text, using `+` for spaces.
77    #[inline]
78    fn encode(&self, input: &str) -> Result<Self::Output, Self::Error> {
79        Ok(FormUrlencodedCodec::encode(self, input))
80    }
81}
82
83impl ValueDecoder<str> for FormUrlencodedCodec {
84    type Error = MiscCodecError;
85    type Output = String;
86
87    /// Decodes form-url-encoded text.
88    #[inline]
89    fn decode(&self, input: &str) -> Result<Self::Output, Self::Error> {
90        FormUrlencodedCodec::decode(self, input)
91    }
92}
93
94unsafe impl Codec for FormUrlencodedCodec {
95    type Value = u8;
96    type Unit = u8;
97    type DecodeError = MiscCodecError;
98    type EncodeError = MiscCodecError;
99
100    /// Returns the shortest representation length for one byte.
101    #[inline(always)]
102    fn min_units_per_value(&self) -> core::num::NonZeroUsize {
103        core::num::NonZeroUsize::MIN
104    }
105
106    /// Returns the longest `%XX` representation length for one byte.
107    #[inline(always)]
108    fn max_units_per_value(&self) -> core::num::NonZeroUsize {
109        unsafe { core::num::NonZeroUsize::new_unchecked(3) }
110    }
111
112    /// Decodes one raw byte, `+`, or `%XX` escape.
113    #[inline]
114    unsafe fn decode_unchecked(
115        &self,
116        input: &[u8],
117        index: usize,
118    ) -> Result<(u8, core::num::NonZeroUsize), Self::DecodeError> {
119        debug_assert!(index < input.len());
120
121        let (value, consumed) = percent_decode_byte(input, index, true)?;
122        debug_assert!(consumed > 0);
123        // SAFETY: `percent_decode_byte` returns a non-zero width for every
124        // successful raw byte, `+`, or escape.
125        let consumed =
126            unsafe { core::num::NonZeroUsize::new_unchecked(consumed) };
127        Ok((value, consumed))
128    }
129
130    /// Encodes one byte using form URL encoding.
131    #[inline]
132    unsafe fn encode_unchecked(
133        &self,
134        value: &u8,
135        output: &mut [u8],
136        index: usize,
137    ) -> Result<usize, Self::EncodeError> {
138        debug_assert!(
139            index
140                + if *value == b' '
141                    || value.is_ascii_alphanumeric()
142                    || matches!(*value, b'-' | b'.' | b'_' | b'~')
143                {
144                    1
145                } else {
146                    3
147                }
148                <= output.len()
149        );
150
151        Ok(percent_encode_byte(*value, output, index, true))
152    }
153}