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}