Skip to main content

boatramp_types/
host.rs

1//! The [`Host`] type: one home for the routing-host normalizations that were
2//! scattered across three crates (`canon_host` in core, `canon_domain_entry` in
3//! the server, `normalize_host` here) with subtly different — and deliberately
4//! distinct — wildcard/case rules that all feed KV keys and DNS record names.
5//!
6//! Each rule is reproduced **exactly** as a named method so a reader picks the
7//! semantic explicitly and every serialized boundary stays byte-for-byte:
8//! [`Host::routing_key`] and [`Host::domain_entry`] *preserve* a `*.` wildcard
9//! (a wildcard route is not its apex), while [`Host::verification`] *strips* it
10//! (a wildcard is verified at its base domain, like ACME). Collapsing the two
11//! is the trap — they are not interchangeable.
12
13use crate::domain_verify::DNS_RECORD_PREFIX;
14
15/// A routing host: a `Host`-header value, a configured domain, or a wildcard
16/// like `*.example.com`. Borrows the raw string and projects the several
17/// distinct canonical forms the codebase needs; construction is free
18/// (normalization happens per projection).
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct Host<'a>(&'a str);
21
22impl<'a> Host<'a> {
23    /// Wrap a raw host string. No normalization happens here — pick a projection.
24    pub fn new(raw: &'a str) -> Self {
25        Self(raw)
26    }
27
28    /// The raw, un-normalized host as given.
29    pub fn as_raw(&self) -> &'a str {
30        self.0
31    }
32
33    /// Routing-key normalization (was `canon_host`): trim, strip trailing dots,
34    /// lowercase — **preserving** any leading `*.`. Backs the `domain/<host>`
35    /// and `wildcard/<suffix>` routing keys.
36    pub fn routing_key(&self) -> String {
37        self.0.trim().trim_end_matches('.').to_ascii_lowercase()
38    }
39
40    /// Domain-entry normalization (was `canon_domain_entry`): normalize the base
41    /// then re-prepend `*.` for a wildcard, else identical to
42    /// [`routing_key`](Self::routing_key). Kept as its own method to reproduce
43    /// the config-canonicalization path byte-for-byte.
44    pub fn domain_entry(&self) -> String {
45        match self.0.strip_prefix("*.") {
46            Some(base) => format!(
47                "*.{}",
48                base.trim().trim_end_matches('.').to_ascii_lowercase()
49            ),
50            None => self.routing_key(),
51        }
52    }
53
54    /// Verification normalization (was `normalize_host`): trim, strip trailing
55    /// dots, **strip** any leading `*.`, lowercase — so a wildcard and its apex
56    /// share one verification key / TXT record. Backs `domainverify/<site>/<host>`.
57    pub fn verification(&self) -> String {
58        let host = self.0.trim().trim_end_matches('.');
59        host.strip_prefix("*.").unwrap_or(host).to_ascii_lowercase()
60    }
61
62    /// Whether this is a wildcard host (`*.example.com`).
63    pub fn is_wildcard(&self) -> bool {
64        self.0.trim().starts_with("*.")
65    }
66
67    /// The ACME/DNS-01 TXT record name for this host (verification-normalized).
68    pub fn dns_record_name(&self) -> String {
69        format!("{DNS_RECORD_PREFIX}.{}", self.verification())
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn verification_strips_wildcard_and_normalizes() {
79        // Oracle: the cases the former `normalize_host` guaranteed.
80        assert_eq!(Host::new("*.Example.COM.").verification(), "example.com");
81        assert_eq!(
82            Host::new("  www.example.com  ").verification(),
83            "www.example.com"
84        );
85        assert_eq!(Host::new("EXAMPLE.com").verification(), "example.com");
86    }
87
88    #[test]
89    fn routing_key_preserves_wildcard() {
90        // Oracle: the cases the former `canon_host` guaranteed.
91        assert_eq!(Host::new("*.Example.com").routing_key(), "*.example.com");
92        assert_eq!(Host::new("Example.COM.").routing_key(), "example.com");
93        assert_eq!(
94            Host::new("  app.example.com  ").routing_key(),
95            "app.example.com"
96        );
97    }
98
99    #[test]
100    fn domain_entry_matches_routing_for_wellformed_hosts() {
101        // Oracle: the former `canon_domain_entry` — equal to routing_key on
102        // well-formed input, wildcard preserved either way.
103        for host in ["*.Example.com", "example.com.", "  API.example.com  "] {
104            assert_eq!(
105                Host::new(host).domain_entry(),
106                Host::new(host).routing_key()
107            );
108        }
109    }
110
111    #[test]
112    fn routing_and_verification_diverge_only_on_the_wildcard() {
113        let h = Host::new("*.example.com");
114        assert_eq!(h.routing_key(), "*.example.com");
115        assert_eq!(h.verification(), "example.com");
116        assert!(h.is_wildcard());
117    }
118
119    #[test]
120    fn dns_record_name_uses_the_verification_form() {
121        assert_eq!(
122            Host::new("*.Example.com").dns_record_name(),
123            format!("{DNS_RECORD_PREFIX}.example.com"),
124        );
125    }
126}