better_url/better_url/host_details/
domain.rs

1//! Details of a domain host.
2
3use std::ops::Bound;
4use std::str::FromStr;
5
6#[cfg(feature = "serde")]
7use serde::{Serialize, Deserialize};
8use thiserror::Error;
9
10use crate::*;
11
12/// The details of a domain host.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
16pub struct DomainDetails {
17    /// The start of the domain middle.
18    pub middle_start: Option<usize>,
19    /// The start of the domain suffix.
20    pub suffix_start: Option<usize>,
21    /// The location of the [fully qualified domain name](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) period.
22    pub fqdn_period : Option<usize>
23}
24
25/// The enum of errors [`DomainDetails::parse`] can return.
26#[derive(Debug, Error)]
27pub enum GetDomainDetailsError {
28    /// Returned when a [`url::ParseError`] is encountered.
29    #[error(transparent)]
30    ParseError(#[from] url::ParseError),
31    /// Returned when the provided host isn't a domain.
32    #[error("The provided host wasn't a domain.")]
33    NotADomain
34}
35
36impl DomainDetails {
37    /// Checks if `domain` is a valid domain, then returns its details.
38    ///
39    /// If you're absolutely certain the value you're using is a valid domain, you can use [`Self::parse_unchecked`].
40    /// # Errors
41    /// If the call to [`url::Host::parse`] returns an error, that error is returned.
42    ///
43    /// If the call to [`url::Host::parse`] doesn't return [`url::Host::Domain`], returns the error [`GetDomainDetailsError::NotADomain`].
44    /// # Examples
45    /// ```
46    /// use better_url::*;
47    ///
48    /// assert_eq!(DomainDetails::parse(    "example.com"   ).unwrap(), DomainDetails {middle_start: Some(0), suffix_start: Some( 8), fqdn_period: None});
49    /// assert_eq!(DomainDetails::parse("www.example.com"   ).unwrap(), DomainDetails {middle_start: Some(4), suffix_start: Some(12), fqdn_period: None});
50    /// assert_eq!(DomainDetails::parse(    "example.co.uk" ).unwrap(), DomainDetails {middle_start: Some(0), suffix_start: Some( 8), fqdn_period: None});
51    /// assert_eq!(DomainDetails::parse("www.example.co.uk" ).unwrap(), DomainDetails {middle_start: Some(4), suffix_start: Some(12), fqdn_period: None});
52    /// assert_eq!(DomainDetails::parse(    "example.com."  ).unwrap(), DomainDetails {middle_start: Some(0), suffix_start: Some( 8), fqdn_period: Some(11)});
53    /// assert_eq!(DomainDetails::parse("www.example.com."  ).unwrap(), DomainDetails {middle_start: Some(4), suffix_start: Some(12), fqdn_period: Some(15)});
54    /// assert_eq!(DomainDetails::parse(    "example.co.uk.").unwrap(), DomainDetails {middle_start: Some(0), suffix_start: Some( 8), fqdn_period: Some(13)});
55    /// assert_eq!(DomainDetails::parse("www.example.co.uk.").unwrap(), DomainDetails {middle_start: Some(4), suffix_start: Some(12), fqdn_period: Some(17)});
56    ///
57    /// DomainDetails::parse("127.0.0.1").unwrap_err();
58    /// DomainDetails::parse("[::1]").unwrap_err();
59    /// ```
60    pub fn parse(domain: &str) -> Result<Self, GetDomainDetailsError> {
61        if !matches!(url::Host::parse(domain)?, url::Host::Domain(_)) {return Err(GetDomainDetailsError::NotADomain);}
62
63        Ok(Self::parse_unchecked(domain))
64    }
65
66    /// Gets the details of a domain without checking it's actually a domain first.
67    ///
68    /// If you are at all possibly not working with a domain (like an IP host), please use [`Self::parse`] instead.
69    /// # Examples
70    /// ```
71    /// use better_url::*;
72    ///
73    /// assert_eq!(DomainDetails::parse_unchecked(    "example.com"   ), DomainDetails {middle_start: Some(0), suffix_start: Some( 8), fqdn_period: None});
74    /// assert_eq!(DomainDetails::parse_unchecked("www.example.com"   ), DomainDetails {middle_start: Some(4), suffix_start: Some(12), fqdn_period: None});
75    /// assert_eq!(DomainDetails::parse_unchecked(    "example.co.uk" ), DomainDetails {middle_start: Some(0), suffix_start: Some( 8), fqdn_period: None});
76    /// assert_eq!(DomainDetails::parse_unchecked("www.example.co.uk" ), DomainDetails {middle_start: Some(4), suffix_start: Some(12), fqdn_period: None});
77    /// assert_eq!(DomainDetails::parse_unchecked(    "example.com."  ), DomainDetails {middle_start: Some(0), suffix_start: Some( 8), fqdn_period: Some(11)});
78    /// assert_eq!(DomainDetails::parse_unchecked("www.example.com."  ), DomainDetails {middle_start: Some(4), suffix_start: Some(12), fqdn_period: Some(15)});
79    /// assert_eq!(DomainDetails::parse_unchecked(    "example.co.uk."), DomainDetails {middle_start: Some(0), suffix_start: Some( 8), fqdn_period: Some(13)});
80    /// assert_eq!(DomainDetails::parse_unchecked("www.example.co.uk."), DomainDetails {middle_start: Some(4), suffix_start: Some(12), fqdn_period: Some(17)});
81    /// ```
82    #[allow(clippy::arithmetic_side_effects, reason = "Shouldn't be possible.")]
83    pub fn parse_unchecked(domain: &str) -> Self {
84        let suffix_start = psl::suffix(domain.as_bytes()).map(|suffix| (suffix.as_bytes().as_ptr() as usize) - (domain.as_ptr() as usize));
85        Self {
86            #[allow(clippy::indexing_slicing, reason = "Can't panic.")]
87            middle_start: suffix_start.and_then(|ss| domain.as_bytes()[..ss].rsplit(|x| *x==b'.').nth(1).map(|middle| (middle.as_ptr() as usize) - (domain.as_ptr() as usize))),
88            suffix_start,
89            fqdn_period : domain.strip_suffix('.').map(|x| x.len())
90        }
91    }
92
93    /// The location of the period between subdomain and domain middle.
94    /// # Examples
95    /// ```
96    /// use better_url::*;
97    ///
98    /// assert_eq!(DomainDetails::parse(    "example.com"   ).unwrap().subdomain_period(), None   );
99    /// assert_eq!(DomainDetails::parse("www.example.com"   ).unwrap().subdomain_period(), Some(3));
100    /// assert_eq!(DomainDetails::parse(    "example.co.uk" ).unwrap().subdomain_period(), None   );
101    /// assert_eq!(DomainDetails::parse("www.example.co.uk" ).unwrap().subdomain_period(), Some(3));
102    /// assert_eq!(DomainDetails::parse(    "example.com."  ).unwrap().subdomain_period(), None   );
103    /// assert_eq!(DomainDetails::parse("www.example.com."  ).unwrap().subdomain_period(), Some(3));
104    /// assert_eq!(DomainDetails::parse(    "example.co.uk.").unwrap().subdomain_period(), None   );
105    /// assert_eq!(DomainDetails::parse("www.example.co.uk.").unwrap().subdomain_period(), Some(3));
106    /// ```
107    pub fn subdomain_period(&self) -> Option<usize> {
108        self.middle_start.and_then(|x| x.checked_sub(1))
109    }
110    /// The location of the period between domain middle and domain suffix.
111    /// # Examples
112    /// ```
113    /// use better_url::*;
114    ///
115    /// assert_eq!(DomainDetails::parse(    "example.com"   ).unwrap().domain_suffix_period(), Some( 7));
116    /// assert_eq!(DomainDetails::parse("www.example.com"   ).unwrap().domain_suffix_period(), Some(11));
117    /// assert_eq!(DomainDetails::parse(    "example.co.uk" ).unwrap().domain_suffix_period(), Some( 7));
118    /// assert_eq!(DomainDetails::parse("www.example.co.uk" ).unwrap().domain_suffix_period(), Some(11));
119    /// assert_eq!(DomainDetails::parse(    "example.com."  ).unwrap().domain_suffix_period(), Some( 7));
120    /// assert_eq!(DomainDetails::parse("www.example.com."  ).unwrap().domain_suffix_period(), Some(11));
121    /// assert_eq!(DomainDetails::parse(    "example.co.uk.").unwrap().domain_suffix_period(), Some( 7));
122    /// assert_eq!(DomainDetails::parse("www.example.co.uk.").unwrap().domain_suffix_period(), Some(11));
123    /// ```
124    pub fn domain_suffix_period(&self) -> Option<usize> {
125        self.suffix_start.and_then(|x| x.checked_sub(1))
126    }
127
128    /// The bounds of domain.
129    ///
130    /// Notably does not include [`Self::fqdn_period`]
131    /// # Examples
132    /// ```
133    /// use better_url::*;
134    ///
135    /// let x =     "example.com"   ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_bounds()],     "example.com"  );
136    /// let x = "www.example.com"   ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_bounds()], "www.example.com"  );
137    /// let x =     "example.co.uk" ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_bounds()],     "example.co.uk");
138    /// let x = "www.example.co.uk" ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_bounds()], "www.example.co.uk");
139    /// let x =     "example.com."  ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_bounds()],     "example.com"  );
140    /// let x = "www.example.com."  ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_bounds()], "www.example.com"  );
141    /// let x =     "example.co.uk."; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_bounds()],     "example.co.uk");
142    /// let x = "www.example.co.uk."; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_bounds()], "www.example.co.uk");
143    /// ```
144    pub fn domain_bounds(&self) -> (Bound<usize>, Bound<usize>) {
145        (Bound::Unbounded, exorub(self.fqdn_period))
146    }
147    /// The bounds of subdomain.
148    /// # Examples
149    /// ```
150    /// use better_url::*;
151    ///
152    /// let x =     "example.com"   ; assert_eq!(   DomainDetails::parse(x).unwrap().subdomain_bounds()          , None );
153    /// let x = "www.example.com"   ; assert_eq!(&x[DomainDetails::parse(x).unwrap().subdomain_bounds().unwrap()], "www");
154    /// let x =     "example.co.uk" ; assert_eq!(   DomainDetails::parse(x).unwrap().subdomain_bounds()          , None );
155    /// let x = "www.example.co.uk" ; assert_eq!(&x[DomainDetails::parse(x).unwrap().subdomain_bounds().unwrap()], "www");
156    /// let x =     "example.com."  ; assert_eq!(   DomainDetails::parse(x).unwrap().subdomain_bounds()          , None );
157    /// let x = "www.example.com."  ; assert_eq!(&x[DomainDetails::parse(x).unwrap().subdomain_bounds().unwrap()], "www");
158    /// let x =     "example.co.uk."; assert_eq!(   DomainDetails::parse(x).unwrap().subdomain_bounds()          , None );
159    /// let x = "www.example.co.uk."; assert_eq!(&x[DomainDetails::parse(x).unwrap().subdomain_bounds().unwrap()], "www");
160    /// ```
161    pub fn subdomain_bounds(&self) -> Option<(Bound<usize>, Bound<usize>)> {
162        self.subdomain_period().map(|x| (Bound::Unbounded, Bound::Excluded(x)))
163    }
164    /// The bounds of not domain suffix.
165    /// # Examples
166    /// ```
167    /// use better_url::*;
168    ///
169    /// let x =     "example.com"   ; assert_eq!(&x[DomainDetails::parse(x).unwrap().not_domain_suffix_bounds().unwrap()],     "example");
170    /// let x = "www.example.com"   ; assert_eq!(&x[DomainDetails::parse(x).unwrap().not_domain_suffix_bounds().unwrap()], "www.example");
171    /// let x =     "example.co.uk" ; assert_eq!(&x[DomainDetails::parse(x).unwrap().not_domain_suffix_bounds().unwrap()],     "example");
172    /// let x = "www.example.co.uk" ; assert_eq!(&x[DomainDetails::parse(x).unwrap().not_domain_suffix_bounds().unwrap()], "www.example");
173    /// let x =     "example.com."  ; assert_eq!(&x[DomainDetails::parse(x).unwrap().not_domain_suffix_bounds().unwrap()],     "example");
174    /// let x = "www.example.com."  ; assert_eq!(&x[DomainDetails::parse(x).unwrap().not_domain_suffix_bounds().unwrap()], "www.example");
175    /// let x =     "example.co.uk."; assert_eq!(&x[DomainDetails::parse(x).unwrap().not_domain_suffix_bounds().unwrap()],     "example");
176    /// let x = "www.example.co.uk."; assert_eq!(&x[DomainDetails::parse(x).unwrap().not_domain_suffix_bounds().unwrap()], "www.example");
177    /// ```
178    pub fn not_domain_suffix_bounds(&self) -> Option<(Bound<usize>, Bound<usize>)> {
179        self.domain_suffix_period().map(|x| (Bound::Unbounded, Bound::Excluded(x)))
180    }
181    /// The bounds of domain middle.
182    /// # Examples
183    /// ```
184    /// use better_url::*;
185    ///
186    /// let x =     "example.com"   ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_middle_bounds().unwrap()], "example");
187    /// let x = "www.example.com"   ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_middle_bounds().unwrap()], "example");
188    /// let x =     "example.co.uk" ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_middle_bounds().unwrap()], "example");
189    /// let x = "www.example.co.uk" ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_middle_bounds().unwrap()], "example");
190    /// let x =     "example.com."  ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_middle_bounds().unwrap()], "example");
191    /// let x = "www.example.com."  ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_middle_bounds().unwrap()], "example");
192    /// let x =     "example.co.uk."; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_middle_bounds().unwrap()], "example");
193    /// let x = "www.example.co.uk."; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_middle_bounds().unwrap()], "example");
194    /// ```
195    pub fn domain_middle_bounds(&self) -> Option<(Bound<usize>, Bound<usize>)> {
196        self.middle_start.zip(self.domain_suffix_period()).map(|(ms, sp)| (Bound::Included(ms), Bound::Excluded(sp)))
197    }
198    /// The bounds of reg domain.
199    ///
200    /// Notably does not include [`Self::fqdn_period`]
201    /// # Examples
202    /// ```
203    /// use better_url::*;
204    ///
205    /// let x =     "example.com"   ; assert_eq!(&x[DomainDetails::parse(x).unwrap().reg_domain_bounds().unwrap()], "example.com"  );
206    /// let x = "www.example.com"   ; assert_eq!(&x[DomainDetails::parse(x).unwrap().reg_domain_bounds().unwrap()], "example.com"  );
207    /// let x =     "example.co.uk" ; assert_eq!(&x[DomainDetails::parse(x).unwrap().reg_domain_bounds().unwrap()], "example.co.uk");
208    /// let x = "www.example.co.uk" ; assert_eq!(&x[DomainDetails::parse(x).unwrap().reg_domain_bounds().unwrap()], "example.co.uk");
209    /// let x =     "example.com."  ; assert_eq!(&x[DomainDetails::parse(x).unwrap().reg_domain_bounds().unwrap()], "example.com"  );
210    /// let x = "www.example.com."  ; assert_eq!(&x[DomainDetails::parse(x).unwrap().reg_domain_bounds().unwrap()], "example.com"  );
211    /// let x =     "example.co.uk."; assert_eq!(&x[DomainDetails::parse(x).unwrap().reg_domain_bounds().unwrap()], "example.co.uk");
212    /// let x = "www.example.co.uk."; assert_eq!(&x[DomainDetails::parse(x).unwrap().reg_domain_bounds().unwrap()], "example.co.uk");
213    /// ```
214    pub fn reg_domain_bounds(&self) -> Option<(Bound<usize>, Bound<usize>)> {
215        self.middle_start.map(|x| (Bound::Included(x), exorub(self.fqdn_period)))
216    }
217    /// The bounds of domain suffix.
218    ///
219    /// Notably does not include [`Self::fqdn_period`]
220    /// # Examples
221    /// ```
222    /// use better_url::*;
223    ///
224    /// let x =     "example.com"   ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_suffix_bounds().unwrap()], "com"  );
225    /// let x = "www.example.com"   ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_suffix_bounds().unwrap()], "com"  );
226    /// let x =     "example.co.uk" ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_suffix_bounds().unwrap()], "co.uk");
227    /// let x = "www.example.co.uk" ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_suffix_bounds().unwrap()], "co.uk");
228    /// let x =     "example.com."  ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_suffix_bounds().unwrap()], "com"  );
229    /// let x = "www.example.com."  ; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_suffix_bounds().unwrap()], "com"  );
230    /// let x =     "example.co.uk."; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_suffix_bounds().unwrap()], "co.uk");
231    /// let x = "www.example.co.uk."; assert_eq!(&x[DomainDetails::parse(x).unwrap().domain_suffix_bounds().unwrap()], "co.uk");
232    /// ```
233    pub fn domain_suffix_bounds(&self) -> Option<(Bound<usize>, Bound<usize>)> {
234        self.suffix_start.map(|x| (Bound::Included(x), exorub(self.fqdn_period)))
235    }
236    /// If [`Self`] describes a [fully qualified domain name](https://en.wikipedia.org/wiki/Fully_qualified_domain_name), return [`true`].
237    /// # Examples
238    /// ```
239    /// use better_url::*;
240    ///
241    /// assert!(!DomainDetails::parse(    "example.com"   ).unwrap().is_fqdn());
242    /// assert!(!DomainDetails::parse("www.example.com"   ).unwrap().is_fqdn());
243    /// assert!(!DomainDetails::parse(    "example.co.uk" ).unwrap().is_fqdn());
244    /// assert!(!DomainDetails::parse("www.example.co.uk" ).unwrap().is_fqdn());
245    /// assert!( DomainDetails::parse(    "example.com."  ).unwrap().is_fqdn());
246    /// assert!( DomainDetails::parse("www.example.com."  ).unwrap().is_fqdn());
247    /// assert!( DomainDetails::parse(    "example.co.uk.").unwrap().is_fqdn());
248    /// assert!( DomainDetails::parse("www.example.co.uk.").unwrap().is_fqdn());
249    /// ```
250    pub fn is_fqdn(&self) -> bool {
251        self.fqdn_period.is_some()
252    }
253}
254
255impl FromStr for DomainDetails {
256    type Err = GetDomainDetailsError;
257
258    fn from_str(s: &str) -> Result<Self, Self::Err> {
259        Self::parse(s)
260    }
261}