1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
60pub enum DomainNameError {
61 Empty,
63 TooLong(usize),
65 LabelTooLong {
67 label: usize,
69 len: usize,
71 },
72 InvalidLabelStart(char),
74 InvalidLabelEnd(char),
76 InvalidChar(char),
78 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#[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 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 let label_len = 1 + (u8::arbitrary(u)? % 20).min(19);
167
168 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 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 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 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 #[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 #[must_use]
332 #[inline]
333 pub fn as_str(&self) -> &str {
334 &self.0
335 }
336
337 #[must_use]
349 #[inline]
350 pub const fn as_inner(&self) -> &heapless::String<253> {
351 &self.0
352 }
353
354 #[must_use]
366 #[inline]
367 pub fn into_inner(self) -> heapless::String<253> {
368 self.0
369 }
370
371 #[must_use]
385 #[inline]
386 pub fn depth(&self) -> usize {
387 self.as_str().chars().filter(|&c| c == '.').count() + 1
388 }
389
390 #[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 #[must_use]
434 #[inline]
435 pub fn is_tld(&self) -> bool {
436 self.depth() == 1
437 }
438
439 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 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 assert!(!child.is_subdomain_of(&parent));
776
777 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}