form_net/urlencoded/
encode.rs1use core::fmt;
5
6const UPPER_HEX: &[u8; 16] = b"0123456789ABCDEF";
7
8#[inline]
10const fn is_unreserved(b: u8) -> bool {
11 b.is_ascii_alphanumeric() || matches!(b, b'*' | b'-' | b'.' | b'_')
12}
13
14pub 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 fn push_encoded(out: &mut String, text: &str) {
72 let _ = encode_into(text, out);
74 }
75
76 #[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 #[derive(Debug)]
115 pub struct Serializer<'s> {
116 out: &'s mut String,
117 first: bool,
118 }
119
120 impl<'s> Serializer<'s> {
121 #[must_use]
123 pub fn new(out: &'s mut String) -> Serializer<'s> {
124 Serializer { out, first: true }
125 }
126
127 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 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 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}