arcbox_helper/validate/
domain.rs1use std::str::FromStr;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Domain(String);
11
12impl Domain {
13 pub fn as_str(&self) -> &str {
14 &self.0
15 }
16}
17
18impl FromStr for Domain {
19 type Err = String;
20
21 fn from_str(s: &str) -> Result<Self, Self::Err> {
22 if s.is_empty() {
23 return Err("domain must not be empty".to_string());
24 }
25 if s.len() > 253 {
26 return Err(format!("domain too long ({} > 253 chars)", s.len()));
27 }
28 if !s
29 .chars()
30 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '.' || c == '-')
31 {
32 return Err(format!(
33 "domain '{s}' contains invalid characters (allowed: a-z, 0-9, '.', '-')"
34 ));
35 }
36 if s.starts_with('.') || s.ends_with('.') {
37 return Err(format!("domain '{s}' must not start or end with '.'"));
38 }
39 if s.contains("..") {
40 return Err(format!(
41 "domain '{s}' contains empty label (consecutive dots)"
42 ));
43 }
44
45 for label in s.split('.') {
46 if label.len() > 63 {
47 return Err(format!("domain '{s}' has label exceeding 63 chars"));
48 }
49 if label.starts_with('-') || label.ends_with('-') {
50 return Err(format!(
51 "domain '{s}' has label starting or ending with '-'"
52 ));
53 }
54 }
55
56 Ok(Self(s.to_owned()))
57 }
58}
59
60impl std::fmt::Display for Domain {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.write_str(&self.0)
63 }
64}