use std::borrow::Cow;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostRule {
Exact(&'static str),
OneLabel {
prefix: &'static str,
suffix: &'static str,
},
}
impl HostRule {
fn matches_normalized(&self, host: &str) -> bool {
match *self {
HostRule::Exact(exact) => host == exact,
HostRule::OneLabel { prefix, suffix } => host
.strip_prefix(prefix)
.and_then(|rest| rest.strip_suffix(suffix))
.is_some_and(one_label),
}
}
pub fn matches(&self, host: &str) -> bool {
self.matches_normalized(&normalize(host))
}
pub fn is_pattern(&self) -> bool {
matches!(self, HostRule::OneLabel { .. })
}
}
pub fn one_label(label: &str) -> bool {
(1..=63).contains(&label.len())
&& label
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
}
pub fn normalize(host: &str) -> Cow<'_, str> {
let trimmed = host.strip_suffix('.').unwrap_or(host);
if trimmed.bytes().any(|b| b.is_ascii_uppercase()) {
Cow::Owned(trimmed.to_ascii_lowercase())
} else {
Cow::Borrowed(trimmed)
}
}
pub fn matches(rules: &[HostRule], host: &str) -> bool {
let host = normalize(host);
rules.iter().any(|r| r.matches_normalized(&host))
}
#[cfg(test)]
mod tests {
use super::*;
const PATTERNS: &[HostRule] = &[
HostRule::OneLabel {
prefix: "bedrock-runtime.",
suffix: ".amazonaws.com",
},
HostRule::OneLabel {
prefix: "bedrock-runtime.",
suffix: ".amazonaws.com.cn",
},
HostRule::OneLabel {
prefix: "",
suffix: ".openai.azure.com",
},
HostRule::OneLabel {
prefix: "",
suffix: ".cognitiveservices.azure.com",
},
HostRule::OneLabel {
prefix: "",
suffix: ".services.ai.azure.com",
},
HostRule::OneLabel {
prefix: "",
suffix: "-aiplatform.googleapis.com",
},
];
fn label(c: char, len: usize) -> String {
std::iter::repeat_n(c, len).collect()
}
#[test]
fn the_corpus_accepts_exactly_one_label_per_pattern() {
let labels: Vec<(String, bool)> = vec![
(label('a', 0), false), (label('a', 1), true), (label('a', 63), true), (label('a', 64), false), ("us-east-1".into(), true), ("my-resource-01".into(), true),
("a.b".into(), false), ("us-east-1.evil".into(), false),
(".".into(), false),
("a..b".into(), false),
("US-EAST-1".into(), true), ("xn--bcher-kva".into(), true), ("bücher".into(), false), ("é".into(), false),
("a_b".into(), false), ("a b".into(), false),
("*".into(), false), ("a*".into(), false),
("a:443".into(), false), ("a/b".into(), false),
];
let mut checked = 0usize;
for rule in PATTERNS {
let HostRule::OneLabel { prefix, suffix } = *rule else {
unreachable!("PATTERNS holds patterns only");
};
for (lbl, accepted) in &labels {
let host = format!("{prefix}{lbl}{suffix}");
assert_eq!(rule.matches(&host), *accepted, "{host:?} against {rule:?}");
assert_eq!(matches(PATTERNS, &host), *accepted, "{host:?} in PATTERNS");
let dotted = format!("{host}.");
assert_eq!(
rule.matches(&dotted),
*accepted,
"{dotted:?} against {rule:?}"
);
let upper = host.to_uppercase();
if upper.is_ascii() {
assert_eq!(
rule.matches(&upper),
*accepted,
"{upper:?} against {rule:?}"
);
}
checked += 1;
}
let good = "us-east-1";
let off = [
format!("{prefix}{good}{suffix}.evil.test"),
format!("{prefix}{good}{suffix}x"),
format!("evil.{prefix}{good}{suffix}"),
format!("{prefix}{good}{}", &suffix[1..]),
suffix.trim_start_matches(['.', '-']).to_string(),
suffix.to_string(),
format!("{prefix}{suffix}"),
format!("{prefix}{good}{suffix}.."),
String::new(),
];
for host in &off {
assert!(!rule.matches(host), "{host:?} must not match {rule:?}");
}
assert!(rule.matches(&format!("{prefix}{good}x{suffix}")));
if !prefix.is_empty() {
assert!(!rule.matches(&format!("x{prefix}{good}{suffix}")));
}
}
assert_eq!(checked, PATTERNS.len() * labels.len());
}
#[test]
fn a_suffix_is_matched_whole() {
let com = PATTERNS[0];
let cn = PATTERNS[1];
assert!(com.matches("bedrock-runtime.us-east-1.amazonaws.com"));
assert!(!com.matches("bedrock-runtime.cn-north-1.amazonaws.com.cn"));
assert!(cn.matches("bedrock-runtime.cn-north-1.amazonaws.com.cn"));
assert!(!cn.matches("bedrock-runtime.us-east-1.amazonaws.com"));
let vertex = PATTERNS[5];
assert!(vertex.matches("us-central1-aiplatform.googleapis.com"));
assert!(!vertex.matches("aiplatform.googleapis.com"));
assert!(!vertex.matches("-aiplatform.googleapis.com"));
assert!(!vertex.matches("a.us-central1-aiplatform.googleapis.com"));
assert!(!matches(PATTERNS, "bedrock-mantle.us-east-1.api.aws"));
}
#[test]
fn exact_rules_compare_normalized() {
let rules = [HostRule::Exact("api.groq.com")];
assert!(matches(&rules, "api.groq.com"));
assert!(matches(&rules, "API.Groq.COM"));
assert!(matches(&rules, "api.groq.com."));
assert!(!matches(&rules, "api.groq.com.."));
assert!(!matches(&rules, "x.api.groq.com"));
assert!(!matches(&rules, "api.groq.co"));
assert!(!matches(&rules, ""));
assert!(!HostRule::Exact("a.test").is_pattern());
assert!(PATTERNS.iter().all(HostRule::is_pattern));
}
#[test]
fn normalize_borrows_when_nothing_changes() {
assert!(matches!(normalize("a.test"), Cow::Borrowed("a.test")));
assert!(matches!(normalize("a.test."), Cow::Borrowed("a.test")));
assert_eq!(normalize("A.Test."), "a.test");
assert!(matches!(normalize("A.test"), Cow::Owned(_)));
}
#[test]
fn one_label_is_the_dns_label_alphabet_and_length() {
assert!(!one_label(""));
assert!(one_label("a"));
assert!(one_label(&label('z', 63)));
assert!(!one_label(&label('z', 64)));
assert!(one_label("0-9"));
assert!(!one_label("A"));
assert!(!one_label("a.b"));
assert!(!one_label("ä"));
}
}