1use core::fmt;
5
6const 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#[inline]
33fn escape(b: u8) -> &'static str {
34 ESCAPES.get(usize::from(b)).copied().unwrap_or("%00")
35}
36
37#[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#[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
73pub 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 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 fn push_encoded(out: &mut String, text: &str) {
120 let _ = encode_into(text, out);
122 }
123
124 #[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 let mut out = String::with_capacity(256);
147 Serializer::new(&mut out).append_pairs(pairs);
148 out
149 }
150
151 #[derive(Debug)]
165 pub struct Serializer<'s> {
166 out: &'s mut String,
167 first: bool,
168 }
169
170 impl<'s> Serializer<'s> {
171 #[must_use]
173 pub fn new(out: &'s mut String) -> Serializer<'s> {
174 Serializer { out, first: true }
175 }
176
177 pub fn append_pair(&mut self, name: &str, value: &str) -> &mut Self {
179 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 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 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}