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    #[derive(Debug)]
165    pub struct Serializer<'s> {
166        out: &'s mut String,
167        first: bool,
168    }
169
170    impl<'s> Serializer<'s> {
171        /// A serializer that appends to `out`.
172        #[must_use]
173        pub fn new(out: &'s mut String) -> Serializer<'s> {
174            Serializer { out, first: true }
175        }
176
177        /// Appends one pair.
178        pub fn append_pair(&mut self, name: &str, value: &str) -> &mut Self {
179            // Exact when nothing needs escaping, the common case: one
180            // allocation step per pair at most.
181            self.out
182                .reserve(name.len().saturating_add(value.len()).saturating_add(2));
183            if !self.first {
184                self.out.push('&');
185            }
186            self.first = false;
187            push_encoded(self.out, name);
188            self.out.push('=');
189            push_encoded(self.out, value);
190            self
191        }
192
193        /// Appends every pair, in order.
194        pub fn append_pairs<I, N, V>(&mut self, pairs: I) -> &mut Self
195        where
196            I: IntoIterator<Item = (N, V)>,
197            N: AsRef<str>,
198            V: AsRef<str>,
199        {
200            for (name, value) in pairs {
201                self.append_pair(name.as_ref(), value.as_ref());
202            }
203            self
204        }
205
206        /// Ends the serializer and returns the string.
207        pub fn finish(self) -> &'s mut String {
208            self.out
209        }
210    }
211}
212
213#[cfg(feature = "alloc")]
214pub use owned::{Serializer, serialize_urlencoded};
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use alloc::string::String;
220
221    #[test]
222    fn test_encode_every_ascii_byte_matches_whatwg() {
223        for b in 0u8..=0x7F {
224            let s = String::from(char::from(b));
225            let mut out = String::new();
226            encode_into(&s, &mut out).unwrap();
227            let expected = if b == b' ' {
228                String::from("+")
229            } else if b.is_ascii_alphanumeric() || b"*-._".contains(&b) {
230                s.clone()
231            } else {
232                alloc::format!("%{b:02X}")
233            };
234            assert_eq!(out, expected, "byte {b:#x}");
235        }
236    }
237
238    #[test]
239    fn test_encode_non_ascii_and_empty() {
240        let mut out = String::new();
241        encode_into("", &mut out).unwrap();
242        assert_eq!(out, "");
243        encode_into("€", &mut out).unwrap();
244        assert_eq!(out, "%E2%82%AC");
245    }
246
247    #[cfg(feature = "alloc")]
248    #[test]
249    fn test_serializer_appends_and_finishes() {
250        let mut s = String::from("x?");
251        let mut ser = Serializer::new(&mut s);
252        ser.append_pair("", "").append_pair("a", "b");
253        assert_eq!(ser.finish().as_str(), "x?=&a=b");
254    }
255}