Skip to main content

urly/
scheme.rs

1use std::hash::{Hash, Hasher};
2
3use crate::chars::SCHEME;
4use crate::error::{ErrorKind, InvalidUrl};
5
6/// URL scheme, e.g. `https`.
7///
8/// Comparison and hashing are ASCII case-insensitive. Schemes of a parsed
9/// [`Url`](crate::Url) are always lowercase.
10#[repr(transparent)]
11pub struct Scheme(str);
12
13str_type!(Scheme);
14
15impl Scheme {
16    /// `http`
17    pub const HTTP: &'static Scheme = Scheme::from_str_unchecked("http");
18    /// `https`
19    pub const HTTPS: &'static Scheme = Scheme::from_str_unchecked("https");
20    /// `ws`
21    pub const WS: &'static Scheme = Scheme::from_str_unchecked("ws");
22    /// `wss`
23    pub const WSS: &'static Scheme = Scheme::from_str_unchecked("wss");
24    /// `amqp`
25    pub const AMQP: &'static Scheme = Scheme::from_str_unchecked("amqp");
26    /// `amqps`
27    pub const AMQPS: &'static Scheme = Scheme::from_str_unchecked("amqps");
28    /// `mqtt`
29    pub const MQTT: &'static Scheme = Scheme::from_str_unchecked("mqtt");
30    /// `mqtts`
31    pub const MQTTS: &'static Scheme = Scheme::from_str_unchecked("mqtts");
32
33    /// Validates a scheme: `ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )`.
34    ///
35    /// ```
36    /// use urly::{ErrorKind, Scheme};
37    ///
38    /// assert_eq!(Scheme::new("HTTPS").unwrap(), Scheme::HTTPS);
39    /// assert_eq!(Scheme::new("1http").unwrap_err().kind(), ErrorKind::InvalidScheme);
40    /// ```
41    pub fn new(src: &str) -> Result<&Scheme, InvalidUrl> {
42        let bytes = src.as_bytes();
43        match bytes.first() {
44            None => Err(InvalidUrl::at(ErrorKind::Empty, 0)),
45            Some(b) if !b.is_ascii_alphabetic() => Err(InvalidUrl::at(ErrorKind::InvalidScheme, 0)),
46            _ => match bytes.iter().position(|b| !SCHEME.contains(*b)) {
47                Some(i) => Err(InvalidUrl::at(ErrorKind::InvalidScheme, i)),
48                None => Ok(Scheme::from_str_unchecked(src)),
49            },
50        }
51    }
52
53    /// Returns the default port of well-known schemes.
54    ///
55    /// ```
56    /// use urly::Scheme;
57    ///
58    /// assert_eq!(Scheme::HTTPS.default_port(), Some(443));
59    /// assert_eq!(Scheme::MQTTS.default_port(), Some(8883));
60    /// assert_eq!(Scheme::new("foo").unwrap().default_port(), None);
61    /// ```
62    pub fn default_port(&self) -> Option<u16> {
63        self.known().map(|(_, port, _)| port)
64    }
65
66    /// Returns `true` for WHATWG special schemes, whose empty path is
67    /// normalized to `/`.
68    pub(crate) fn is_special(&self) -> bool {
69        self.known().is_some_and(|(_, _, special)| special)
70    }
71
72    fn known(&self) -> Option<(&'static str, u16, bool)> {
73        const KNOWN: [(&str, u16, bool); 9] = [
74            ("http", 80, true),
75            ("https", 443, true),
76            ("ws", 80, true),
77            ("wss", 443, true),
78            ("ftp", 21, true),
79            ("amqp", 5672, false),
80            ("amqps", 5671, false),
81            ("mqtt", 1883, false),
82            ("mqtts", 8883, false),
83        ];
84        KNOWN
85            .into_iter()
86            .find(|(s, _, _)| s.eq_ignore_ascii_case(&self.0))
87    }
88}
89
90impl PartialEq for Scheme {
91    fn eq(&self, other: &Scheme) -> bool {
92        self.0.eq_ignore_ascii_case(&other.0)
93    }
94}
95
96impl Eq for Scheme {}
97
98impl PartialEq<str> for Scheme {
99    fn eq(&self, other: &str) -> bool {
100        self.0.eq_ignore_ascii_case(other)
101    }
102}
103
104impl PartialEq<Scheme> for str {
105    fn eq(&self, other: &Scheme) -> bool {
106        self.eq_ignore_ascii_case(&other.0)
107    }
108}
109
110impl Hash for Scheme {
111    fn hash<H: Hasher>(&self, state: &mut H) {
112        for b in self.0.bytes() {
113            state.write_u8(b.to_ascii_lowercase());
114        }
115        state.write_u8(0xff);
116    }
117}