1use std::borrow::Cow;
22
23use crate::chars::{ALLOWED, HEX_UPPER, NONE, QS, Set, UNRESERVED, pct_at, push_pct};
24
25#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
28pub enum Component {
29 UserInfo,
31 Path,
33 Query,
35 QueryPart,
37 Fragment,
39 Opaque,
42}
43
44struct Quoter {
45 safe: Set,
46 protected: Set,
47 qs: bool,
48}
49
50static USERINFO: Quoter = Quoter {
51 safe: ALLOWED.union(QS),
52 protected: NONE,
53 qs: false,
54};
55static PATH: Quoter = Quoter {
56 safe: ALLOWED.union(QS).union(Set::new(b"@:/+")),
57 protected: Set::new(b"/+"),
58 qs: false,
59};
60static QUERY: Quoter = Quoter {
61 safe: ALLOWED.union(Set::new(b"?/:@=+&;")),
62 protected: QS,
63 qs: true,
64};
65static QUERY_LITERAL: Quoter = Quoter {
67 safe: QUERY.safe.without(b'+'),
68 protected: QS,
69 qs: true,
70};
71static QUERY_PART: Quoter = Quoter {
72 safe: ALLOWED.union(Set::new(b"?/:@")),
73 protected: NONE,
74 qs: true,
75};
76static FRAGMENT: Quoter = Quoter {
77 safe: ALLOWED.union(QS).union(Set::new(b"?/:@")),
78 protected: NONE,
79 qs: false,
80};
81static OPAQUE: Quoter = Quoter {
82 safe: UNRESERVED,
83 protected: NONE,
84 qs: false,
85};
86
87const PATH_SAFE: Set = Set::new(b"/%");
88
89impl Component {
90 fn quoter(self, requote: bool) -> &'static Quoter {
91 match self {
92 Component::UserInfo => &USERINFO,
93 Component::Path => &PATH,
94 Component::Query if requote => &QUERY,
95 Component::Query => &QUERY_LITERAL,
96 Component::QueryPart => &QUERY_PART,
97 Component::Fragment => &FRAGMENT,
98 Component::Opaque => &OPAQUE,
99 }
100 }
101}
102
103pub fn quote(src: &str, component: Component) -> Cow<'_, str> {
105 quote_with(src, component, false)
106}
107
108pub fn requote(src: &str, component: Component) -> Cow<'_, str> {
110 quote_with(src, component, true)
111}
112
113pub fn unquote(src: &str, component: Component) -> Cow<'_, str> {
119 match component {
120 Component::Query => unquote_with(src, true, &QS),
121 Component::QueryPart => unquote_with(src, true, &NONE),
122 _ => unquote_with(src, false, &NONE),
123 }
124}
125
126fn quote_with(src: &str, component: Component, requote: bool) -> Cow<'_, str> {
127 let q = component.quoter(requote);
128 let bytes = src.as_bytes();
129 let is_safe = |b: u8| q.safe.contains(b);
130
131 let mut i = 0;
133 while i < bytes.len() {
134 let b = bytes[i];
135 if is_safe(b) {
136 i += 1;
137 continue;
138 }
139 if requote
140 && let Some(v) = pct_at(bytes, i)
141 && !bytes[i + 1].is_ascii_lowercase()
142 && !bytes[i + 2].is_ascii_lowercase()
143 && (q.protected.contains(v) || !q.safe.contains(v))
144 {
145 i += 3;
146 continue;
147 }
148 break;
149 }
150 if i == bytes.len() {
151 return Cow::Borrowed(src);
152 }
153
154 let mut out = String::with_capacity(bytes.len() + 16);
155 out.push_str(&src[..i]);
156 while i < bytes.len() {
157 let b = bytes[i];
158 if is_safe(b) {
159 out.push(b as char);
160 i += 1;
161 } else if requote && let Some(v) = pct_at(bytes, i) {
162 if q.protected.contains(v) || !q.safe.contains(v) {
163 push_pct(&mut out, v);
164 } else {
165 out.push(v as char);
166 }
167 i += 3;
168 } else if q.qs && b == b' ' {
169 out.push('+');
170 i += 1;
171 } else {
172 push_pct(&mut out, b);
173 i += 1;
174 }
175 }
176 Cow::Owned(out)
177}
178
179pub(crate) fn unquote_with<'a>(src: &'a str, plus: bool, ignore: &Set) -> Cow<'a, str> {
181 let bytes = src.as_bytes();
182 let Some(start) = bytes
183 .iter()
184 .position(|b| *b == b'%' || (plus && *b == b'+'))
185 else {
186 return Cow::Borrowed(src);
187 };
188
189 let mut buf = Vec::with_capacity(bytes.len());
191 buf.extend_from_slice(&bytes[..start]);
192 let mut i = start;
193 while i < bytes.len() {
194 let b = bytes[i];
195 if let Some(v) = pct_at(bytes, i) {
196 if ignore.contains(v) {
197 buf.extend_from_slice(&[
198 b'%',
199 HEX_UPPER[usize::from(v >> 4)],
200 HEX_UPPER[usize::from(v & 15)],
201 ]);
202 } else {
203 buf.push(v);
204 }
205 i += 3;
206 } else {
207 buf.push(if plus && b == b'+' { b' ' } else { b });
208 i += 1;
209 }
210 }
211 if simdutf8::basic::from_utf8(&buf).is_ok() {
212 return Cow::Owned(unsafe { String::from_utf8_unchecked(buf) });
214 }
215 unquote_invalid(src, plus, ignore, start)
216}
217
218fn unquote_invalid<'a>(src: &'a str, plus: bool, ignore: &Set, start: usize) -> Cow<'a, str> {
220 let bytes = src.as_bytes();
221 let mut out = String::with_capacity(bytes.len());
222 out.push_str(&src[..start]);
223 let mut pending = Vec::new();
224 let mut i = start;
225 while i < bytes.len() {
226 let b = bytes[i];
227 if let Some(v) = pct_at(bytes, i) {
228 if ignore.contains(v) {
229 flush(&mut out, &mut pending);
230 push_pct(&mut out, v);
231 } else {
232 pending.push(v);
233 }
234 i += 3;
235 continue;
236 }
237 flush(&mut out, &mut pending);
238 if plus && b == b'+' {
239 out.push(' ');
240 i += 1;
241 } else {
242 let len = utf8_len(b);
244 out.push_str(&src[i..i + len]);
245 i += len;
246 }
247 }
248 flush(&mut out, &mut pending);
249 Cow::Owned(out)
250}
251
252const fn utf8_len(b: u8) -> usize {
253 match b {
254 0..0x80 => 1,
255 0xc0..0xe0 => 2,
256 0xe0..0xf0 => 3,
257 _ => 4,
258 }
259}
260
261fn flush(out: &mut String, pending: &mut Vec<u8>) {
263 let mut rest = &pending[..];
264 while !rest.is_empty() {
265 match simdutf8::compat::from_utf8(rest) {
266 Ok(s) => {
267 out.push_str(s);
268 break;
269 }
270 Err(e) => {
271 let valid = e.valid_up_to();
272 out.push_str(unsafe { std::str::from_utf8_unchecked(&rest[..valid]) });
274 let invalid = e.error_len().unwrap_or(rest.len() - valid);
275 for b in &rest[valid..valid + invalid] {
276 push_pct(out, *b);
277 }
278 rest = &rest[valid + invalid..];
279 }
280 }
281 }
282 pending.clear();
283}
284
285pub(crate) fn unquote_path_safe(src: &str) -> Cow<'_, str> {
287 unquote_with(src, false, &PATH_SAFE)
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn quote_rules() {
296 assert_eq!(quote("", Component::Path), "");
297 assert_eq!(quote("/a b/ü", Component::Path), "/a%20b/%C3%BC");
298 assert_eq!(quote("%41", Component::Path), "%2541");
299 assert_eq!(quote("u:p@x", Component::UserInfo), "u%3Ap%40x");
300 assert_eq!(
301 quote("a+b=c&d;e", Component::QueryPart),
302 "a%2Bb%3Dc%26d%3Be"
303 );
304 assert_eq!(quote("a=b&c d", Component::Query), "a=b&c+d");
305 assert_eq!(quote("x#y", Component::Fragment), "x%23y");
306 assert_eq!(
307 quote("a=b; c,\"%~", Component::Opaque),
308 "a%3Db%3B%20c%2C%22%25~"
309 );
310 assert!(matches!(
311 quote("a-b_c.1~", Component::Opaque),
312 Cow::Borrowed(_)
313 ));
314 }
315
316 #[test]
317 fn requote_rules() {
318 assert!(matches!(
319 requote("/a%2Fb%20c", Component::Path),
320 Cow::Borrowed(_)
321 ));
322 assert_eq!(requote("%7e%41%2f%2b", Component::Path), "~A%2F%2B");
323 assert_eq!(requote("100%", Component::Path), "100%25");
324 assert_eq!(requote("%zz%4", Component::Path), "%25zz%254");
325 assert_eq!(requote("a=%3d&b", Component::Query), "a=%3D&b");
326 assert_eq!(requote("a b", Component::Query), "a+b");
327 assert_eq!(requote("%C3%BC", Component::Fragment), "%C3%BC");
328 }
329
330 #[test]
331 fn unquote_rules() {
332 assert!(matches!(unquote("abc", Component::Path), Cow::Borrowed(_)));
333 assert_eq!(unquote("%C3%BC%20x", Component::Path), "ü x");
334 assert_eq!(unquote("a+b", Component::Path), "a+b");
335 assert_eq!(unquote("a+b%2B", Component::QueryPart), "a b+");
336 assert_eq!(unquote("a+b%26c%3D", Component::Query), "a b%26c%3D");
337 assert_eq!(unquote("%FF%C3%BCx%C3", Component::Path), "%FFüx%C3");
338 assert_eq!(unquote("100%", Component::Path), "100%");
339 assert_eq!(unquote_path_safe("a%2Fb%25c%20"), "a%2Fb%25c ");
340 assert_eq!(unquote("ü%20", Component::Fragment), "ü ");
341 }
342}