Skip to main content

base_http/
target.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! Request target components. Schemes and HTTP hosts are case-insensitive, so
5//! schemes and authorities are stored in lowercase (RFC 3986, sections 3.1 and
6//! 6.2.2.1; RFC 9110, section 4.2.3); paths are preserved as supplied.
7//! Percent-encoded authority bytes are lowercased too, which is equivalent
8//! though RFC 3986 prefers uppercase hexadecimal digits. Drivers validate target
9//! combinations and wire syntax before transmission.
10use arrayvec::ArrayString;
11use std::{
12    borrow::{Borrow, Cow},
13    cmp::Ordering,
14    fmt,
15    hash::{Hash, Hasher},
16    ops::Deref,
17};
18
19/// A lowercase request scheme. `http` and `https` in any case use borrowed
20/// constants; other schemes are lowercased when converted. Comparisons with
21/// `str` use the lowercase text.
22#[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// Request authorities are almost always lowercase already, and are usually
64// shorter than a vector register, so check eight bytes at a time first.
65#[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    // Set the high bit of each byte in b'A'..=b'Z'. Clearing high bits first
76    // prevents carries between bytes; non-ASCII bytes are then excluded.
77    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    // The last word may overlap the previous one.
90    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// Short authorities and paths require no separate allocation, however they
104// were supplied.
105#[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    /// A lowercase request authority, such as `example.com:8080`. HTTP
168    /// authorities are a case-insensitive host and an optional port, so
169    /// conversions lowercase ASCII letters. Comparisons, hashing and map lookups
170    /// with `str` use the lowercase text.
171    Authority => lowercase,
172    /// A request path with optional query, or another supplied request-target
173    /// form. Paths are case-sensitive and preserved as supplied.
174    Path => preserve
175}
176
177// Each component behaves as its text: comparison, hashing and formatting do
178// not depend on storage.
179macro_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        // 12 two-byte characters fill the inline capacity exactly.
305        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}