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}