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]
183 pub fn new(out: &'s mut String) -> Serializer<'s> {
184 Serializer { out, first: true }
185 }
186
187 pub fn append_pair(&mut self, name: &str, value: &str) -> &mut Self {
189 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 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 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}