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    pub fn is_subdomain_of(&self, other: &Self) -> bool {
431        if self.depth() <= other.depth() {
432            return false;
433        }
434
435        let self_str = self.as_str();
436        let other_str = other.as_str();
437
438        self_str.len() > other_str.len() + 1 && self_str.ends_with(&format!(".{other_str}"))
439    }
440
441    /// Returns `true` if this domain name is a top-level domain (TLD).
442    ///
443    /// A TLD is a domain name with only one label.
444    ///
445    /// # Examples
446    ///
447    /// ```rust
448    /// use bare_types::net::DomainName;
449    ///
450    /// let tld = DomainName::new("com").unwrap();
451    /// assert!(tld.is_tld());
452    ///
453    /// let domain = DomainName::new("example.com").unwrap();
454    /// assert!(!domain.is_tld());
455    /// ```
456    #[must_use]
457    #[inline]
458    pub fn is_tld(&self) -> bool {
459        self.depth() == 1
460    }
461
462    /// Returns an iterator over the labels in this domain name.
463    ///
464    /// # Examples
465    ///
466    /// ```rust
467    /// use bare_types::net::DomainName;
468    ///
469    /// let domain = DomainName::new("www.example.com").unwrap();
470    /// let labels: Vec<&str> = domain.labels().collect();
471    /// assert_eq!(labels, vec!["www", "example", "com"]);
472    /// ```
473    pub fn labels(&self) -> impl Iterator<Item = &str> {
474        self.as_str().split('.')
475    }
476}
477
478impl TryFrom<&str> for DomainName {
479    type Error = DomainNameError;
480
481    fn try_from(s: &str) -> Result<Self, Self::Error> {
482        Self::new(s)
483    }
484}
485
486impl From<DomainName> for heapless::String<253> {
487    fn from(domain: DomainName) -> Self {
488        domain.0
489    }
490}
491
492impl FromStr for DomainName {
493    type Err = DomainNameError;
494
495    fn from_str(s: &str) -> Result<Self, Self::Err> {
496        Self::new(s)
497    }
498}
499
500impl fmt::Display for DomainName {
501    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502        write!(f, "{}", self.0)
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn test_new_valid_domain_name() {
512        assert!(DomainName::new("example.com").is_ok());
513        assert!(DomainName::new("www.example.com").is_ok());
514        assert!(DomainName::new("a").is_ok());
515    }
516
517    #[test]
518    fn test_empty_domain_name() {
519        assert_eq!(DomainName::new(""), Err(DomainNameError::Empty));
520    }
521
522    #[test]
523    fn test_too_long_domain_name() {
524        let long = "a".repeat(254);
525        assert_eq!(DomainName::new(&long), Err(DomainNameError::TooLong(254)));
526    }
527
528    #[test]
529    fn test_label_too_long() {
530        let long_label = "a".repeat(64);
531        assert_eq!(
532            DomainName::new(&long_label),
533            Err(DomainNameError::LabelTooLong { label: 0, len: 64 })
534        );
535    }
536
537    #[test]
538    fn test_invalid_label_start() {
539        assert_eq!(
540            DomainName::new("-example.com"),
541            Err(DomainNameError::InvalidLabelStart('-'))
542        );
543    }
544
545    #[test]
546    fn test_invalid_label_end() {
547        assert_eq!(
548            DomainName::new("example-.com"),
549            Err(DomainNameError::InvalidLabelEnd('-'))
550        );
551    }
552
553    #[test]
554    fn test_invalid_char() {
555        assert_eq!(
556            DomainName::new("example_com"),
557            Err(DomainNameError::InvalidChar('_'))
558        );
559    }
560
561    #[test]
562    fn test_empty_label() {
563        assert_eq!(
564            DomainName::new("example..com"),
565            Err(DomainNameError::EmptyLabel)
566        );
567        assert_eq!(
568            DomainName::new(".example.com"),
569            Err(DomainNameError::EmptyLabel)
570        );
571        assert_eq!(
572            DomainName::new("example.com."),
573            Err(DomainNameError::EmptyLabel)
574        );
575    }
576
577    #[test]
578    fn test_as_str() {
579        let domain = DomainName::new("example.com").unwrap();
580        assert_eq!(domain.as_str(), "example.com");
581    }
582
583    #[test]
584    fn test_into_inner() {
585        let domain = DomainName::new("example.com").unwrap();
586        let inner = domain.into_inner();
587        assert_eq!(inner.as_str(), "example.com");
588    }
589
590    #[test]
591    fn test_depth() {
592        let domain = DomainName::new("com").unwrap();
593        assert_eq!(domain.depth(), 1);
594
595        let domain = DomainName::new("example.com").unwrap();
596        assert_eq!(domain.depth(), 2);
597
598        let domain = DomainName::new("www.example.com").unwrap();
599        assert_eq!(domain.depth(), 3);
600    }
601
602    #[test]
603    fn test_is_subdomain_of() {
604        let parent = DomainName::new("example.com").unwrap();
605        let child = DomainName::new("www.example.com").unwrap();
606        let grandchild = DomainName::new("sub.www.example.com").unwrap();
607
608        assert!(child.is_subdomain_of(&parent));
609        assert!(grandchild.is_subdomain_of(&parent));
610        assert!(grandchild.is_subdomain_of(&child));
611        assert!(!parent.is_subdomain_of(&child));
612        assert!(!parent.is_subdomain_of(&parent));
613        assert!(!child.is_subdomain_of(&child));
614    }
615
616    #[test]
617    fn test_is_tld() {
618        let tld = DomainName::new("com").unwrap();
619        assert!(tld.is_tld());
620
621        let domain = DomainName::new("example.com").unwrap();
622        assert!(!domain.is_tld());
623    }
624
625    #[test]
626    fn test_labels() {
627        let domain = DomainName::new("www.example.com").unwrap();
628        let labels: Vec<&str> = domain.labels().collect();
629        assert_eq!(labels, vec!["www", "example", "com"]);
630    }
631
632    #[test]
633    fn test_labels_single() {
634        let domain = DomainName::new("com").unwrap();
635        let labels: Vec<&str> = domain.labels().collect();
636        assert_eq!(labels, vec!["com"]);
637    }
638
639    #[test]
640    fn test_try_from_str() {
641        let domain = DomainName::try_from("example.com").unwrap();
642        assert_eq!(domain.as_str(), "example.com");
643    }
644
645    #[test]
646    fn test_from_domain_name_to_string() {
647        let domain = DomainName::new("example.com").unwrap();
648        let inner: heapless::String<253> = domain.into();
649        assert_eq!(inner.as_str(), "example.com");
650    }
651
652    #[test]
653    fn test_from_str() {
654        let domain: DomainName = "example.com".parse().unwrap();
655        assert_eq!(domain.as_str(), "example.com");
656    }
657
658    #[test]
659    fn test_from_str_invalid() {
660        assert!("".parse::<DomainName>().is_err());
661        assert!("-example.com".parse::<DomainName>().is_err());
662        assert!("example..com".parse::<DomainName>().is_err());
663    }
664
665    #[test]
666    fn test_display() {
667        let domain = DomainName::new("example.com").unwrap();
668        assert_eq!(format!("{domain}"), "example.com");
669    }
670
671    #[test]
672    fn test_equality() {
673        let domain1 = DomainName::new("example.com").unwrap();
674        let domain2 = DomainName::new("example.com").unwrap();
675        let domain3 = DomainName::new("www.example.com").unwrap();
676
677        assert_eq!(domain1, domain2);
678        assert_ne!(domain1, domain3);
679    }
680
681    #[test]
682    fn test_ordering() {
683        let domain1 = DomainName::new("a.example.com").unwrap();
684        let domain2 = DomainName::new("b.example.com").unwrap();
685
686        assert!(domain1 < domain2);
687    }
688
689    #[test]
690    fn test_clone() {
691        let domain = DomainName::new("example.com").unwrap();
692        let domain2 = domain.clone();
693        assert_eq!(domain, domain2);
694    }
695
696    #[test]
697    fn test_valid_characters() {
698        assert!(DomainName::new("a-b.example.com").is_ok());
699        assert!(DomainName::new("a1.example.com").is_ok());
700        assert!(DomainName::new("example-123.com").is_ok());
701    }
702
703    #[test]
704    fn test_maximum_length() {
705        let domain = format!(
706            "{}.{}.{}.{}",
707            "a".repeat(63),
708            "b".repeat(63),
709            "c".repeat(63),
710            "d".repeat(61)
711        );
712        assert_eq!(domain.len(), 253);
713        assert!(DomainName::new(&domain).is_ok());
714    }
715
716    #[test]
717    fn test_error_display() {
718        assert_eq!(
719            format!("{}", DomainNameError::Empty),
720            "domain name cannot be empty"
721        );
722        assert_eq!(
723            format!("{}", DomainNameError::TooLong(300)),
724            "domain name exceeds maximum length of 253 characters (got 300)"
725        );
726        assert_eq!(
727            format!("{}", DomainNameError::LabelTooLong { label: 0, len: 70 }),
728            "label 0 exceeds maximum length of 63 characters (got 70)"
729        );
730        assert_eq!(
731            format!("{}", DomainNameError::InvalidLabelStart('-')),
732            "label cannot start with '-'"
733        );
734        assert_eq!(
735            format!("{}", DomainNameError::InvalidLabelEnd('-')),
736            "label cannot end with '-'"
737        );
738        assert_eq!(
739            format!("{}", DomainNameError::InvalidChar('_')),
740            "invalid character '_' in domain name"
741        );
742        assert_eq!(
743            format!("{}", DomainNameError::EmptyLabel),
744            "domain name cannot contain empty labels"
745        );
746    }
747
748    #[test]
749    fn test_case_insensitive() {
750        let domain1 = DomainName::new("Example.COM").unwrap();
751        let domain2 = DomainName::new("example.com").unwrap();
752        assert_eq!(domain1, domain2);
753        assert_eq!(domain1.as_str(), "example.com");
754    }
755
756    #[test]
757    fn test_digit_start_labels() {
758        // RFC 1035 allows labels to start with digits
759        assert!(DomainName::new("123.example.com").is_ok());
760        assert!(DomainName::new("50-name.example.com").is_ok());
761        assert!(DomainName::new("235235").is_ok());
762        assert!(DomainName::new("0a.example.com").is_ok());
763        assert!(DomainName::new("9z.example.com").is_ok());
764    }
765
766    #[test]
767    fn test_digit_start_labels_valid() {
768        let domain = DomainName::new("123.example.com").unwrap();
769        assert_eq!(domain.as_str(), "123.example.com");
770        assert_eq!(domain.depth(), 3);
771
772        let labels: Vec<&str> = domain.labels().collect();
773        assert_eq!(labels, vec!["123", "example", "com"]);
774    }
775
776    #[test]
777    fn test_hyphen_not_at_boundaries() {
778        assert!(DomainName::new("a-b.example.com").is_ok());
779        assert!(DomainName::new("a-b-c.example.com").is_ok());
780        assert!(DomainName::new("example.a-b.com").is_ok());
781    }
782
783    #[test]
784    fn test_multiple_labels() {
785        let domain = DomainName::new("a.b.c.d.e.f.g").unwrap();
786        assert_eq!(domain.depth(), 7);
787
788        let labels: Vec<&str> = domain.labels().collect();
789        assert_eq!(labels, vec!["a", "b", "c", "d", "e", "f", "g"]);
790    }
791
792    #[test]
793    fn test_subdomain_edge_cases() {
794        let parent = DomainName::new("example.com").unwrap();
795        let child = DomainName::new("example.com").unwrap();
796
797        // Same domain is not a subdomain
798        assert!(!child.is_subdomain_of(&parent));
799
800        // TLD is not a subdomain of anything
801        let tld = DomainName::new("com").unwrap();
802        assert!(!tld.is_subdomain_of(&parent));
803    }
804
805    #[test]
806    fn test_single_label_domain() {
807        let domain = DomainName::new("localhost").unwrap();
808        assert_eq!(domain.depth(), 1);
809        assert!(domain.is_tld());
810
811        let labels: Vec<&str> = domain.labels().collect();
812        assert_eq!(labels, vec!["localhost"]);
813    }
814
815    #[test]
816    fn test_numeric_only_label() {
817        assert!(DomainName::new("123").is_ok());
818        assert!(DomainName::new("123.456").is_ok());
819        assert!(DomainName::new("123.456.789").is_ok());
820    }
821
822    #[test]
823    fn test_mixed_alphanumeric_labels() {
824        assert!(DomainName::new("a1b2c3.example.com").is_ok());
825        assert!(DomainName::new("123abc.example.com").is_ok());
826        assert!(DomainName::new("abc123.example.com").is_ok());
827    }
828
829    #[test]
830    fn test_maximum_label_length() {
831        let label = "a".repeat(63);
832        {
833            let domain = DomainName::new(&label).unwrap();
834            assert_eq!(domain.depth(), 1);
835        }
836
837        let domain = format!("{}.{}", "a".repeat(63), "b".repeat(63));
838        assert_eq!(domain.len(), 127);
839        assert!(DomainName::new(&domain).is_ok());
840    }
841
842    #[test]
843    fn test_maximum_total_length() {
844        let domain = format!(
845            "{}.{}.{}.{}",
846            "a".repeat(63),
847            "b".repeat(63),
848            "c".repeat(63),
849            "d".repeat(61)
850        );
851        assert_eq!(domain.len(), 253);
852
853        let domain = DomainName::new(&domain).unwrap();
854        assert_eq!(domain.depth(), 4);
855        assert_eq!(domain.as_str().len(), 253);
856    }
857
858    #[test]
859    fn test_maximum_total_length_plus_one() {
860        let domain = format!(
861            "{}.{}.{}.{}",
862            "a".repeat(63),
863            "b".repeat(63),
864            "c".repeat(63),
865            "d".repeat(62)
866        );
867        assert_eq!(domain.len(), 254);
868        assert_eq!(DomainName::new(&domain), Err(DomainNameError::TooLong(254)));
869    }
870
871    #[test]
872    fn test_unicode_rejected() {
873        assert!(DomainName::new("exämple.com").is_err());
874        assert!(DomainName::new("例え.com").is_err());
875        assert!(DomainName::new("例え.テスト").is_err());
876    }
877
878    #[test]
879    fn test_special_characters_rejected() {
880        assert!(DomainName::new("example_com").is_err());
881        assert!(DomainName::new("example.com/test").is_err());
882        assert!(DomainName::new("example.com?").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    }
887
888    #[test]
889    fn test_whitespace_rejected() {
890        assert!(DomainName::new("example .com").is_err());
891        assert!(DomainName::new("example. com").is_err());
892        assert!(DomainName::new("example . com").is_err());
893        assert!(DomainName::new("example\t.com").is_err());
894        assert!(DomainName::new("example\n.com").is_err());
895    }
896
897    #[test]
898    fn test_empty_labels_rejected() {
899        assert!(DomainName::new(".example.com").is_err());
900        assert!(DomainName::new("example..com").is_err());
901        assert!(DomainName::new("example.com.").is_err());
902        assert!(DomainName::new("..").is_err());
903        assert!(DomainName::new(".").is_err());
904    }
905
906    #[test]
907    fn test_hyphen_at_start_rejected() {
908        assert!(DomainName::new("-example.com").is_err());
909        assert!(DomainName::new("example.-com").is_err());
910        assert!(DomainName::new("-.example.com").is_err());
911    }
912
913    #[test]
914    fn test_hyphen_at_end_rejected() {
915        assert!(DomainName::new("example-.com").is_err());
916        assert!(DomainName::new("example.com-").is_err());
917        assert!(DomainName::new("example.-").is_err());
918    }
919
920    #[test]
921    fn test_consecutive_hyphens_allowed() {
922        assert!(DomainName::new("a--b.example.com").is_ok());
923        assert!(DomainName::new("a---b.example.com").is_ok());
924    }
925
926    #[test]
927    fn test_hash() {
928        use core::hash::Hash;
929        use core::hash::Hasher;
930
931        #[derive(Default)]
932        struct SimpleHasher(u64);
933
934        impl Hasher for SimpleHasher {
935            fn finish(&self) -> u64 {
936                self.0
937            }
938
939            fn write(&mut self, bytes: &[u8]) {
940                for byte in bytes {
941                    self.0 = self.0.wrapping_mul(31).wrapping_add(*byte as u64);
942                }
943            }
944        }
945
946        let domain1 = DomainName::new("example.com").unwrap();
947        let domain2 = DomainName::new("example.com").unwrap();
948        let domain3 = DomainName::new("www.example.com").unwrap();
949
950        let mut hasher1 = SimpleHasher::default();
951        let mut hasher2 = SimpleHasher::default();
952        let mut hasher3 = SimpleHasher::default();
953
954        domain1.hash(&mut hasher1);
955        domain2.hash(&mut hasher2);
956        domain3.hash(&mut hasher3);
957
958        assert_eq!(hasher1.finish(), hasher2.finish());
959        assert_ne!(hasher1.finish(), hasher3.finish());
960    }
961
962    #[test]
963    fn test_ordering_lexicographic() {
964        let domain1 = DomainName::new("a.example.com").unwrap();
965        let domain2 = DomainName::new("b.example.com").unwrap();
966        let domain3 = DomainName::new("a.example.com").unwrap();
967
968        assert!(domain1 < domain2);
969        assert!(domain2 > domain1);
970        assert_eq!(domain1, domain3);
971    }
972
973    #[test]
974    fn test_ordering_different_lengths() {
975        let domain1 = DomainName::new("a.com").unwrap();
976        let domain2 = DomainName::new("a.example.com").unwrap();
977
978        assert!(domain1 < domain2);
979    }
980
981    #[test]
982    fn test_debug() {
983        let domain = DomainName::new("example.com").unwrap();
984        assert_eq!(format!("{:?}", domain), "DomainName(\"example.com\")");
985    }
986
987    #[test]
988    fn test_as_inner() {
989        let domain = DomainName::new("example.com").unwrap();
990        let inner = domain.as_inner();
991        assert_eq!(inner.as_str(), "example.com");
992    }
993
994    #[test]
995    fn test_from_into_inner_roundtrip() {
996        let domain = DomainName::new("example.com").unwrap();
997        let inner: heapless::String<253> = domain.into();
998        let domain2 = DomainName::new(inner.as_str()).unwrap();
999        assert_eq!(domain2.as_str(), "example.com");
1000    }
1001}