1use arrayvec::ArrayString;
11use std::{
12 borrow::{Borrow, Cow},
13 cmp::Ordering,
14 fmt,
15 hash::{Hash, Hasher},
16 ops::Deref,
17};
18
19#[derive(Clone)]
23pub struct Scheme(Cow<'static, str>);
24
25impl Scheme {
26 pub const HTTP: Self = Self(Cow::Borrowed("http"));
27 pub const HTTPS: Self = Self(Cow::Borrowed("https"));
28
29 pub fn as_str(&self) -> &str {
30 &self.0
31 }
32 fn into_string(self) -> String {
33 self.0.into_owned()
34 }
35}
36
37impl Scheme {
38 fn known(value: &str) -> Option<Self> {
39 if value.eq_ignore_ascii_case("http") {
40 Some(Self::HTTP)
41 } else if value.eq_ignore_ascii_case("https") {
42 Some(Self::HTTPS)
43 } else {
44 None
45 }
46 }
47}
48
49impl From<&str> for Scheme {
50 fn from(value: &str) -> Self {
51 Self::known(value).unwrap_or_else(|| Self(Cow::Owned(value.to_ascii_lowercase())))
52 }
53}
54impl From<String> for Scheme {
55 fn from(mut value: String) -> Self {
56 Self::known(&value).unwrap_or_else(|| {
57 value.make_ascii_lowercase();
58 Self(Cow::Owned(value))
59 })
60 }
61}
62
63#[inline]
66fn lowercase(value: &mut str) {
67 if has_uppercase(value.as_bytes()) {
68 value.make_ascii_lowercase();
69 }
70}
71
72#[inline]
73fn has_uppercase(bytes: &[u8]) -> bool {
74 const ONES: u64 = u64::MAX / 255;
75 let word = |word: u64| {
78 let ascii = word & (0x7f * ONES);
79 (ascii + (0x80 - b'A') as u64 * ONES)
80 & !(ascii + (0x80 - b'Z' - 1) as u64 * ONES)
81 & !word
82 & (0x80 * ONES)
83 != 0
84 };
85 let load = |at: usize| u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap());
86 if bytes.len() < 8 {
87 return bytes.iter().any(u8::is_ascii_uppercase);
88 }
89 let mut at = 0;
91 while at + 8 < bytes.len() {
92 if word(load(at)) {
93 return true;
94 }
95 at += 8;
96 }
97 word(load(bytes.len() - 8))
98}
99
100#[inline]
101fn preserve(_: &mut str) {}
102
103#[derive(Clone)]
106enum Storage {
107 Inline(ArrayString<24>),
108 Heap(String),
109}
110
111macro_rules! stored {
112 ($($(#[$meta:meta])* $name:ident => $normalize:ident),*) => {$(
113 $(#[$meta])*
114 #[derive(Clone)]
115 pub struct $name(Storage);
116
117 impl $name {
118 pub fn as_str(&self) -> &str {
119 match &self.0 {
120 Storage::Inline(value) => value.as_str(),
121 Storage::Heap(value) => value,
122 }
123 }
124 fn into_string(self) -> String {
125 match self.0 {
126 Storage::Inline(value) => value.as_str().to_owned(),
127 Storage::Heap(value) => value,
128 }
129 }
130 }
131
132 impl From<&str> for $name {
133 #[inline]
134 fn from(value: &str) -> Self {
135 match ArrayString::from(value) {
136 Ok(mut value) => {
137 $normalize(&mut value);
138 Self(Storage::Inline(value))
139 }
140 Err(_) => {
141 let mut value = value.to_owned();
142 $normalize(&mut value);
143 Self(Storage::Heap(value))
144 }
145 }
146 }
147 }
148 impl From<String> for $name {
149 #[inline]
150 fn from(mut value: String) -> Self {
151 match ArrayString::from(&value) {
152 Ok(mut inline) => {
153 $normalize(&mut inline);
154 Self(Storage::Inline(inline))
155 }
156 Err(_) => {
157 $normalize(&mut value);
158 Self(Storage::Heap(value))
159 }
160 }
161 }
162 }
163 )*};
164}
165
166stored! {
167 Authority => lowercase,
172 Path => preserve
175}
176
177macro_rules! text {
180 ($($name:ident),*) => {$(
181 impl From<&String> for $name {
182 fn from(value: &String) -> Self {
183 Self::from(value.as_str())
184 }
185 }
186 impl From<Box<str>> for $name {
187 fn from(value: Box<str>) -> Self {
188 Self::from(String::from(value))
189 }
190 }
191 impl From<Cow<'_, str>> for $name {
192 fn from(value: Cow<'_, str>) -> Self {
193 match value {
194 Cow::Borrowed(value) => Self::from(value),
195 Cow::Owned(value) => Self::from(value),
196 }
197 }
198 }
199 impl From<$name> for String {
200 fn from(value: $name) -> Self {
201 value.into_string()
202 }
203 }
204 impl Deref for $name {
205 type Target = str;
206 fn deref(&self) -> &str {
207 self.as_str()
208 }
209 }
210 impl AsRef<str> for $name {
211 fn as_ref(&self) -> &str {
212 self.as_str()
213 }
214 }
215 impl Borrow<str> for $name {
216 fn borrow(&self) -> &str {
217 self.as_str()
218 }
219 }
220 impl fmt::Display for $name {
221 fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
222 output.write_str(self.as_str())
223 }
224 }
225 impl fmt::Debug for $name {
226 fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
227 output.debug_tuple(stringify!($name)).field(&self.as_str()).finish()
228 }
229 }
230 impl PartialEq for $name {
231 fn eq(&self, other: &Self) -> bool {
232 self.as_str() == other.as_str()
233 }
234 }
235 impl Eq for $name {}
236 impl Hash for $name {
237 fn hash<H: Hasher>(&self, state: &mut H) {
238 self.as_str().hash(state)
239 }
240 }
241 impl PartialOrd for $name {
242 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
243 Some(self.cmp(other))
244 }
245 }
246 impl Ord for $name {
247 fn cmp(&self, other: &Self) -> Ordering {
248 self.as_str().cmp(other.as_str())
249 }
250 }
251 text!(@eq $name, str);
252 text!(@eq $name, &str);
253 text!(@eq $name, String);
254 )*};
255 (@eq $name:ident, $other:ty) => {
256 impl PartialEq<$other> for $name {
257 fn eq(&self, other: &$other) -> bool {
258 self.as_str() == &other[..]
259 }
260 }
261 impl PartialEq<$name> for $other {
262 fn eq(&self, other: &$name) -> bool {
263 &self[..] == other.as_str()
264 }
265 }
266 };
267}
268
269text!(Scheme, Authority, Path);
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use std::collections::{BTreeSet, HashMap};
275
276 #[test]
277 fn schemes_use_constants() {
278 assert!(matches!(Scheme::from("http").0, Cow::Borrowed("http")));
279 assert!(matches!(
280 Scheme::from(String::from("https")).0,
281 Cow::Borrowed("https")
282 ));
283 assert!(matches!(Scheme::from("HTTP").0, Cow::Borrowed("http")));
284 assert!(matches!(
285 Scheme::from(String::from("HttpS")).0,
286 Cow::Borrowed("https")
287 ));
288 assert_eq!(Scheme::from("WS"), "ws");
289 assert_eq!(Scheme::from(String::from("Coap+TCP")), "coap+tcp");
290 assert_eq!(Scheme::from(&String::from("WSS")), "wss");
291 assert_eq!(Scheme::from(Cow::Borrowed("Https")), Scheme::HTTPS);
292 assert_eq!(Scheme::from(Box::<str>::from("FTP")), "ftp");
293 assert_ne!(Scheme::from("HTTPS"), "HTTPS");
294 assert_eq!(String::from(Scheme::HTTPS), "https");
295 }
296
297 #[test]
298 fn short_text_is_inline_however_supplied() {
299 let inline = |path: &Path| matches!(path.0, Storage::Inline(_));
300 assert!(inline(&Path::from("/")));
301 assert!(inline(&Path::from(String::from("/"))));
302 assert!(inline(&Path::from("x".repeat(24))));
303 assert!(!inline(&Path::from("x".repeat(25))));
304 assert!(inline(&Path::from("é".repeat(12).as_str())));
306 assert!(!inline(&Path::from("é".repeat(13).as_str())));
307 assert_eq!(String::from(Path::from("x".repeat(25))), "x".repeat(25));
308 }
309
310 #[test]
311 fn uppercase_detection_matches_bytes() {
312 for length in 0..=24 {
313 for at in 0..length {
314 for byte in 0..=255u8 {
315 let mut bytes = vec![b'a'; length];
316 bytes[at] = byte;
317 assert_eq!(
318 has_uppercase(&bytes),
319 byte.is_ascii_uppercase(),
320 "{length} {at} {byte}"
321 );
322 }
323 }
324 }
325 for neighbour in [0x00, 0x40, 0x5b, 0x7f, 0x80, 0xc0, 0xff] {
326 for byte in 0..=255u8 {
327 let bytes = [
328 neighbour, byte, neighbour, neighbour, neighbour, neighbour, neighbour,
329 neighbour,
330 ];
331 assert_eq!(
332 has_uppercase(&bytes),
333 bytes.iter().any(u8::is_ascii_uppercase),
334 "{neighbour} {byte}"
335 );
336 }
337 }
338 }
339
340 #[test]
341 fn authorities_are_lowercase_and_paths_are_preserved() {
342 let long = "Long.Example.COM:".to_owned() + &"8".repeat(10);
343 assert_eq!(Authority::from("Example.COM:80"), "example.com:80");
344 assert_eq!(
345 Authority::from(String::from("[::ABCD]:443")),
346 "[::abcd]:443"
347 );
348 assert_eq!(Authority::from(long.as_str()), long.to_ascii_lowercase());
349 assert_eq!(Authority::from(long.clone()), long.to_ascii_lowercase());
350 assert_eq!(Authority::from("É.example"), "É.example");
351 assert_eq!(Authority::from(Cow::Borrowed("A:1")), "a:1");
352 assert_eq!(Authority::from(Box::<str>::from("B:2")), "b:2");
353 assert_eq!(Path::from("/A?B=C"), "/A?B=C");
354 assert_eq!(Path::from("/A".repeat(20)), "/A".repeat(20));
355 }
356
357 #[test]
358 fn text_behaves_as_str() {
359 let long = "x".repeat(40);
360 let owned = String::from("/a");
361 assert_eq!(Path::from(&owned), Path::from(Cow::Borrowed("/a")));
362 assert_eq!(
363 Authority::from(long.clone()),
364 Authority::from(long.as_str())
365 );
366 assert_eq!(Path::from(Box::<str>::from("/a")), "/a");
367 let path = Path::from("/a");
368 assert!("/a" == path && owned == path && path == owned);
369 assert_eq!(format!("{:?}", Path::from("/a")), "Path(\"/a\")");
370 assert_eq!(format!("{:?}", Scheme::HTTP), "Scheme(\"http\")");
371 let sorted: BTreeSet<_> = ["/b", "/a"].into_iter().map(Path::from).collect();
372 assert_eq!(sorted.first().unwrap(), "/a");
373 let schemes = HashMap::from([(Scheme::HTTP, 80)]);
374 assert_eq!(schemes.get("http"), Some(&80));
375 }
376}