Skip to main content

dinio_sdk/ops/
names.rs

1//! Owner names: Core stores full names (lowercase, no trailing dot) and rejects names
2//! outside the zone. Users think in zone-relative names, as in a zone file.
3
4use crate::error::{Error, Result};
5
6/// Full owner name from what a user typed, zone-file style:
7/// `@` → the apex; a trailing dot → absolute as given; a name already equal to the zone
8/// or ending in `.<zone>` → as given; anything else is relative to the zone.
9///
10/// Names outside the zone are not refused here; Core refuses them.
11///
12/// # Errors
13///
14/// [`Error::InvalidInput`] if `name` is empty.
15///
16/// # Examples
17///
18/// ```
19/// use dinio_sdk::ops::qualify_name;
20///
21/// assert_eq!(qualify_name("@", "example.com")?, "example.com");
22/// assert_eq!(qualify_name("www", "example.com")?, "www.example.com");
23/// assert_eq!(qualify_name("www.example.com", "example.com")?, "www.example.com");
24/// assert_eq!(qualify_name("mail.other.org.", "example.com")?, "mail.other.org");
25/// # Ok::<(), dinio_sdk::Error>(())
26/// ```
27pub fn qualify_name(name: &str, zone: &str) -> Result<String> {
28    let name = name.trim();
29    let zone = normalize(zone);
30    if name.is_empty() {
31        return Err(Error::InvalidInput("record name must not be empty".into()));
32    }
33    if name == "@" {
34        return Ok(zone);
35    }
36    if let Some(absolute) = name.strip_suffix('.') {
37        return Ok(absolute.to_lowercase());
38    }
39    let lower = name.to_lowercase();
40    if lower == zone || lower.ends_with(&format!(".{zone}")) {
41        return Ok(lower);
42    }
43    Ok(format!("{lower}.{zone}"))
44}
45
46/// Full owner name → zone-relative form for display (`@` for the apex). Names outside the
47/// zone are returned unchanged.
48///
49/// # Examples
50///
51/// ```
52/// use dinio_sdk::ops::relative_name;
53///
54/// assert_eq!(relative_name("example.com", "example.com"), "@");
55/// assert_eq!(relative_name("www.example.com", "example.com"), "www");
56/// ```
57pub fn relative_name(full: &str, zone: &str) -> String {
58    let zone = normalize(zone);
59    if full == zone {
60        return "@".into();
61    }
62    match full.strip_suffix(&format!(".{zone}")) {
63        Some(relative) => relative.to_owned(),
64        None => full.to_owned(),
65    }
66}
67
68fn normalize(name: &str) -> String {
69    name.trim().trim_end_matches('.').to_lowercase()
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn qualify() {
78        let z = "example.com";
79        assert_eq!(qualify_name("@", z).unwrap(), "example.com");
80        assert_eq!(qualify_name("www", z).unwrap(), "www.example.com");
81        assert_eq!(qualify_name("WWW", z).unwrap(), "www.example.com");
82        assert_eq!(
83            qualify_name("www.example.com", z).unwrap(),
84            "www.example.com"
85        );
86        assert_eq!(qualify_name("example.com", z).unwrap(), "example.com");
87        assert_eq!(qualify_name("a.b", z).unwrap(), "a.b.example.com");
88        assert_eq!(qualify_name("*.dev", z).unwrap(), "*.dev.example.com");
89        assert_eq!(qualify_name("other.org.", z).unwrap(), "other.org");
90        assert_eq!(
91            qualify_name("_sip._tcp", "Example.COM.").unwrap(),
92            "_sip._tcp.example.com"
93        );
94        assert!(qualify_name("  ", z).is_err());
95    }
96
97    #[test]
98    fn relative() {
99        let z = "example.com";
100        assert_eq!(relative_name("example.com", z), "@");
101        assert_eq!(relative_name("www.example.com", z), "www");
102        assert_eq!(relative_name("notexample.com", z), "notexample.com");
103    }
104}