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}