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