links_domainmap/
domain.rs

1//! Types for domain names, either as a reference identifier or a presented
2//! identifier, with support for internationalized domain names
3//!
4//! Rules for domain names as implemented here (based on a mix of [RFC 952],
5//! [RFC 1034], [RFC 1123], [RFC 2181], the [WHATWG URL specification][whatwg
6//! url], [browser][chrome] [implementations][firefox], and [browser
7//! bugs][bugzilla]) are:
8//! - A domain name has a maximum total length (including '.'s) of 253
9//!   characters/octets/bytes ([RFC 1034] section 3.1, [Unicode TR46] section 4)
10//! - Domain name labels (the things seperated by '.') have a maximum length of
11//!   63 characters/octets/bytes each, not including the separators ([RFC 1034]
12//!   section 3.5, [RFC 1123] section 2.1, [RFC 2181] section 11)
13//! - A domain name label must consist of only ASCII letters (`'a'..='z' |
14//!   'A'..='Z'`), digits (`'0'..='9'`), and hyphens (`'-'`). A label can not
15//!   start or end with a hyphen. ([RFC 952] section B, [RFC 1034] section 3.5,
16//!   [RFC 1123] section 2.1)
17//! - Additionally, a label can also contain underscores (`'_'`) in the same
18//!   places as letters for compatibility reasons. ([Additional discussion
19//!   around underscores in a Firefox bug][bugzilla], implementations in
20//!   [Chromium][chrome] and [Firefox][firefox])
21//! - A wildcard (`"*"`) can only comprise the entire left-most label of a
22//!   domain name and matches exactly one label. ([RFC 2818] section 3.1, [RFC
23//!   6125] section 6.4.3; i.e. `"*.example.com"` is valid and matches
24//!   `"foo.example.com"` and `"bar.example.com"` but does not match
25//!   `"foo.bar.example.com"` or `"example.com"`, and all of the following are
26//!   invalid: `"*.*.example.com"`, `"foo.*.example.com"`, `"fo*.example.com"`,
27//!   `"*oo.example.com"`, `"f*o.example.com"`)
28//! - No special treatment is given to wildcards on top-level domains (e.g.
29//!   `"*.com"`), or on other public suffixes (e.g. "`*.co.uk`" or
30//!   `"*.pvt.k12.ma.us"`), which allows some potentially invalid wildcard
31//!   domains; full wildcard domains (`"*"`) are not allowed
32//! - [Percent-encoded domain names][whatwg url] (e.g. `"e%78ample.com"`) are
33//!   not supported, and `'%'` is treated as an invalid character
34//!
35//! [RFC 952]: https://www.rfc-editor.org/rfc/rfc952
36//! [RFC 1034]: https://www.rfc-editor.org/rfc/rfc1034
37//! [RFC 1123]: https://www.rfc-editor.org/rfc/rfc1123
38//! [RFC 2181]: https://www.rfc-editor.org/rfc/rfc2181
39//! [RFC 5890]: https://www.rfc-editor.org/rfc/rfc5890
40//! [RFC 6125]: https://www.rfc-editor.org/rfc/rfc6125
41//! [Unicode TR46]: https://www.unicode.org/reports/tr46/tr46-29.html
42//! [bugzilla]: https://bugzilla.mozilla.org/show_bug.cgi?id=1136616
43//! [whatwg url]: https://url.spec.whatwg.org/#host-parsing
44//! [chrome]: https://github.com/chromium/chromium/blob/18095fefc0746e934e623019294b10844d8ec989/net/base/url_util.cc#L359-L377
45//! [firefox]: https://searchfox.org/mozilla-central/rev/23690c9281759b41eedf730d3dcb9ae04ccaddf8/security/nss/lib/mozpkix/lib/pkixnames.cpp#1979-1997
46//! [reference identifier]: https://www.rfc-editor.org/rfc/rfc6125#page-12
47//! [presented identifier]: https://www.rfc-editor.org/rfc/rfc6125#page-11
48
49use alloc::{string::String, vec::Vec};
50use core::{
51	cmp::Ordering,
52	error::Error,
53	fmt::{Debug, Display, Formatter, Result as FmtResult, Write},
54};
55
56/// A domain name label, stored in lowercase in its ASCII-encoded form
57#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
58pub struct Label(String);
59
60impl Label {
61	/// Create a new [`Label`] from the given [ACE] input, checking if all
62	/// characters in the label are valid, and that the length is appropriate.
63	/// This function does not do any conversions, it only checks the input for
64	/// ASCII validity. The validity of A-labels is not checked and fake
65	/// A-labels (labels starting with "xn--", while not being valid punycode)
66	/// are accepted.
67	///
68	/// # Errors
69	///
70	/// This function returns a [`ParseError`] if parsing of the label fails
71	///
72	/// [ACE]: https://datatracker.ietf.org/doc/html/rfc3490#section-2
73	pub(crate) fn new_ace(mut label: String) -> Result<Self, ParseError> {
74		if label.is_empty() {
75			return Err(ParseError::LabelEmpty);
76		}
77
78		if label.len() > 63 {
79			return Err(ParseError::LabelTooLong);
80		}
81
82		if let Some(invalid) = label
83			.chars()
84			.find(|&c| !(c.is_ascii_alphanumeric() || c == '-' || c == '_'))
85		{
86			return Err(ParseError::InvalidChar(invalid));
87		}
88
89		if label.starts_with('-') || label.ends_with('-') {
90			return Err(ParseError::InvalidHyphen);
91		}
92
93		label.make_ascii_lowercase();
94
95		Ok(Self(label))
96	}
97
98	/// Create a new [`Label`] from the given possibly-internationalized input
99	/// label, parsing and encoding the input into an A-label if necessary. If
100	/// the input contains a fake A-label, [`ParseError::Idna`] is returned.
101	///
102	/// # Errors
103	///
104	/// This function returns a [`ParseError`] if parsing of the label fails
105	pub(crate) fn new_idn(label: &str) -> Result<Self, ParseError> {
106		let label = idna::domain_to_ascii(label)?;
107		Self::new_ace(label)
108	}
109
110	/// Get the internal string representing this label
111	///
112	/// The returned value is an ASCII lowercase string, with non-ASCII
113	/// characters encoded using punycode. The string does not contain any `.`s.
114	/// It may however be a "fake A-label", i.e. start with "xn--", but
115	/// not be valid punycode.
116	///
117	/// # Example
118	///
119	/// ```rust
120	/// # use links_domainmap::{Domain, Label, ParseError};
121	/// # fn main() -> Result<(), ParseError> {
122	/// let domain = Domain::presented("παράδειγμα.EXAMPLE.com")?;
123	/// for label in domain.labels() {
124	/// 	assert!(label.as_str().is_ascii());
125	/// 	assert!(label
126	/// 		.as_str()
127	/// 		.chars()
128	/// 		.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'));
129	/// }
130	/// # Ok(())
131	/// # }
132	/// ```
133	#[must_use]
134	pub fn as_str(&self) -> &str {
135		self.as_ref()
136	}
137}
138
139impl Display for Label {
140	fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
141		if fmt.alternate() {
142			let (res, err) = idna::domain_to_unicode(self.as_str());
143
144			// Try encoding as Unicode, but use the original label if that fails
145			let res = if err.is_err() {
146				self.as_str()
147			} else {
148				res.as_str()
149			};
150
151			fmt.write_str(res)
152		} else {
153			fmt.write_str(self.as_str())
154		}
155	}
156}
157
158impl AsRef<str> for Label {
159	fn as_ref(&self) -> &str {
160		self.0.as_ref()
161	}
162}
163
164/// A domain name split into individual labels (not including the root label).
165///
166/// Labels are stored in most-significant-first order, i.e. `"www.example.com."`
167/// would be stored as `["com", "example", "www"]`. Labels are stored in their
168/// ASCII-encoded form (A-labels for internationalized domain name labels). If
169/// the left-most label is equal to `'*'`, `is_wildcard` is set to true, and the
170/// label itself is *not* stored in `labels`.
171///
172/// See the [library documentation][crate] for details about syntax rules for
173/// domain names.
174///
175/// # `Eq` vs `matches()`
176///
177/// The [`PartialEq`] / [`Eq`] implementation for [`Domain`] is equivalent to a
178/// string comparison of the domains (e.g. `"example.com" == "example.com"`, but
179/// `"*.example.com" != "www.example.com"`). [`Domain::matches`], on the other
180/// hand checks if a [reference identifier] domain matches a given [presented
181/// identifier] domain (e.g. `"example.com".matches("example.com")`, and
182/// `"*.example.com".matches("www.example.com")`). Care should be taken to use
183/// the appropriate method in each situation.
184///
185/// [presented identifier]: https://www.rfc-editor.org/rfc/rfc6125#page-11
186/// [reference identifier]: https://www.rfc-editor.org/rfc/rfc6125#page-12
187#[derive(Debug, Clone, PartialEq, Eq, Hash)]
188pub struct Domain {
189	/// Indicates whether the domain is a wildcard, i.e. that the left-most
190	/// label is exactly equal to `"*"`
191	is_wildcard: bool,
192	/// The labels of the domain, in right-to-left (most-significant-first)
193	/// order, not including the seperators or wildcard label (if any)
194	labels: Vec<Label>,
195}
196
197impl Domain {
198	/// Create a new `Domain` from a [reference identifier], without allowing
199	/// wildcards. Note that this function assumes the input is already
200	/// [ACE][IDNA]-encoded and it does not check the validity of A-labels,
201	/// allowing so-called "[fake A-labels][IDNA]" (labels starting with "xn--",
202	/// while not being valid punycode).
203	///
204	/// [presented identifier]: https://www.rfc-editor.org/rfc/rfc6125#page-11
205	/// [IDNA]: https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1
206	///
207	/// # Errors
208	///
209	/// Returns a [`ParseError`] if the parsing of the domain name fails. See
210	/// the documentation for the error type for an explanation of possible
211	/// error variants.
212	///
213	/// # Examples
214	///
215	/// ```rust
216	/// # use links_domainmap::{Domain, ParseError};
217	/// # fn main() -> Result<(), ParseError> {
218	/// let example = Domain::reference(&"www.example.com".to_string())?;
219	/// assert!(!example.is_wildcard());
220	/// assert_eq!(example.labels().len(), 3);
221	/// assert_eq!(example.labels()[0].as_ref(), "com");
222	/// assert_eq!(example.labels()[1].as_ref(), "example");
223	/// assert_eq!(example.labels()[2].as_ref(), "www");
224	///
225	/// let wildcard = Domain::reference(&"*.example.com".to_string());
226	/// assert!(wildcard.is_err());
227	/// assert!(matches!(wildcard, Err(ParseError::InvalidChar('*'))));
228	/// # Ok(())
229	/// # }
230	/// ```
231	///
232	/// ```rust
233	/// # use links_domainmap::{Domain, DomainMap};
234	/// # use core::error::Error;
235	/// #
236	/// # struct StubClientHello;
237	/// # impl StubClientHello {
238	/// # 	fn server_name(&self) -> Option<&str> {
239	/// # 		Some("example.com")
240	/// # 	}
241	/// # }
242	/// # fn get_default_cert() -> Option<()> {
243	/// # 	None
244	/// # }
245	/// # fn test() -> Option<()> {
246	/// # let client_hello = StubClientHello;
247	/// # let mut certificates = DomainMap::<()>::new();
248	/// # certificates.set(Domain::presented("example.com").unwrap(), ());
249	/// if let Some(server_name) = client_hello.server_name() {
250	/// 	let domain = Domain::reference(&server_name).ok()?;
251	/// 	let certificate = certificates.get(&domain)?;
252	/// 	Some(certificate.clone())
253	/// } else {
254	/// 	get_default_cert()
255	/// }
256	/// # }
257	/// # test().unwrap();
258	/// ```
259	pub fn reference(input: &str) -> Result<Self, ParseError> {
260		const SEPERATOR: char = '.';
261
262		let input = input.strip_suffix(SEPERATOR).unwrap_or(input);
263
264		if input.is_empty() {
265			return Err(ParseError::Empty);
266		}
267
268		if input.len() > 253 {
269			return Err(ParseError::TooLong);
270		}
271
272		let labels = input
273			.split(SEPERATOR)
274			.rev()
275			.map(|l| Label::new_ace(l.into()))
276			.collect::<Result<Vec<_>, _>>()?;
277
278		Ok(Self {
279			is_wildcard: false,
280			labels,
281		})
282	}
283
284	/// Create a new `Domain` from a [presented identifier], while also checking
285	/// for wildcards. This function accepts and encodes ASCII labels, A-labels,
286	/// or U-labels, or a mix of them. If the leftmost label is "*", then the
287	/// domain name is considered a wildcard domain, and `is_wildcard` is set to
288	/// true. Additionally, this function also accepts absolute domain names
289	/// (i.e. domain names ending with a '.'), which is [not allowed in
290	/// certificates][RFC 5280].
291	///
292	/// [presented identifier]: https://www.rfc-editor.org/rfc/rfc6125#page-11
293	/// [RFC 5280]: https://www.rfc-editor.org/rfc/rfc5280
294	///
295	/// # Errors
296	///
297	/// Returns a [`ParseError`] if the parsing of the domain name fails.
298	/// See the documentation for the error type for an explanation of possible
299	/// error variants.
300	///
301	/// # Examples
302	///
303	/// ```rust
304	/// # use links_domainmap::{Domain, ParseError};
305	/// # fn main() -> Result<(), ParseError> {
306	/// let example = Domain::presented(&"www.example.com".to_string())?;
307	/// assert!(!example.is_wildcard());
308	/// assert_eq!(example.labels().len(), 3);
309	/// assert_eq!(example.labels()[0].as_ref(), "com");
310	/// assert_eq!(example.labels()[1].as_ref(), "example");
311	/// assert_eq!(example.labels()[2].as_ref(), "www");
312	///
313	/// let idn = Domain::presented(&"παράδειγμα.例子.example.com".to_string())?;
314	/// assert!(!idn.is_wildcard());
315	/// assert_eq!(idn.labels().len(), 4);
316	/// assert_eq!(idn.labels()[0].as_ref(), "com");
317	/// assert_eq!(idn.labels()[1].as_ref(), "example");
318	/// assert_eq!(idn.labels()[2].as_ref(), "xn--fsqu00a");
319	/// assert_eq!(idn.labels()[3].as_ref(), "xn--hxajbheg2az3al");
320	///
321	/// let wildcard = Domain::presented(&"*.example.com".to_string())?;
322	/// assert!(wildcard.is_wildcard());
323	/// assert_eq!(wildcard.labels().len(), 2);
324	/// assert_eq!(wildcard.labels()[0].as_ref(), "com");
325	/// assert_eq!(wildcard.labels()[1].as_ref(), "example");
326	///
327	/// let wildcard_idn = Domain::presented(&"*.приклад.com".to_string())?;
328	/// assert!(wildcard_idn.is_wildcard());
329	/// assert_eq!(wildcard_idn.labels().len(), 2);
330	/// assert_eq!(wildcard_idn.labels()[0].as_ref(), "com");
331	/// assert_eq!(wildcard_idn.labels()[1].as_ref(), "xn--80aikifvh");
332	/// # Ok(())
333	/// # }
334	/// ```
335	///
336	/// ```rust
337	/// # use links_domainmap::{Domain, DomainMap, ParseError};
338	/// #
339	/// # struct StubConfigSource;
340	/// # impl StubConfigSource {
341	/// # 	fn get_config(&self) -> Vec<(String, ())> {
342	/// # 		vec![("example.com".to_string(), ()), ("przykład。com".to_string(), ())]
343	/// # 	}
344	/// # }
345	/// # fn main() -> Result<(), ParseError> {
346	/// # let tls_config = StubConfigSource;
347	/// # let mut certificates = DomainMap::<()>::new();
348	/// for (domain_name, certificate) in tls_config.get_config() {
349	/// 	let domain = Domain::presented(&domain_name)?;
350	/// 	let certificate = certificates.set(domain, certificate);
351	/// }
352	/// # Ok(())
353	/// # }
354	/// ```
355	pub fn presented(input: &str) -> Result<Self, ParseError> {
356		const SEPERATORS: &[char] = &['\u{002e}', '\u{3002}', '\u{ff0e}', '\u{ff61}'];
357
358		let input = input.strip_suffix(SEPERATORS).unwrap_or(input);
359
360		if input.is_empty() {
361			return Err(ParseError::Empty);
362		}
363
364		let mut labels = input.split(SEPERATORS).peekable();
365
366		let is_wildcard = *labels.peek().ok_or(ParseError::Empty)? == "*";
367
368		if is_wildcard {
369			// Skip the `"*"` label
370			labels.next();
371		}
372
373		let labels = labels
374			.rev()
375			.map(Label::new_idn)
376			.collect::<Result<Vec<_>, _>>()?;
377
378		if labels.is_empty() {
379			// Input was `"*"`, an invalid input considered empty by this crate
380			return Err(ParseError::Empty);
381		}
382
383		let wildcard_len = if is_wildcard { "*.".len() } else { 0 };
384		if labels.iter().map(|l| l.0.len() + 1).sum::<usize>() - 1 + wildcard_len > 253 {
385			return Err(ParseError::TooLong);
386		}
387
388		Ok(Self {
389			is_wildcard,
390			labels,
391		})
392	}
393
394	/// Whether this `Domain` represents a wildcard, i.e. the left-most label is
395	/// "*". If this is `true`, this domain matches another non-wildcard domain,
396	/// if this domain's labels are a prefix of the other domain's, and the
397	/// other domain has exactly one extra label, e.g. if this domain has the
398	/// labels `["com", "example"]`, then it would match another domain with
399	/// labels `["com", "example", "foo"]` or `["com", "example", "bar"]`, but
400	/// not `["com", "example"]` or `["com", "example", "bar", "foo"]`.
401	#[must_use]
402	pub const fn is_wildcard(&self) -> bool {
403		self.is_wildcard
404	}
405
406	/// Get the labels of this `Domain`. The labels are in right-to-left /
407	/// most-significant-first order, i.e. `"www.example.com"` would have the
408	/// labels `["com", "example", "www"]`. If this domain is a [wildcard
409	/// domain][`Domain::is_wildcard`], the wildcard label is not included in
410	/// the returned slice. See [`Domain`]'s documentation for details.
411	#[must_use]
412	pub fn labels(&self) -> &[Label] {
413		self.labels.as_slice()
414	}
415
416	/// Check whether this [`Domain`] matches the given [presented identifier].
417	/// This domain is treated as a [reference identifier], and therefore if its
418	/// `is_wildcard` property is set, this function returns `None`.
419	///
420	/// [presented identifier]: https://www.rfc-editor.org/rfc/rfc6125#page-11
421	/// [reference identifier]: https://www.rfc-editor.org/rfc/rfc6125#page-12
422	#[must_use]
423	pub fn matches(&self, presented: &Self) -> Option<bool> {
424		if self.is_wildcard() {
425			return None;
426		}
427
428		if presented.is_wildcard() {
429			Some(presented.labels() == &self.labels()[..self.labels().len() - 1])
430		} else {
431			Some(presented.labels() == self.labels())
432		}
433	}
434}
435
436/// Format a [`Domain`] with the given formatter. Use alternate formatting
437/// (`"{:#}"`) to encode labels into Unicode; by default internationalized
438/// labels are formatted in their ASCII compatible encoding form.
439impl Display for Domain {
440	fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
441		if self.is_wildcard() {
442			fmt.write_str("*.")?;
443		}
444
445		Display::fmt(
446			self.labels
447				.last()
448				.expect("a domain always has at least one label"),
449			fmt,
450		)?;
451
452		for label in self.labels.iter().rev().skip(1) {
453			fmt.write_char('.')?;
454			Display::fmt(label, fmt)?;
455		}
456
457		Ok(())
458	}
459}
460
461impl PartialOrd for Domain {
462	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
463		Some(self.cmp(other))
464	}
465}
466
467impl Ord for Domain {
468	fn cmp(&self, other: &Self) -> Ordering {
469		match self.labels.cmp(&other.labels) {
470			Ordering::Equal => match (self.is_wildcard, other.is_wildcard) {
471				(true, false) => Ordering::Greater,
472				(false, true) => Ordering::Less,
473				_ => Ordering::Equal,
474			},
475			ord => ord,
476		}
477	}
478}
479
480/// An error encountered while parsing a domain name
481#[derive(Debug)]
482pub enum ParseError {
483	/// The domain name has no non-wildcard labels
484	Empty,
485	/// The length of the domain exceeds 253
486	TooLong,
487	/// The label is empty
488	LabelEmpty,
489	/// The length of the label exceeds 63
490	LabelTooLong,
491	/// The input contains an invalid character
492	InvalidChar(char),
493	/// A label has a hyphen at the start or end
494	InvalidHyphen,
495	/// An error occurred during processing of an internationalized input
496	Idna(idna::Errors),
497}
498
499impl PartialEq for ParseError {
500	fn eq(&self, other: &Self) -> bool {
501		match (self, other) {
502			(Self::Empty, Self::Empty)
503			| (Self::TooLong, Self::TooLong)
504			| (Self::LabelEmpty, Self::LabelEmpty)
505			| (Self::LabelTooLong, Self::LabelTooLong)
506			| (Self::InvalidHyphen, Self::InvalidHyphen)
507			| (Self::Idna(_), Self::Idna(_)) => true,
508			(Self::InvalidChar(a), Self::InvalidChar(b)) => a == b,
509			_ => false,
510		}
511	}
512}
513
514impl Display for ParseError {
515	fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
516		match self {
517			Self::Empty => f.write_str("the domain name has no non-wildcard labels"),
518			Self::TooLong => f.write_str("the length of the domain exceeds 253"),
519			Self::LabelEmpty => f.write_str("the label is empty"),
520			Self::LabelTooLong => f.write_str("the length of the label exceeds 63"),
521			Self::InvalidChar(char) => f.write_fmt(format_args!(
522				"the input contains the invalid character '{char}'"
523			)),
524			Self::InvalidHyphen => f.write_str("a label has a hyphen at the start or end"),
525			Self::Idna(errors) => f.write_fmt(format_args!(
526				"an error occurred during processing of an internationalized input: {errors}"
527			)),
528		}
529	}
530}
531
532impl Error for ParseError {
533	fn source(&self) -> Option<&(dyn Error + 'static)> {
534		match self {
535			Self::Idna(errors) => Some(errors),
536			_ => None,
537		}
538	}
539}
540
541impl From<idna::Errors> for ParseError {
542	fn from(value: idna::Errors) -> Self {
543		Self::Idna(value)
544	}
545}
546
547#[cfg(test)]
548mod tests {
549	use alloc::{boxed::Box, collections::BTreeMap, format, string::ToString};
550	use core::error::Error;
551
552	use super::*;
553	use crate::tests::*;
554
555	#[test]
556	fn domain_reference() {
557		for (input, expected) in DOMAIN_REFERENCE {
558			let res = Domain::reference(input).map(|d| &*Box::leak(d.to_string().into_boxed_str()));
559
560			assert_eq!(res, *expected);
561		}
562	}
563
564	#[test]
565	fn domain_presented() {
566		for (input, expected) in DOMAIN_PRESENTED {
567			let res = Domain::presented(input).map(|d| &*Box::leak(d.to_string().into_boxed_str()));
568
569			assert_eq!(res, *expected);
570		}
571
572		assert!(Domain::presented("xn--example.com").is_err());
573
574		assert!(Domain::presented("xn--example.com")
575			.unwrap_err()
576			.source()
577			.unwrap()
578			.is::<idna::Errors>());
579	}
580
581	#[test]
582	fn domain_matches() {
583		for &(reference, presented, expected, _) in DOMAIN_MATCHES_EQ {
584			let reference = Domain::reference(reference).unwrap();
585			let presented = Domain::presented(presented).unwrap();
586			let res = reference.matches(&presented).unwrap();
587
588			assert_eq!(res, expected);
589		}
590
591		for &(a, b, expected) in DOMAIN_PRESENTED_MATCHES_PRESENTED {
592			let a = Domain::presented(a).unwrap();
593			let b = Domain::presented(b).unwrap();
594			let res = a.matches(&b);
595
596			assert_eq!(res, expected);
597		}
598
599		for &(a, b, expected) in DOMAIN_REFERENCE_MATCHES_REFERENCE {
600			let a = Domain::reference(a).unwrap();
601			let b = Domain::reference(b).unwrap();
602			let res = a.matches(&b);
603
604			assert_eq!(res, expected);
605		}
606	}
607
608	#[test]
609	fn domain_eq() {
610		for &(reference, presented, _, expected) in DOMAIN_MATCHES_EQ {
611			let reference = Domain::reference(reference).unwrap();
612			let presented = Domain::presented(presented).unwrap();
613			let res = reference == presented;
614
615			assert_eq!(res, expected);
616
617			assert_eq!(reference == presented, presented == reference);
618		}
619
620		for &(a, b, expected) in DOMAIN_PRESENTED_EQ_PRESENTED {
621			let a = Domain::presented(a).unwrap();
622			let b = Domain::presented(b).unwrap();
623
624			let res = a == b;
625			assert_eq!(res, expected);
626
627			let res = b == a;
628			assert_eq!(res, expected);
629		}
630
631		for &(a, b, expected) in DOMAIN_REFERENCE_EQ_REFERENCE {
632			let a = Domain::reference(a).unwrap();
633			let b = Domain::reference(b).unwrap();
634
635			let res = a == b;
636			assert_eq!(res, expected);
637
638			let res = b == a;
639			assert_eq!(res, expected);
640		}
641	}
642
643	#[test]
644	fn domain_display() {
645		for &(input, to_string, regular_format, alternate_format) in DOMAIN_DISPLAY {
646			if let Ok(presented) = Domain::presented(input) {
647				assert_eq!(presented.to_string(), to_string);
648				assert_eq!(format!("{presented}"), regular_format);
649				assert_eq!(format!("{presented:#}"), alternate_format);
650			};
651
652			if let Ok(reference) = Domain::reference(input) {
653				assert_eq!(reference.to_string(), to_string);
654				assert_eq!(format!("{reference}"), regular_format);
655				assert_eq!(format!("{reference:#}"), alternate_format);
656			};
657		}
658	}
659
660	#[test]
661	fn domain_misc_traits() {
662		let domain = Domain::presented("example.com").unwrap();
663		let wildcard = Domain::presented("*.example.com").unwrap();
664		let foo = Domain::presented("foo.example.com").unwrap();
665		let a = Domain::presented("foo.an-example.com").unwrap();
666
667		#[allow(clippy::redundant_clone)]
668		let cloned = domain.clone();
669		assert!(domain == cloned);
670
671		assert_eq!(format!("{domain:?}"), format!("{cloned:?}"));
672		assert!(format!("{domain:?}").contains("Domain"));
673
674		assert!(domain == domain);
675		assert!(wildcard == wildcard);
676		assert!(foo != Domain::presented("com.example.foo").unwrap());
677
678		assert!(domain < wildcard);
679		assert!(domain < foo);
680		assert!(wildcard < foo);
681		assert!(a < domain);
682		assert!(a < foo);
683		assert!(a < wildcard);
684
685		assert!(wildcard > domain);
686		assert!(foo > domain);
687		assert!(foo > wildcard);
688		assert!(domain > a);
689		assert!(foo > a);
690		assert!(wildcard > a);
691
692		for (a, b) in [&domain, &wildcard, &foo, &a]
693			.into_iter()
694			.zip([&domain, &wildcard, &foo, &a])
695		{
696			assert_eq!(a.partial_cmp(b), Some(a.cmp(b)));
697			assert_eq!(a.partial_cmp(b), b.partial_cmp(a).map(Ordering::reverse));
698		}
699
700		let mut btree_map = BTreeMap::<_, usize>::new();
701		btree_map.insert(domain.clone(), 3);
702		assert_eq!(btree_map.get(&domain), Some(&3));
703	}
704
705	#[test]
706	fn parseerror_error() {
707		assert!(Domain::presented("xn--example.com").is_err());
708		assert!(Domain::presented("xn--example.com")
709			.unwrap_err()
710			.source()
711			.unwrap()
712			.is::<idna::Errors>());
713
714		assert!(Domain::presented("example..com").is_err());
715		assert!(Domain::presented("example..com")
716			.unwrap_err()
717			.source()
718			.is_none());
719
720		assert_eq!(
721			Domain::presented("www.a$df.com").unwrap_err(),
722			Domain::presented("a$df.com").unwrap_err(),
723		);
724		assert_eq!(
725			Domain::presented("xn--example.com").unwrap_err(),
726			Domain::presented("foo.xn--example.com").unwrap_err()
727		);
728		assert_ne!(
729			Domain::presented("www.a$df.com").unwrap_err(),
730			Domain::presented("a#df.com").unwrap_err(),
731		);
732		assert_ne!(
733			Domain::presented("xn--example.com").unwrap_err(),
734			Domain::presented("foo.*.com").unwrap_err()
735		);
736	}
737
738	#[test]
739	fn parseerror_debug_display() {
740		format!("{:?}", ParseError::Empty).contains("Empty");
741		format!("{}", ParseError::Empty).contains("the domain name has no non-wildcard labels");
742
743		format!("{:?}", ParseError::TooLong).contains("TooLong");
744		format!("{}", ParseError::TooLong).contains("the length of the domain exceeds 253");
745
746		format!("{:?}", ParseError::LabelEmpty).contains("LabelEmpty");
747		format!("{}", ParseError::LabelEmpty).contains("the label is empty");
748
749		format!("{:?}", ParseError::LabelTooLong).contains("LabelTooLong");
750		format!("{}", ParseError::LabelTooLong).contains("the length of the label exceeds 63");
751
752		format!("{:?}", ParseError::InvalidChar(' ')).contains("InvalidChar");
753		format!("{}", ParseError::InvalidChar(' '))
754			.contains("the input contains the invalid character ' '");
755
756		format!("{:?}", ParseError::InvalidHyphen).contains("InvalidHyphen");
757		format!("{}", ParseError::InvalidHyphen)
758			.contains("a label has a hyphen at the start or end");
759
760		format!("{:?}", Domain::presented("xn--example").unwrap_err()).contains("Idna");
761		format!("{}", Domain::presented("xn--example").unwrap_err())
762			.contains("an error occurred during processing of an internationalized input: ");
763	}
764}