smolvm_protocol/
host_pattern.rs1pub fn encode_strict(pattern: &str) -> Result<String, String> {
10 let pattern = pattern.strip_suffix('.').unwrap_or(pattern);
12 let (host, wildcard) = match pattern.strip_prefix("*.") {
14 Some(host) => (host, true),
15 None => (pattern, false),
16 };
17 let empty_host = host.is_empty();
19 let name_too_long = host.len() > 253;
21 let extra_wildcard = host.contains('*');
23 let ip_literal = !wildcard && host.parse::<std::net::IpAddr>().is_ok();
25 let invalid_label = host.split('.').any(|label| {
26 let empty = label.is_empty();
28 let too_long = label.len() > 63;
30 let edge_hyphen = label.starts_with('-') || label.ends_with('-');
32 let non_hostname_character = !label
34 .bytes()
35 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-');
36 empty || too_long || edge_hyphen || non_hostname_character
37 });
38 if empty_host || name_too_long || extra_wildcard || ip_literal || invalid_label {
39 return Err(format!(
40 "invalid host pattern {pattern:?}: use an exact DNS hostname or *.domain"
41 ));
42 }
43 Ok(if wildcard {
44 format!("*.{}", host.to_ascii_lowercase())
45 } else {
46 format!("={}", host.to_ascii_lowercase())
47 })
48}
49
50pub fn matches(host: &str, entry: &str) -> bool {
52 let host = host.trim_end_matches('.').to_ascii_lowercase();
54 let entry = entry.trim_end_matches('.').to_ascii_lowercase();
55 if let Some(exact) = entry.strip_prefix('=') {
57 return !exact.is_empty() && host == exact;
58 }
59 if let Some(suffix) = entry.strip_prefix("*.") {
61 return !suffix.is_empty()
62 && host
63 .strip_suffix(suffix)
64 .is_some_and(|prefix| prefix.ends_with('.') && prefix.len() > 1);
65 }
66 !entry.is_empty()
68 && (host == entry
69 || host
70 .strip_suffix(&entry)
71 .is_some_and(|prefix| prefix.ends_with('.')))
72}
73
74pub fn static_resolution_host(entry: &str) -> Option<&str> {
77 if entry.starts_with("*.") {
78 None
79 } else {
80 Some(entry.strip_prefix('=').unwrap_or(entry))
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn old_and_new_entries_have_distinct_semantics() {
90 assert!(matches("api.example.com", "example.com"));
91 assert!(matches("example.com", "example.com"));
92 let exact = encode_strict("Example.COM").unwrap();
93 assert_eq!(exact, "=example.com");
94 assert_eq!(encode_strict("Example.COM.").unwrap(), exact);
95 assert!(matches("EXAMPLE.COM.", &exact));
96 assert!(!matches("api.example.com", &exact));
97 let wildcard = encode_strict("*.example.com").unwrap();
98 assert!(matches("api.example.com", &wildcard));
99 assert!(matches("a.b.example.com", &wildcard));
100 assert!(!matches("example.com", &wildcard));
101 assert!(!matches("notexample.com", &wildcard));
102 assert!(!matches("example.com.evil.test", &wildcard));
103 }
104
105 #[test]
106 fn rejects_invalid_patterns() {
107 for pattern in [
108 "",
109 "*",
110 "*.",
111 "*.*.example.com",
112 "foo*.example.com",
113 "https://example.com",
114 "example.com:443",
115 "example.com..",
116 "1.2.3.4",
117 ] {
118 assert!(encode_strict(pattern).is_err(), "{pattern}");
119 }
120 }
121
122 #[test]
123 fn enforces_dns_name_and_label_size_limits() {
124 let longest_name = [
125 "a".repeat(63),
126 "b".repeat(63),
127 "c".repeat(63),
128 "d".repeat(61),
129 ]
130 .join(".");
131 assert_eq!(longest_name.len(), 253);
132 assert!(encode_strict(&longest_name).is_ok());
133 assert!(encode_strict(&format!("{longest_name}d")).is_err());
134 assert!(encode_strict(&"a".repeat(64)).is_err());
135 }
136}