use core::{fmt, str::FromStr};
use crate::std::{
borrow::{Cow, ToOwned as _},
boxed::Box,
string::String,
};
use rama_core::error::{BoxError, BoxErrorExt, ErrorContext};
use rama_utils::thirdparty::wildcard::{Wildcard, WildcardBuilder};
use super::{Domain, DomainLabels, DomainRef};
#[derive(Clone)]
pub struct DomainPattern(DomainPatternKind);
#[derive(Clone)]
enum DomainPatternKind {
Exact(Domain),
Sub(Domain),
Glob(Wildcard<'static>),
}
impl DomainPattern {
#[must_use]
pub const fn exact(domain: Domain) -> Self {
Self(DomainPatternKind::Exact(domain))
}
#[must_use]
pub fn sub(domain: Domain) -> Self {
let apex = domain
.as_wildcard_parent()
.or_else(|| domain.strip_leading_dot())
.unwrap_or(domain);
Self(DomainPatternKind::Sub(apex))
}
pub fn try_glob(pattern: impl Into<Cow<'static, str>>) -> Result<Self, BoxError> {
let pattern = pattern.into();
validate_domain_glob(&pattern)?;
Ok(Self(DomainPatternKind::Glob(build_glob(pattern)?)))
}
pub fn try_new(pattern: impl TryIntoDomainPattern) -> Result<Self, BoxError> {
private::TryIntoDomainPatternPriv::try_into_domain_pattern(pattern)
}
#[must_use]
pub fn matches(&self, domain: DomainRef<'_>) -> bool {
match &self.0 {
DomainPatternKind::Exact(expected) => domain == expected.view(),
DomainPatternKind::Sub(apex) => domain.is_subdomain_of(apex),
DomainPatternKind::Glob(pattern) => pattern.is_match(domain.as_str().as_bytes()),
}
}
}
impl fmt::Debug for DomainPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
DomainPatternKind::Exact(domain) => f.debug_tuple("Exact").field(domain).finish(),
DomainPatternKind::Sub(domain) => f.debug_tuple("Sub").field(domain).finish(),
DomainPatternKind::Glob(_) => f.write_str("Glob(..)"),
}
}
}
impl FromStr for DomainPattern {
type Err = BoxError;
fn from_str(pattern: &str) -> Result<Self, Self::Err> {
let pattern = pattern.trim();
match Domain::try_from(pattern) {
Ok(domain) => {
let kind = match domain
.as_wildcard_parent()
.or_else(|| domain.strip_leading_dot())
{
Some(apex) => DomainPatternKind::Sub(apex),
None => DomainPatternKind::Exact(domain),
};
Ok(Self(kind))
}
Err(_) if pattern.contains('*') => Self::try_glob(pattern.to_owned()),
Err(error) => Err(error).context("parse exact domain pattern"),
}
}
}
impl TryFrom<String> for DomainPattern {
type Error = BoxError;
fn try_from(pattern: String) -> Result<Self, Self::Error> {
pattern.parse()
}
}
impl TryFrom<Box<str>> for DomainPattern {
type Error = BoxError;
fn try_from(pattern: Box<str>) -> Result<Self, Self::Error> {
pattern.parse()
}
}
fn validate_domain_glob(pattern: &str) -> Result<(), BoxError> {
if pattern.is_empty() {
return Err(BoxError::from_static_str("domain glob cannot be empty"));
}
if !pattern.is_ascii() {
return Err(BoxError::from_static_str(
"domain glob must be ASCII; use an exact or subtree pattern for IDNA names",
));
}
if !pattern.contains('*') {
return Err(BoxError::from_static_str(
"domain glob must contain at least one '*' wildcard",
));
}
let probe = pattern.replace('*', "a");
Domain::try_from(probe)
.map(drop)
.context("validate domain glob shape")
}
pub(crate) fn build_glob(pattern: Cow<'static, str>) -> Result<Wildcard<'static>, BoxError> {
let builder = match pattern {
Cow::Borrowed(pattern) => WildcardBuilder::new(pattern.as_bytes()),
Cow::Owned(pattern) => WildcardBuilder::from_owned(pattern.into_bytes()),
};
builder
.without_one_metasymbol()
.without_escape()
.case_insensitive(true)
.build()
.context("compile wildcard pattern")
}
#[expect(private_bounds)]
pub trait TryIntoDomainPattern: private::TryIntoDomainPatternPriv {}
impl TryIntoDomainPattern for DomainPattern {}
impl TryIntoDomainPattern for &str {}
impl TryIntoDomainPattern for String {}
impl TryIntoDomainPattern for Box<str> {}
mod private {
use super::*;
pub(super) trait TryIntoDomainPatternPriv {
fn try_into_domain_pattern(self) -> Result<DomainPattern, BoxError>;
}
impl TryIntoDomainPatternPriv for DomainPattern {
fn try_into_domain_pattern(self) -> Result<DomainPattern, BoxError> {
Ok(self)
}
}
impl TryIntoDomainPatternPriv for &str {
fn try_into_domain_pattern(self) -> Result<DomainPattern, BoxError> {
self.parse()
}
}
impl TryIntoDomainPatternPriv for String {
fn try_into_domain_pattern(self) -> Result<DomainPattern, BoxError> {
self.parse()
}
}
impl TryIntoDomainPatternPriv for Box<str> {
fn try_into_domain_pattern(self) -> Result<DomainPattern, BoxError> {
self.parse()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn explicit_constructors_do_not_parse_or_guess_semantics() {
let domain = Domain::from_static("example.com");
assert!(DomainPattern::exact(domain.clone()).matches(domain.view()));
assert!(
!DomainPattern::exact(domain.clone())
.matches(Domain::from_static("api.example.com").view())
);
assert!(DomainPattern::sub(domain).matches(Domain::from_static("api.example.com").view()));
}
#[test]
fn parser_distinguishes_exact_subtree_and_glob() {
let exact = DomainPattern::try_new("example.com").unwrap();
let wildcard_sub = DomainPattern::try_new("*.example.com").unwrap();
let leading_dot_sub = DomainPattern::try_new(".example.com").unwrap();
let glob = DomainPattern::try_new("api-*.example.com").unwrap();
let wildcard_prefix_glob = DomainPattern::try_new("*.corp*").unwrap();
let all = DomainPattern::try_new("*").unwrap();
assert!(exact.matches(Domain::from_static("example.com").view()));
assert!(!exact.matches(Domain::from_static("api.example.com").view()));
assert!(wildcard_sub.matches(Domain::from_static("example.com").view()));
assert!(wildcard_sub.matches(Domain::from_static("deep.api.example.com").view()));
assert!(leading_dot_sub.matches(Domain::from_static("example.com").view()));
assert!(leading_dot_sub.matches(Domain::from_static("deep.api.example.com").view()));
assert!(glob.matches(Domain::from_static("api-one.example.com").view()));
assert!(!glob.matches(Domain::from_static("www.example.com").view()));
assert!(wildcard_prefix_glob.matches(Domain::from_static("api.corporate").view()));
assert!(!wildcard_prefix_glob.matches(Domain::from_static("corp.example").view()));
assert!(all.matches(Domain::from_static("example.com").view()));
assert!(all.matches(Domain::from_static("internal").view()));
}
#[test]
fn glob_star_is_not_bounded_by_domain_labels() {
let pattern = DomainPattern::try_glob("api-*.example.com").unwrap();
assert!(pattern.matches(Domain::from_static("api-one.example.com").view()));
assert!(pattern.matches(Domain::from_static("api-one.internal.example.com").view()));
assert!(!pattern.matches(Domain::from_static("www.example.com").view()));
}
#[test]
fn invalid_non_glob_reports_an_exact_domain_error() {
let error = DomainPattern::try_new("not a valid domain").unwrap_err();
assert!(error.to_string().contains("parse exact domain pattern"));
}
#[test]
fn subtree_inputs_store_the_same_normalized_apex() {
for input in [
"example.com",
"*.example.com",
".example.com",
".*.example.com",
] {
let apex = match DomainPattern::sub(Domain::from_static(input)).0 {
DomainPatternKind::Sub(apex) => Some(apex),
DomainPatternKind::Exact(_) | DomainPatternKind::Glob(_) => None,
}
.unwrap();
assert_eq!(apex.as_str(), "example.com", "{input}");
}
}
#[test]
fn try_into_trait_does_not_reparse_an_existing_pattern() {
let pattern = DomainPattern::sub(Domain::from_static("example.com"));
let pattern = DomainPattern::try_new(pattern).unwrap();
assert!(pattern.matches(Domain::from_static("api.example.com").view()));
}
}