Skip to main content

form_net/urlencoded/
encode.rs

1//! The urlencoded serializer, byte-identical to the WHATWG serializer for
2//! UTF-8.
3
4use core::fmt;
5
6/// `%XX` for every byte, so an escape is one `write_str` of three bytes
7/// with nothing to check.
8const ESCAPES: [&str; 256] = [
9    "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07", "%08", "%09", "%0A", "%0B", "%0C",
10    "%0D", "%0E", "%0F", "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17", "%18", "%19",
11    "%1A", "%1B", "%1C", "%1D", "%1E", "%1F", "%20", "%21", "%22", "%23", "%24", "%25", "%26",
12    "%27", "%28", "%29", "%2A", "%2B", "%2C", "%2D", "%2E", "%2F", "%30", "%31", "%32", "%33",
13    "%34", "%35", "%36", "%37", "%38", "%39", "%3A", "%3B", "%3C", "%3D", "%3E", "%3F", "%40",
14    "%41", "%42", "%43", "%44", "%45", "%46", "%47", "%48", "%49", "%4A", "%4B", "%4C", "%4D",
15    "%4E", "%4F", "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57", "%58", "%59", "%5A",
16    "%5B", "%5C", "%5D", "%5E", "%5F", "%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67",
17    "%68", "%69", "%6A", "%6B", "%6C", "%6D", "%6E", "%6F", "%70", "%71", "%72", "%73", "%74",
18    "%75", "%76", "%77", "%78", "%79", "%7A", "%7B", "%7C", "%7D", "%7E", "%7F", "%80", "%81",
19    "%82", "%83", "%84", "%85", "%86", "%87", "%88", "%89", "%8A", "%8B", "%8C", "%8D", "%8E",
20    "%8F", "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97", "%98", "%99", "%9A", "%9B",
21    "%9C", "%9D", "%9E", "%9F", "%A0", "%A1", "%A2", "%A3", "%A4", "%A5", "%A6", "%A7", "%A8",
22    "%A9", "%AA", "%AB", "%AC", "%AD", "%AE", "%AF", "%B0", "%B1", "%B2", "%B3", "%B4", "%B5",
23    "%B6", "%B7", "%B8", "%B9", "%BA", "%BB", "%BC", "%BD", "%BE", "%BF", "%C0", "%C1", "%C2",
24    "%C3", "%C4", "%C5", "%C6", "%C7", "%C8", "%C9", "%CA", "%CB", "%CC", "%CD", "%CE", "%CF",
25    "%D0", "%D1", "%D2", "%D3", "%D4", "%D5", "%D6", "%D7", "%D8", "%D9", "%DA", "%DB", "%DC",
26    "%DD", "%DE", "%DF", "%E0", "%E1", "%E2", "%E3", "%E4", "%E5", "%E6", "%E7", "%E8", "%E9",
27    "%EA", "%EB", "%EC", "%ED", "%EE", "%EF", "%F0", "%F1", "%F2", "%F3", "%F4", "%F5", "%F6",
28    "%F7", "%F8", "%F9", "%FA", "%FB", "%FC", "%FD", "%FE", "%FF",
29];
30
31/// The escape of `b` as text.
32#[inline]
33fn escape(b: u8) -> &'static str {
34    ESCAPES.get(usize::from(b)).copied().unwrap_or("%00")
35}
36
37/// Whether each byte is written as itself: ASCII alphanumerics and `*-._`.
38// Built at compile time; a `u8` index into 256 entries cannot go out of
39// bounds, and `i += 1` stops before 255 overflows.
40#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
41const UNRESERVED: [bool; 256] = {
42    let mut t = [false; 256];
43    let mut i = 0u8;
44    loop {
45        t[i as usize] = i.is_ascii_alphanumeric() || matches!(i, b'*' | b'-' | b'.' | b'_');
46        if i == 255 {
47            break;
48        }
49        i += 1;
50    }
51    t
52};
53
54#[inline]
55fn is_unreserved(b: u8) -> bool {
56    *UNRESERVED.get(usize::from(b)).unwrap_or(&false)
57}
58
59/// The index of the first byte at or after `i` that must be escaped, or
60/// `bytes.len()`. Form names and values are short and their runs shorter,
61/// so a table lookup per byte beats a word test (measured).
62#[inline]
63fn next_reserved(bytes: &[u8], mut i: usize) -> usize {
64    while let Some(&b) = bytes.get(i) {
65        if !is_unreserved(b) {
66            return i;
67        }
68        i = i.wrapping_add(1);
69    }
70    bytes.len()
71}
72
73/// Encodes one component (a name or a value) without allocating: every
74/// byte outside ASCII alphanumerics and `*-._` is percent-encoded with
75/// uppercase hex, and space is written as `+`. Runs of bytes that need no
76/// escape are written with one `write_str`.
77///
78/// # Errors
79///
80/// Only what `out` returns.
81///
82/// # Examples
83///
84/// ```
85/// let mut s = String::new();
86/// form_net::encode_into("a b&c", &mut s).unwrap();
87/// assert_eq!(s, "a+b%26c");
88/// ```
89pub fn encode_into<W: fmt::Write>(text: &str, out: &mut W) -> fmt::Result {
90    let bytes = text.as_bytes();
91    let mut i = 0usize;
92    while i < bytes.len() {
93        let end = next_reserved(bytes, i);
94        // Runs end before a byte that is escaped, so they end on a char
95        // boundary (escaped bytes include every non-ASCII byte).
96        if let Some(run) = text.get(i..end) {
97            if !run.is_empty() {
98                out.write_str(run)?;
99            }
100        }
101        let Some(&b) = bytes.get(end) else { break };
102        if b == b' ' {
103            out.write_char('+')?;
104        } else {
105            out.write_str(escape(b))?;
106        }
107        i = end.wrapping_add(1);
108    }
109    Ok(())
110}
111
112#[cfg(feature = "alloc")]
113mod owned {
114    use alloc::string::String;
115
116    use super::encode_into;
117
118    /// Writes into a `String`, which cannot fail.
119    fn push_encoded(out: &mut String, text: &str) {
120        // Writing into a String never returns an error.
121        let _ = encode_into(text, out);
122    }
123
124    /// Builds a urlencoded body or query from pairs, byte-identical to the
125    /// WHATWG serializer for UTF-8: names and values are encoded as
126    /// [`encode_into`](crate::encode_into) does, joined with `=`, and pairs
127    /// are joined with `&`. Infallible: every `str` is valid input.
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// assert_eq!(form_net::serialize_urlencoded([("q", "rust forms"), ("page", "2")]), "q=rust+forms&page=2");
133    /// assert_eq!(form_net::serialize_urlencoded([("a+b", "1&2=3")]), "a%2Bb=1%262%3D3");
134    /// assert_eq!(form_net::serialize_urlencoded([("city", "Zürich")]), "city=Z%C3%BCrich");
135    /// assert_eq!(form_net::serialize_urlencoded(Vec::<(&str, &str)>::new()), "");
136    /// ```
137    #[must_use]
138    pub fn serialize_urlencoded<I, N, V>(pairs: I) -> String
139    where
140        I: IntoIterator<Item = (N, V)>,
141        N: AsRef<str>,
142        V: AsRef<str>,
143    {
144        // Room for a typical form up front: growing from empty took four
145        // reallocations for ten pairs.
146        let mut out = String::with_capacity(256);
147        Serializer::new(&mut out).append_pairs(pairs);
148        out
149    }
150
151    /// Appends urlencoded pairs to an existing string, for example a URL that
152    /// already ends in `?`. The first pair is not preceded by `&`; whatever
153    /// the string held before is left untouched.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// let mut url = String::from("https://example.com/search?");
159    /// form_net::Serializer::new(&mut url)
160    ///     .append_pair("q", "rust forms")
161    ///     .append_pairs([("page", "2"), ("sort", "new")]);
162    /// assert_eq!(url, "https://example.com/search?q=rust+forms&page=2&sort=new");
163    /// ```
164    pub struct Serializer<'s> {
165        out: &'s mut String,
166        first: bool,
167    }
168
169    /// Shows how much was written, never what: the output holds every
170    /// value (T-30).
171    impl core::fmt::Debug for Serializer<'_> {
172        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
173            f.debug_struct("Serializer")
174                .field("len", &self.out.len())
175                .field("first", &self.first)
176                .finish()
177        }
178    }
179
180    impl<'s> Serializer<'s> {
181        /// A serializer that appends to `out`.
182        #[must_use]
183        pub fn new(out: &'s mut String) -> Serializer<'s> {
184            Serializer { out, first: true }
185        }
186
187        /// Appends one pair.
188        pub fn append_pair(&mut self, name: &str, value: &str) -> &mut Self {
189            // Exact when nothing needs escaping, the common case: one
190            // allocation step per pair at most.
191            self.out
192                .reserve(name.len().saturating_add(value.len()).saturating_add(2));
193            if !self.first {
194                self.out.push('&');
195            }
196            self.first = false;
197            push_encoded(self.out, name);
198            self.out.push('=');
199            push_encoded(self.out, value);
200            self
201        }
202
203        /// Appends every pair, in order.
204        pub fn append_pairs<I, N, V>(&mut self, pairs: I) -> &mut Self
205        where
206            I: IntoIterator<Item = (N, V)>,
207            N: AsRef<str>,
208            V: AsRef<str>,
209        {
210            for (name, value) in pairs {
211                self.append_pair(name.as_ref(), value.as_ref());
212            }
213            self
214        }
215
216        /// Ends the serializer and returns the string.
217        pub fn finish(self) -> &'s mut String {
218            self.out
219        }
220    }
221}
222
223#[cfg(feature = "alloc")]
224pub use owned::{Serializer, serialize_urlencoded};
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use alloc::string::String;
230
231    #[test]
232    fn test_encode_every_ascii_byte_matches_whatwg() {
233        for b in 0u8..=0x7F {
234            let s = String::from(char::from(b));
235            let mut out = String::new();
236            encode_into(&s, &mut out).unwrap();
237            let expected = if b == b' ' {
238                String::from("+")
239            } else if b.is_ascii_alphanumeric() || b"*-._".contains(&b) {
240                s.clone()
241            } else {
242                alloc::format!("%{b:02X}")
243            };
244            assert_eq!(out, expected, "byte {b:#x}");
245        }
246    }
247
248    #[test]
249    fn test_encode_non_ascii_and_empty() {
250        let mut out = String::new();
251        encode_into("", &mut out).unwrap();
252        assert_eq!(out, "");
253        encode_into("€", &mut out).unwrap();
254        assert_eq!(out, "%E2%82%AC");
255    }
256
257    #[cfg(feature = "alloc")]
258    #[test]
259    fn test_serializer_appends_and_finishes() {
260        let mut s = String::from("x?");
261        let mut ser = Serializer::new(&mut s);
262        ser.append_pair("", "").append_pair("a", "b");
263        assert_eq!(ser.finish().as_str(), "x?=&a=b");
264    }
265}