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 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 #[must_use]
457 #[inline]
458 pub fn is_tld(&self) -> bool {
459 self.depth() == 1
460 }
461
462 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 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 assert!(!child.is_subdomain_of(&parent));
799
800 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}