Skip to main content

bare_types/net/
domainname.rs

1//! Domain name type for DNS programming.
2//!
3//! This module provides a type-safe abstraction for DNS domain names,
4//! ensuring compliance with RFC 1035 domain name specifications.
5//!
6//! # RFC 1035 Domain Name Rules
7//!
8//! According to [RFC 1035 §2.3.4](https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4):
9//!
10//! - Total length: 1-253 characters
11//! - Each label (segment separated by dots): 1-63 characters
12//! - Valid characters: letters (a-z, A-Z), digits (0-9), and hyphens (-)
13//! - Labels cannot start or end with a hyphen
14//! - Domain names are case-insensitive (stored in lowercase internally)
15//! - Only ASCII characters are allowed
16//! - Labels CAN start with digits (unlike RFC 1123 hostnames)
17//!
18//! # Domain Name vs Hostname
19//!
20//! The key difference between `DomainName` and `Hostname`:
21//! - **`DomainName`** (RFC 1035): Labels can start with digits (e.g., "123.example.com")
22//! - **`Hostname`** (RFC 1123): Labels must start with letters (e.g., "www.example.com")
23//!
24//! # Examples
25//!
26//! ```rust
27//! use bare_types::net::DomainName;
28//!
29//! // Create a domain name
30//! let domain = DomainName::new("example.com")?;
31//!
32//! // Check depth (number of labels)
33//! assert_eq!(domain.depth(), 2);
34//!
35//! // Check if it's a subdomain
36//! let parent = DomainName::new("example.com")?;
37//! let child = DomainName::new("www.example.com")?;
38//! assert!(child.is_subdomain_of(&parent));
39//!
40//! // Get the string representation
41//! assert_eq!(domain.as_str(), "example.com");
42//!
43//! // Parse from string
44//! let domain: DomainName = "123.example.com".parse()?;
45//! # Ok::<(), bare_types::net::DomainNameError>(())
46//! ```
47
48use core::fmt;
49use core::str::FromStr;
50
51#[cfg(feature = "serde")]
52use serde::{Deserialize, Serialize};
53
54#[cfg(feature = "zeroize")]
55use zeroize::Zeroize;
56
57/// Error type for domain name validation.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
60#[non_exhaustive]
61pub enum DomainNameError {
62    /// Empty domain name
63    ///
64    /// The provided string is empty. Domain names must contain at least one character.
65    Empty,
66    /// Domain name exceeds maximum length of 253 characters
67    ///
68    /// According to RFC 1035, domain names must not exceed 253 characters.
69    /// This variant contains the actual length of the provided domain name.
70    TooLong(usize),
71    /// Label exceeds maximum length of 63 characters
72    ///
73    /// Each label (segment separated by dots) must not exceed 63 characters.
74    /// This variant contains the label index and its actual length.
75    LabelTooLong {
76        /// Label index
77        label: usize,
78        /// Label length
79        len: usize,
80    },
81    /// Label starts with invalid character
82    ///
83    /// Labels must start with an alphanumeric character (letter or digit).
84    /// Hyphens are not allowed as the first character.
85    /// This variant contains the invalid character.
86    InvalidLabelStart(char),
87    /// Label ends with invalid character
88    ///
89    /// Labels must end with an alphanumeric character (letter or digit).
90    /// Hyphens are not allowed as the last character.
91    /// This variant contains the invalid character.
92    InvalidLabelEnd(char),
93    /// Invalid character in domain name
94    ///
95    /// Domain names can only contain ASCII letters, digits, and hyphens.
96    /// This variant contains the invalid character.
97    InvalidChar(char),
98    /// Empty label (consecutive dots or leading/trailing dots)
99    ///
100    /// Consecutive dots (e.g., "example..com") or leading/trailing dots
101    /// (e.g., ".example.com" or "example.com.") are not allowed.
102    EmptyLabel,
103}
104
105impl fmt::Display for DomainNameError {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            Self::Empty => write!(f, "domain name cannot be empty"),
109            Self::TooLong(len) => write!(
110                f,
111                "domain name exceeds maximum length of 253 characters (got {len})"
112            ),
113            Self::LabelTooLong { label, len } => {
114                write!(
115                    f,
116                    "label {label} exceeds maximum length of 63 characters (got {len})"
117                )
118            }
119            Self::InvalidLabelStart(c) => write!(f, "label cannot start with '{c}'"),
120            Self::InvalidLabelEnd(c) => write!(f, "label cannot end with '{c}'"),
121            Self::InvalidChar(c) => write!(f, "invalid character '{c}' in domain name"),
122            Self::EmptyLabel => write!(f, "domain name cannot contain empty labels"),
123        }
124    }
125}
126
127#[cfg(feature = "std")]
128impl std::error::Error for DomainNameError {}
129
130/// A DNS domain name.
131///
132/// This type provides type-safe domain names with RFC 1035 validation.
133/// It uses the newtype pattern with `#[repr(transparent)]` for zero-cost abstraction.
134///
135/// # Invariants
136///
137/// - Total length is 1-253 characters
138/// - Each label is 1-63 characters
139/// - Only ASCII letters, digits, and hyphens are allowed
140/// - Labels cannot start or end with hyphens
141/// - Labels CAN start with digits (unlike RFC 1123 hostnames)
142/// - Stored in lowercase for case-insensitive comparison
143///
144/// # Examples
145///
146/// ```rust
147/// use bare_types::net::DomainName;
148///
149/// // Create a domain name
150/// let domain = DomainName::new("example.com")?;
151///
152/// // Access the string representation
153/// assert_eq!(domain.as_str(), "example.com");
154///
155/// // Check depth (number of labels)
156/// assert_eq!(domain.depth(), 2);
157///
158/// // Check if it's a subdomain
159/// let parent = DomainName::new("example.com")?;
160/// let child = DomainName::new("www.example.com")?;
161/// assert!(child.is_subdomain_of(&parent));
162///
163/// // Iterate over labels
164/// let labels: Vec<&str> = domain.labels().collect();
165/// assert_eq!(labels, vec!["example", "com"]);
166///
167/// // Parse from string
168/// let domain: DomainName = "123.example.com".parse()?;
169/// # Ok::<(), bare_types::net::DomainNameError>(())
170/// ```
171#[repr(transparent)]
172#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
173#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
174#[cfg_attr(feature = "zeroize", derive(Zeroize))]
175pub struct DomainName(heapless::String<253>);
176
177#[cfg(feature = "arbitrary")]
178impl<'a> arbitrary::Arbitrary<'a> for DomainName {
179    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
180        const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
181        const DIGITS: &[u8] = b"0123456789";
182
183        // Generate 1-4 labels
184        let label_count = 1 + (u8::arbitrary(u)? % 4);
185        let mut inner = heapless::String::<253>::new();
186
187        for label_idx in 0..label_count {
188            // Generate 1-20 character label
189            let label_len = 1 + (u8::arbitrary(u)? % 20).min(19);
190
191            // First character: alphanumeric (digits allowed for domain names)
192            let first_byte = u8::arbitrary(u)?;
193            let first = match first_byte % 2 {
194                0 => ALPHABET[((first_byte >> 1) % 26) as usize] as char,
195                _ => DIGITS[((first_byte >> 1) % 10) as usize] as char,
196            };
197            inner
198                .push(first)
199                .map_err(|_| arbitrary::Error::IncorrectFormat)?;
200
201            // Middle characters: alphanumeric or hyphen
202            for _ in 1..label_len.saturating_sub(1) {
203                let byte = u8::arbitrary(u)?;
204                let c = match byte % 4 {
205                    0 => ALPHABET[((byte >> 2) % 26) as usize] as char,
206                    1 => DIGITS[((byte >> 2) % 10) as usize] as char,
207                    _ => '-',
208                };
209                inner
210                    .push(c)
211                    .map_err(|_| arbitrary::Error::IncorrectFormat)?;
212            }
213
214            // Last character: alphanumeric (if label_len > 1)
215            if label_len > 1 {
216                let last_byte = u8::arbitrary(u)?;
217                let last = match last_byte % 2 {
218                    0 => ALPHABET[((last_byte >> 1) % 26) as usize] as char,
219                    _ => DIGITS[((last_byte >> 1) % 10) as usize] as char,
220                };
221                inner
222                    .push(last)
223                    .map_err(|_| arbitrary::Error::IncorrectFormat)?;
224            }
225
226            // Add dot between labels (but not after the last one)
227            if label_idx < label_count - 1 {
228                inner
229                    .push('.')
230                    .map_err(|_| arbitrary::Error::IncorrectFormat)?;
231            }
232        }
233
234        Ok(Self(inner))
235    }
236}
237
238impl DomainName {
239    /// Creates a new domain name from a string.
240    ///
241    /// # Errors
242    ///
243    /// Returns `DomainNameError` if the string does not comply with RFC 1035.
244    ///
245    /// # Examples
246    ///
247    /// ```rust
248    /// use bare_types::net::DomainName;
249    ///
250    /// let domain = DomainName::new("example.com")?;
251    /// assert_eq!(domain.as_str(), "example.com");
252    ///
253    /// // Labels can start with digits (RFC 1035)
254    /// let domain = DomainName::new("123.example.com")?;
255    /// assert_eq!(domain.as_str(), "123.example.com");
256    /// # Ok::<(), bare_types::net::DomainNameError>(())
257    /// ```
258    #[allow(clippy::missing_panics_doc)]
259    pub fn new(s: &str) -> Result<Self, DomainNameError> {
260        if s.is_empty() {
261            return Err(DomainNameError::Empty);
262        }
263
264        if s.len() > 253 {
265            return Err(DomainNameError::TooLong(s.len()));
266        }
267
268        let mut inner = heapless::String::<253>::new();
269        let mut label_index = 0;
270        let mut label_len = 0;
271        let mut first_char: Option<char> = None;
272        let mut last_char: char = '\0';
273
274        for c in s.chars() {
275            if c == '.' {
276                if label_len == 0 {
277                    return Err(DomainNameError::EmptyLabel);
278                }
279
280                if label_len > 63 {
281                    return Err(DomainNameError::LabelTooLong {
282                        label: label_index,
283                        len: label_len,
284                    });
285                }
286
287                let first = first_char.expect("label_len > 0 guarantees first_char is Some");
288                if !first.is_ascii_alphanumeric() {
289                    return Err(DomainNameError::InvalidLabelStart(first));
290                }
291
292                if !last_char.is_ascii_alphanumeric() {
293                    return Err(DomainNameError::InvalidLabelEnd(last_char));
294                }
295
296                inner.push('.').map_err(|_| DomainNameError::TooLong(253))?;
297                label_index += 1;
298                label_len = 0;
299                first_char = None;
300            } else {
301                if !c.is_ascii() {
302                    return Err(DomainNameError::InvalidChar(c));
303                }
304
305                if !c.is_ascii_alphanumeric() && c != '-' {
306                    return Err(DomainNameError::InvalidChar(c));
307                }
308
309                if label_len == 0 {
310                    first_char = Some(c);
311                }
312                last_char = c;
313                label_len += 1;
314
315                inner
316                    .push(c.to_ascii_lowercase())
317                    .map_err(|_| DomainNameError::TooLong(253))?;
318            }
319        }
320
321        if label_len == 0 {
322            return Err(DomainNameError::EmptyLabel);
323        }
324
325        if label_len > 63 {
326            return Err(DomainNameError::LabelTooLong {
327                label: label_index,
328                len: label_len,
329            });
330        }
331
332        let first = first_char.expect("label_len > 0 guarantees first_char is Some");
333        if !first.is_ascii_alphanumeric() {
334            return Err(DomainNameError::InvalidLabelStart(first));
335        }
336
337        if !last_char.is_ascii_alphanumeric() {
338            return Err(DomainNameError::InvalidLabelEnd(last_char));
339        }
340
341        Ok(Self(inner))
342    }
343
344    /// Returns the domain name as a string slice.
345    ///
346    /// # Examples
347    ///
348    /// ```rust
349    /// use bare_types::net::DomainName;
350    ///
351    /// let domain = DomainName::new("example.com").unwrap();
352    /// assert_eq!(domain.as_str(), "example.com");
353    /// ```
354    #[must_use]
355    #[inline]
356    pub fn as_str(&self) -> &str {
357        &self.0
358    }
359
360    /// Returns a reference to the underlying `heapless::String`.
361    ///
362    /// # Examples
363    ///
364    /// ```rust
365    /// use bare_types::net::DomainName;
366    ///
367    /// let domain = DomainName::new("example.com").unwrap();
368    /// let inner: &heapless::String<253> = domain.as_inner();
369    /// assert_eq!(inner.as_str(), "example.com");
370    /// ```
371    #[must_use]
372    #[inline]
373    pub const fn as_inner(&self) -> &heapless::String<253> {
374        &self.0
375    }
376
377    /// Consumes this domain name and returns the underlying string.
378    ///
379    /// # Examples
380    ///
381    /// ```rust
382    /// use bare_types::net::DomainName;
383    ///
384    /// let domain = DomainName::new("example.com").unwrap();
385    /// let inner = domain.into_inner();
386    /// assert_eq!(inner.as_str(), "example.com");
387    /// ```
388    #[must_use]
389    #[inline]
390    pub fn into_inner(self) -> heapless::String<253> {
391        self.0
392    }
393
394    /// Returns the depth (number of labels) of this domain name.
395    ///
396    /// # Examples
397    ///
398    /// ```rust
399    /// use bare_types::net::DomainName;
400    ///
401    /// let domain = DomainName::new("example.com").unwrap();
402    /// assert_eq!(domain.depth(), 2);
403    ///
404    /// let domain = DomainName::new("www.example.com").unwrap();
405    /// assert_eq!(domain.depth(), 3);
406    /// ```
407    #[must_use]
408    #[inline]
409    pub fn depth(&self) -> usize {
410        self.as_str().chars().filter(|&c| c == '.').count() + 1
411    }
412
413    /// Returns `true` if this domain name is a subdomain of `other`.
414    ///
415    /// A domain name is considered a subdomain if it has more labels
416    /// and ends with the parent domain name.
417    ///
418    /// # Examples
419    ///
420    /// ```rust
421    /// use bare_types::net::DomainName;
422    ///
423    /// let parent = DomainName::new("example.com").unwrap();
424    /// let child = DomainName::new("www.example.com").unwrap();
425    /// assert!(child.is_subdomain_of(&parent));
426    /// assert!(!parent.is_subdomain_of(&child));
427    /// assert!(!parent.is_subdomain_of(&parent));
428    /// ```
429    #[must_use]
430    #[inline]
431    pub fn is_subdomain_of(&self, other: &Self) -> bool {
432        if self.depth() <= other.depth() {
433            return false;
434        }
435
436        let self_str = self.as_str();
437        let other_str = other.as_str();
438
439        self_str.len() > other_str.len() + 1 && self_str.ends_with(&format!(".{other_str}"))
440    }
441
442    /// Returns `true` if this domain name is a top-level domain (TLD).
443    ///
444    /// A TLD is a domain name with only one label.
445    ///
446    /// # Examples
447    ///
448    /// ```rust
449    /// use bare_types::net::DomainName;
450    ///
451    /// let tld = DomainName::new("com").unwrap();
452    /// assert!(tld.is_tld());
453    ///
454    /// let domain = DomainName::new("example.com").unwrap();
455    /// assert!(!domain.is_tld());
456    /// ```
457    #[must_use]
458    #[inline]
459    pub fn is_tld(&self) -> bool {
460        self.depth() == 1
461    }
462
463    /// Returns an iterator over the labels in this domain name.
464    ///
465    /// # Examples
466    ///
467    /// ```rust
468    /// use bare_types::net::DomainName;
469    ///
470    /// let domain = DomainName::new("www.example.com").unwrap();
471    /// let labels: Vec<&str> = domain.labels().collect();
472    /// assert_eq!(labels, vec!["www", "example", "com"]);
473    /// ```
474    pub fn labels(&self) -> impl Iterator<Item = &str> {
475        self.as_str().split('.')
476    }
477}
478
479impl TryFrom<&str> for DomainName {
480    type Error = DomainNameError;
481
482    fn try_from(s: &str) -> Result<Self, Self::Error> {
483        Self::new(s)
484    }
485}
486
487impl From<DomainName> for heapless::String<253> {
488    fn from(domain: DomainName) -> Self {
489        domain.0
490    }
491}
492
493impl FromStr for DomainName {
494    type Err = DomainNameError;
495
496    fn from_str(s: &str) -> Result<Self, Self::Err> {
497        Self::new(s)
498    }
499}
500
501impl fmt::Display for DomainName {
502    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503        write!(f, "{}", self.0)
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510
511    #[test]
512    fn test_new_valid_domain_name() {
513        assert!(DomainName::new("example.com").is_ok());
514        assert!(DomainName::new("www.example.com").is_ok());
515        assert!(DomainName::new("a").is_ok());
516    }
517
518    #[test]
519    fn test_empty_domain_name() {
520        assert_eq!(DomainName::new(""), Err(DomainNameError::Empty));
521    }
522
523    #[test]
524    fn test_too_long_domain_name() {
525        let long = "a".repeat(254);
526        assert_eq!(DomainName::new(&long), Err(DomainNameError::TooLong(254)));
527    }
528
529    #[test]
530    fn test_label_too_long() {
531        let long_label = "a".repeat(64);
532        assert_eq!(
533            DomainName::new(&long_label),
534            Err(DomainNameError::LabelTooLong { label: 0, len: 64 })
535        );
536    }
537
538    #[test]
539    fn test_invalid_label_start() {
540        assert_eq!(
541            DomainName::new("-example.com"),
542            Err(DomainNameError::InvalidLabelStart('-'))
543        );
544    }
545
546    #[test]
547    fn test_invalid_label_end() {
548        assert_eq!(
549            DomainName::new("example-.com"),
550            Err(DomainNameError::InvalidLabelEnd('-'))
551        );
552    }
553
554    #[test]
555    fn test_invalid_char() {
556        assert_eq!(
557            DomainName::new("example_com"),
558            Err(DomainNameError::InvalidChar('_'))
559        );
560    }
561
562    #[test]
563    fn test_empty_label() {
564        assert_eq!(
565            DomainName::new("example..com"),
566            Err(DomainNameError::EmptyLabel)
567        );
568        assert_eq!(
569            DomainName::new(".example.com"),
570            Err(DomainNameError::EmptyLabel)
571        );
572        assert_eq!(
573            DomainName::new("example.com."),
574            Err(DomainNameError::EmptyLabel)
575        );
576    }
577
578    #[test]
579    fn test_as_str() {
580        let domain = DomainName::new("example.com").unwrap();
581        assert_eq!(domain.as_str(), "example.com");
582    }
583
584    #[test]
585    fn test_into_inner() {
586        let domain = DomainName::new("example.com").unwrap();
587        let inner = domain.into_inner();
588        assert_eq!(inner.as_str(), "example.com");
589    }
590
591    #[test]
592    fn test_depth() {
593        let domain = DomainName::new("com").unwrap();
594        assert_eq!(domain.depth(), 1);
595
596        let domain = DomainName::new("example.com").unwrap();
597        assert_eq!(domain.depth(), 2);
598
599        let domain = DomainName::new("www.example.com").unwrap();
600        assert_eq!(domain.depth(), 3);
601    }
602
603    #[test]
604    fn test_is_subdomain_of() {
605        let parent = DomainName::new("example.com").unwrap();
606        let child = DomainName::new("www.example.com").unwrap();
607        let grandchild = DomainName::new("sub.www.example.com").unwrap();
608
609        assert!(child.is_subdomain_of(&parent));
610        assert!(grandchild.is_subdomain_of(&parent));
611        assert!(grandchild.is_subdomain_of(&child));
612        assert!(!parent.is_subdomain_of(&child));
613        assert!(!parent.is_subdomain_of(&parent));
614        assert!(!child.is_subdomain_of(&child));
615    }
616
617    #[test]
618    fn test_is_tld() {
619        let tld = DomainName::new("com").unwrap();
620        assert!(tld.is_tld());
621
622        let domain = DomainName::new("example.com").unwrap();
623        assert!(!domain.is_tld());
624    }
625
626    #[test]
627    fn test_labels() {
628        let domain = DomainName::new("www.example.com").unwrap();
629        let labels: Vec<&str> = domain.labels().collect();
630        assert_eq!(labels, vec!["www", "example", "com"]);
631    }
632
633    #[test]
634    fn test_labels_single() {
635        let domain = DomainName::new("com").unwrap();
636        let labels: Vec<&str> = domain.labels().collect();
637        assert_eq!(labels, vec!["com"]);
638    }
639
640    #[test]
641    fn test_try_from_str() {
642        let domain = DomainName::try_from("example.com").unwrap();
643        assert_eq!(domain.as_str(), "example.com");
644    }
645
646    #[test]
647    fn test_from_domain_name_to_string() {
648        let domain = DomainName::new("example.com").unwrap();
649        let inner: heapless::String<253> = domain.into();
650        assert_eq!(inner.as_str(), "example.com");
651    }
652
653    #[test]
654    fn test_from_str() {
655        let domain: DomainName = "example.com".parse().unwrap();
656        assert_eq!(domain.as_str(), "example.com");
657    }
658
659    #[test]
660    fn test_from_str_invalid() {
661        assert!("".parse::<DomainName>().is_err());
662        assert!("-example.com".parse::<DomainName>().is_err());
663        assert!("example..com".parse::<DomainName>().is_err());
664    }
665
666    #[test]
667    fn test_display() {
668        let domain = DomainName::new("example.com").unwrap();
669        assert_eq!(format!("{domain}"), "example.com");
670    }
671
672    #[test]
673    fn test_equality() {
674        let domain1 = DomainName::new("example.com").unwrap();
675        let domain2 = DomainName::new("example.com").unwrap();
676        let domain3 = DomainName::new("www.example.com").unwrap();
677
678        assert_eq!(domain1, domain2);
679        assert_ne!(domain1, domain3);
680    }
681
682    #[test]
683    fn test_ordering() {
684        let domain1 = DomainName::new("a.example.com").unwrap();
685        let domain2 = DomainName::new("b.example.com").unwrap();
686
687        assert!(domain1 < domain2);
688    }
689
690    #[test]
691    fn test_clone() {
692        let domain = DomainName::new("example.com").unwrap();
693        let domain2 = domain.clone();
694        assert_eq!(domain, domain2);
695    }
696
697    #[test]
698    fn test_valid_characters() {
699        assert!(DomainName::new("a-b.example.com").is_ok());
700        assert!(DomainName::new("a1.example.com").is_ok());
701        assert!(DomainName::new("example-123.com").is_ok());
702    }
703
704    #[test]
705    fn test_maximum_length() {
706        let domain = format!(
707            "{}.{}.{}.{}",
708            "a".repeat(63),
709            "b".repeat(63),
710            "c".repeat(63),
711            "d".repeat(61)
712        );
713        assert_eq!(domain.len(), 253);
714        assert!(DomainName::new(&domain).is_ok());
715    }
716
717    #[test]
718    fn test_error_display() {
719        assert_eq!(
720            format!("{}", DomainNameError::Empty),
721            "domain name cannot be empty"
722        );
723        assert_eq!(
724            format!("{}", DomainNameError::TooLong(300)),
725            "domain name exceeds maximum length of 253 characters (got 300)"
726        );
727        assert_eq!(
728            format!("{}", DomainNameError::LabelTooLong { label: 0, len: 70 }),
729            "label 0 exceeds maximum length of 63 characters (got 70)"
730        );
731        assert_eq!(
732            format!("{}", DomainNameError::InvalidLabelStart('-')),
733            "label cannot start with '-'"
734        );
735        assert_eq!(
736            format!("{}", DomainNameError::InvalidLabelEnd('-')),
737            "label cannot end with '-'"
738        );
739        assert_eq!(
740            format!("{}", DomainNameError::InvalidChar('_')),
741            "invalid character '_' in domain name"
742        );
743        assert_eq!(
744            format!("{}", DomainNameError::EmptyLabel),
745            "domain name cannot contain empty labels"
746        );
747    }
748
749    #[test]
750    fn test_case_insensitive() {
751        let domain1 = DomainName::new("Example.COM").unwrap();
752        let domain2 = DomainName::new("example.com").unwrap();
753        assert_eq!(domain1, domain2);
754        assert_eq!(domain1.as_str(), "example.com");
755    }
756
757    #[test]
758    fn test_digit_start_labels() {
759        // RFC 1035 allows labels to start with digits
760        assert!(DomainName::new("123.example.com").is_ok());
761        assert!(DomainName::new("50-name.example.com").is_ok());
762        assert!(DomainName::new("235235").is_ok());
763        assert!(DomainName::new("0a.example.com").is_ok());
764        assert!(DomainName::new("9z.example.com").is_ok());
765    }
766
767    #[test]
768    fn test_digit_start_labels_valid() {
769        let domain = DomainName::new("123.example.com").unwrap();
770        assert_eq!(domain.as_str(), "123.example.com");
771        assert_eq!(domain.depth(), 3);
772
773        let labels: Vec<&str> = domain.labels().collect();
774        assert_eq!(labels, vec!["123", "example", "com"]);
775    }
776
777    #[test]
778    fn test_hyphen_not_at_boundaries() {
779        assert!(DomainName::new("a-b.example.com").is_ok());
780        assert!(DomainName::new("a-b-c.example.com").is_ok());
781        assert!(DomainName::new("example.a-b.com").is_ok());
782    }
783
784    #[test]
785    fn test_multiple_labels() {
786        let domain = DomainName::new("a.b.c.d.e.f.g").unwrap();
787        assert_eq!(domain.depth(), 7);
788
789        let labels: Vec<&str> = domain.labels().collect();
790        assert_eq!(labels, vec!["a", "b", "c", "d", "e", "f", "g"]);
791    }
792
793    #[test]
794    fn test_subdomain_edge_cases() {
795        let parent = DomainName::new("example.com").unwrap();
796        let child = DomainName::new("example.com").unwrap();
797
798        // Same domain is not a subdomain
799        assert!(!child.is_subdomain_of(&parent));
800
801        // TLD is not a subdomain of anything
802        let tld = DomainName::new("com").unwrap();
803        assert!(!tld.is_subdomain_of(&parent));
804    }
805
806    #[test]
807    fn test_single_label_domain() {
808        let domain = DomainName::new("localhost").unwrap();
809        assert_eq!(domain.depth(), 1);
810        assert!(domain.is_tld());
811
812        let labels: Vec<&str> = domain.labels().collect();
813        assert_eq!(labels, vec!["localhost"]);
814    }
815
816    #[test]
817    fn test_numeric_only_label() {
818        assert!(DomainName::new("123").is_ok());
819        assert!(DomainName::new("123.456").is_ok());
820        assert!(DomainName::new("123.456.789").is_ok());
821    }
822
823    #[test]
824    fn test_mixed_alphanumeric_labels() {
825        assert!(DomainName::new("a1b2c3.example.com").is_ok());
826        assert!(DomainName::new("123abc.example.com").is_ok());
827        assert!(DomainName::new("abc123.example.com").is_ok());
828    }
829
830    #[test]
831    fn test_maximum_label_length() {
832        let label = "a".repeat(63);
833        {
834            let domain = DomainName::new(&label).unwrap();
835            assert_eq!(domain.depth(), 1);
836        }
837
838        let domain = format!("{}.{}", "a".repeat(63), "b".repeat(63));
839        assert_eq!(domain.len(), 127);
840        assert!(DomainName::new(&domain).is_ok());
841    }
842
843    #[test]
844    fn test_maximum_total_length() {
845        let domain = format!(
846            "{}.{}.{}.{}",
847            "a".repeat(63),
848            "b".repeat(63),
849            "c".repeat(63),
850            "d".repeat(61)
851        );
852        assert_eq!(domain.len(), 253);
853
854        let domain = DomainName::new(&domain).unwrap();
855        assert_eq!(domain.depth(), 4);
856        assert_eq!(domain.as_str().len(), 253);
857    }
858
859    #[test]
860    fn test_maximum_total_length_plus_one() {
861        let domain = format!(
862            "{}.{}.{}.{}",
863            "a".repeat(63),
864            "b".repeat(63),
865            "c".repeat(63),
866            "d".repeat(62)
867        );
868        assert_eq!(domain.len(), 254);
869        assert_eq!(DomainName::new(&domain), Err(DomainNameError::TooLong(254)));
870    }
871
872    #[test]
873    fn test_unicode_rejected() {
874        assert!(DomainName::new("exämple.com").is_err());
875        assert!(DomainName::new("例え.com").is_err());
876        assert!(DomainName::new("例え.テスト").is_err());
877    }
878
879    #[test]
880    fn test_special_characters_rejected() {
881        assert!(DomainName::new("example_com").is_err());
882        assert!(DomainName::new("example.com/test").is_err());
883        assert!(DomainName::new("example.com?").is_err());
884        assert!(DomainName::new("example.com#").is_err());
885        assert!(DomainName::new("example.com@").is_err());
886        assert!(DomainName::new("example.com!").is_err());
887    }
888
889    #[test]
890    fn test_whitespace_rejected() {
891        assert!(DomainName::new("example .com").is_err());
892        assert!(DomainName::new("example. com").is_err());
893        assert!(DomainName::new("example . com").is_err());
894        assert!(DomainName::new("example\t.com").is_err());
895        assert!(DomainName::new("example\n.com").is_err());
896    }
897
898    #[test]
899    fn test_empty_labels_rejected() {
900        assert!(DomainName::new(".example.com").is_err());
901        assert!(DomainName::new("example..com").is_err());
902        assert!(DomainName::new("example.com.").is_err());
903        assert!(DomainName::new("..").is_err());
904        assert!(DomainName::new(".").is_err());
905    }
906
907    #[test]
908    fn test_hyphen_at_start_rejected() {
909        assert!(DomainName::new("-example.com").is_err());
910        assert!(DomainName::new("example.-com").is_err());
911        assert!(DomainName::new("-.example.com").is_err());
912    }
913
914    #[test]
915    fn test_hyphen_at_end_rejected() {
916        assert!(DomainName::new("example-.com").is_err());
917        assert!(DomainName::new("example.com-").is_err());
918        assert!(DomainName::new("example.-").is_err());
919    }
920
921    #[test]
922    fn test_consecutive_hyphens_allowed() {
923        assert!(DomainName::new("a--b.example.com").is_ok());
924        assert!(DomainName::new("a---b.example.com").is_ok());
925    }
926
927    #[test]
928    fn test_hash() {
929        use core::hash::Hash;
930        use core::hash::Hasher;
931
932        #[derive(Default)]
933        struct SimpleHasher(u64);
934
935        impl Hasher for SimpleHasher {
936            fn finish(&self) -> u64 {
937                self.0
938            }
939
940            fn write(&mut self, bytes: &[u8]) {
941                for byte in bytes {
942                    self.0 = self.0.wrapping_mul(31).wrapping_add(*byte as u64);
943                }
944            }
945        }
946
947        let domain1 = DomainName::new("example.com").unwrap();
948        let domain2 = DomainName::new("example.com").unwrap();
949        let domain3 = DomainName::new("www.example.com").unwrap();
950
951        let mut hasher1 = SimpleHasher::default();
952        let mut hasher2 = SimpleHasher::default();
953        let mut hasher3 = SimpleHasher::default();
954
955        domain1.hash(&mut hasher1);
956        domain2.hash(&mut hasher2);
957        domain3.hash(&mut hasher3);
958
959        assert_eq!(hasher1.finish(), hasher2.finish());
960        assert_ne!(hasher1.finish(), hasher3.finish());
961    }
962
963    #[test]
964    fn test_ordering_lexicographic() {
965        let domain1 = DomainName::new("a.example.com").unwrap();
966        let domain2 = DomainName::new("b.example.com").unwrap();
967        let domain3 = DomainName::new("a.example.com").unwrap();
968
969        assert!(domain1 < domain2);
970        assert!(domain2 > domain1);
971        assert_eq!(domain1, domain3);
972    }
973
974    #[test]
975    fn test_ordering_different_lengths() {
976        let domain1 = DomainName::new("a.com").unwrap();
977        let domain2 = DomainName::new("a.example.com").unwrap();
978
979        assert!(domain1 < domain2);
980    }
981
982    #[test]
983    fn test_debug() {
984        let domain = DomainName::new("example.com").unwrap();
985        assert_eq!(format!("{:?}", domain), "DomainName(\"example.com\")");
986    }
987
988    #[test]
989    fn test_as_inner() {
990        let domain = DomainName::new("example.com").unwrap();
991        let inner = domain.as_inner();
992        assert_eq!(inner.as_str(), "example.com");
993    }
994
995    #[test]
996    fn test_from_into_inner_roundtrip() {
997        let domain = DomainName::new("example.com").unwrap();
998        let inner: heapless::String<253> = domain.into();
999        let domain2 = DomainName::new(inner.as_str()).unwrap();
1000        assert_eq!(domain2.as_str(), "example.com");
1001    }
1002}