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))]
60#[non_exhaustive]
61pub enum DomainNameError {
62 Empty,
66 TooLong(usize),
71 LabelTooLong {
76 label: usize,
78 len: usize,
80 },
81 InvalidLabelStart(char),
87 InvalidLabelEnd(char),
93 InvalidChar(char),
98 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#[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 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 let label_len = 1 + (u8::arbitrary(u)? % 20).min(19);
190
191 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 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 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 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 #[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 #[must_use]
355 #[inline]
356 pub fn as_str(&self) -> &str {
357 &self.0
358 }
359
360 #[must_use]
372 #[inline]
373 pub const fn as_inner(&self) -> &heapless::String<253> {
374 &self.0
375 }
376
377 #[must_use]
389 #[inline]
390 pub fn into_inner(self) -> heapless::String<253> {
391 self.0
392 }
393
394 #[must_use]
408 #[inline]
409 pub fn depth(&self) -> usize {
410 self.as_str().chars().filter(|&c| c == '.').count() + 1
411 }
412
413 #[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 #[must_use]
458 #[inline]
459 pub fn is_tld(&self) -> bool {
460 self.depth() == 1
461 }
462
463 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 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 assert!(!child.is_subdomain_of(&parent));
800
801 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}