Skip to main content

arcbox_helper/validate/
domain.rs

1use std::str::FromStr;
2
3/// A validated DNS domain name (e.g. `arcbox.local`).
4///
5/// Guarantees:
6/// - Non-empty, ≤253 chars, lowercase alphanumeric + `.` + `-`
7/// - No leading/trailing dots, no consecutive dots
8/// - Each label ≤63 chars, no leading/trailing `-` (RFC 1035/1123)
9#[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}