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 pub struct Serializer<'s> {
165 out: &'s mut String,
166 first: bool,
167 }
168
169 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 #[must_use]
192 pub fn new(out: &'s mut String) -> Serializer<'s> {
193 Serializer { out, first: true }
194 }
195
196 pub fn append_pair(&mut self, name: &str, value: &str) -> &mut Self {
208 self.out
211 .reserve(name.len().saturating_add(value.len()).saturating_add(2));
212 if !self.first {
213 self.out.push('&');
214 }
215 self.first = false;
216 push_encoded(self.out, name);
217 self.out.push('=');
218 push_encoded(self.out, value);
219 self
220 }
221
222 pub fn append_pairs<I, N, V>(&mut self, pairs: I) -> &mut Self
233 where
234 I: IntoIterator<Item = (N, V)>,
235 N: AsRef<str>,
236 V: AsRef<str>,
237 {
238 for (name, value) in pairs {
239 self.append_pair(name.as_ref(), value.as_ref());
240 }
241 self
242 }
243
244 pub fn finish(self) -> &'s mut String {
257 self.out
258 }
259 }
260}
261
262#[cfg(feature = "alloc")]
263pub use owned::{Serializer, serialize_urlencoded};
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use alloc::string::String;
269
270 #[test]
271 fn test_encode_every_ascii_byte_matches_whatwg() {
272 for b in 0u8..=0x7F {
273 let s = String::from(char::from(b));
274 let mut out = String::new();
275 encode_into(&s, &mut out).unwrap();
276 let expected = if b == b' ' {
277 String::from("+")
278 } else if b.is_ascii_alphanumeric() || b"*-._".contains(&b) {
279 s.clone()
280 } else {
281 alloc::format!("%{b:02X}")
282 };
283 assert_eq!(out, expected, "byte {b:#x}");
284 }
285 }
286
287 #[test]
288 fn test_encode_non_ascii_and_empty() {
289 let mut out = String::new();
290 encode_into("", &mut out).unwrap();
291 assert_eq!(out, "");
292 encode_into("€", &mut out).unwrap();
293 assert_eq!(out, "%E2%82%AC");
294 }
295
296 #[cfg(feature = "alloc")]
297 #[test]
298 fn test_serializer_appends_and_finishes() {
299 let mut s = String::from("x?");
300 let mut ser = Serializer::new(&mut s);
301 ser.append_pair("", "").append_pair("a", "b");
302 assert_eq!(ser.finish().as_str(), "x?=&a=b");
303 }
304}