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
6const UPPER_HEX: &[u8; 16] = b"0123456789ABCDEF";
7
8/// Whether `b` is written as itself: ASCII alphanumerics and `*-._`.
9#[inline]
10const fn is_unreserved(b: u8) -> bool {
11    b.is_ascii_alphanumeric() || matches!(b, b'*' | b'-' | b'.' | b'_')
12}
13
14/// Encodes one component (a name or a value) without allocating: every
15/// byte outside ASCII alphanumerics and `*-._` is percent-encoded with
16/// uppercase hex, and space is written as `+`. Runs of bytes that need no
17/// escape are written with one `write_str`.
18///
19/// # Errors
20///
21/// Only what `out` returns.
22///
23/// # Examples
24///
25/// ```
26/// let mut s = String::new();
27/// form_net::encode_into("a b&c", &mut s).unwrap();
28/// assert_eq!(s, "a+b%26c");
29/// ```
30pub fn encode_into<W: fmt::Write>(text: &str, out: &mut W) -> fmt::Result {
31    let bytes = text.as_bytes();
32    let mut run_start = 0usize;
33    for (i, &b) in bytes.iter().enumerate() {
34        if is_unreserved(b) {
35            continue;
36        }
37        if let Some(run) = text.get(run_start..i) {
38            if !run.is_empty() {
39                out.write_str(run)?;
40            }
41        }
42        if b == b' ' {
43            out.write_char('+')?;
44        } else {
45            let hi = UPPER_HEX.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
46            let lo = UPPER_HEX
47                .get(usize::from(b & 0x0F))
48                .copied()
49                .unwrap_or(b'0');
50            out.write_char('%')?;
51            out.write_char(char::from(hi))?;
52            out.write_char(char::from(lo))?;
53        }
54        run_start = i.wrapping_add(1);
55    }
56    if let Some(run) = text.get(run_start..) {
57        if !run.is_empty() {
58            out.write_str(run)?;
59        }
60    }
61    Ok(())
62}
63
64#[cfg(feature = "alloc")]
65mod owned {
66    use alloc::string::String;
67
68    use super::encode_into;
69
70    /// Writes into a `String`, which cannot fail.
71    fn push_encoded(out: &mut String, text: &str) {
72        // Writing into a String never returns an error.
73        let _ = encode_into(text, out);
74    }
75
76    /// Builds a urlencoded body or query from pairs, byte-identical to the
77    /// WHATWG serializer for UTF-8: names and values are encoded as
78    /// [`encode_into`](crate::encode_into) does, joined with `=`, and pairs
79    /// are joined with `&`. Infallible: every `str` is valid input.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// assert_eq!(form_net::serialize_urlencoded([("q", "rust forms"), ("page", "2")]), "q=rust+forms&page=2");
85    /// assert_eq!(form_net::serialize_urlencoded([("a+b", "1&2=3")]), "a%2Bb=1%262%3D3");
86    /// assert_eq!(form_net::serialize_urlencoded([("city", "Zürich")]), "city=Z%C3%BCrich");
87    /// assert_eq!(form_net::serialize_urlencoded(Vec::<(&str, &str)>::new()), "");
88    /// ```
89    #[must_use]
90    pub fn serialize_urlencoded<I, N, V>(pairs: I) -> String
91    where
92        I: IntoIterator<Item = (N, V)>,
93        N: AsRef<str>,
94        V: AsRef<str>,
95    {
96        let mut out = String::new();
97        Serializer::new(&mut out).append_pairs(pairs);
98        out
99    }
100
101    /// Appends urlencoded pairs to an existing string, for example a URL that
102    /// already ends in `?`. The first pair is not preceded by `&`; whatever
103    /// the string held before is left untouched.
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// let mut url = String::from("https://example.com/search?");
109    /// form_net::Serializer::new(&mut url)
110    ///     .append_pair("q", "rust forms")
111    ///     .append_pairs([("page", "2"), ("sort", "new")]);
112    /// assert_eq!(url, "https://example.com/search?q=rust+forms&page=2&sort=new");
113    /// ```
114    #[derive(Debug)]
115    pub struct Serializer<'s> {
116        out: &'s mut String,
117        first: bool,
118    }
119
120    impl<'s> Serializer<'s> {
121        /// A serializer that appends to `out`.
122        #[must_use]
123        pub fn new(out: &'s mut String) -> Serializer<'s> {
124            Serializer { out, first: true }
125        }
126
127        /// Appends one pair.
128        pub fn append_pair(&mut self, name: &str, value: &str) -> &mut Self {
129            if !self.first {
130                self.out.push('&');
131            }
132            self.first = false;
133            push_encoded(self.out, name);
134            self.out.push('=');
135            push_encoded(self.out, value);
136            self
137        }
138
139        /// Appends every pair, in order.
140        pub fn append_pairs<I, N, V>(&mut self, pairs: I) -> &mut Self
141        where
142            I: IntoIterator<Item = (N, V)>,
143            N: AsRef<str>,
144            V: AsRef<str>,
145        {
146            for (name, value) in pairs {
147                self.append_pair(name.as_ref(), value.as_ref());
148            }
149            self
150        }
151
152        /// Ends the serializer and returns the string.
153        pub fn finish(self) -> &'s mut String {
154            self.out
155        }
156    }
157}
158
159#[cfg(feature = "alloc")]
160pub use owned::{Serializer, serialize_urlencoded};
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use alloc::string::String;
166
167    #[test]
168    fn test_encode_every_ascii_byte_matches_whatwg() {
169        for b in 0u8..=0x7F {
170            let s = String::from(char::from(b));
171            let mut out = String::new();
172            encode_into(&s, &mut out).unwrap();
173            let expected = if b == b' ' {
174                String::from("+")
175            } else if b.is_ascii_alphanumeric() || b"*-._".contains(&b) {
176                s.clone()
177            } else {
178                alloc::format!("%{b:02X}")
179            };
180            assert_eq!(out, expected, "byte {b:#x}");
181        }
182    }
183
184    #[test]
185    fn test_encode_non_ascii_and_empty() {
186        let mut out = String::new();
187        encode_into("", &mut out).unwrap();
188        assert_eq!(out, "");
189        encode_into("€", &mut out).unwrap();
190        assert_eq!(out, "%E2%82%AC");
191    }
192
193    #[cfg(feature = "alloc")]
194    #[test]
195    fn test_serializer_appends_and_finishes() {
196        let mut s = String::from("x?");
197        let mut ser = Serializer::new(&mut s);
198        ser.append_pair("", "").append_pair("a", "b");
199        assert_eq!(ser.finish().as_str(), "x?=&a=b");
200    }
201}