use regex::Regex;
use crate::error::InterceptionError;
#[derive(Debug, Clone)]
pub struct UrlPattern {
pattern: String,
regex: Regex,
}
impl UrlPattern {
#[allow(clippy::result_large_err)] pub fn new(pattern: impl Into<String>) -> Result<Self, InterceptionError> {
let pattern = pattern.into();
let regex_src = compile_to_regex(&pattern);
let regex = Regex::new(®ex_src)
.map_err(|e| InterceptionError::InvalidPattern(format!("{pattern}: {e}")))?;
Ok(Self { pattern, regex })
}
pub fn matches(&self, url: &str) -> bool {
self.regex.is_match(url)
}
pub fn pattern_str(&self) -> &str {
&self.pattern
}
}
fn compile_to_regex(pattern: &str) -> String {
let mut out = String::with_capacity(pattern.len() + 4);
out.push('^');
for ch in pattern.chars() {
match ch {
'*' => out.push_str(".*"),
'?' => out.push('.'),
other => out.push_str(®ex::escape(&other.to_string())),
}
}
out.push('$');
out
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn wildcard_star_matches_all() {
let p = UrlPattern::new("*").unwrap();
assert!(p.matches("https://example.com/"));
assert!(p.matches("http://foo.bar/baz?qux=1"));
assert!(p.matches(""));
assert_eq!(p.pattern_str(), "*");
}
#[test]
fn subdomain_wildcard_matches() {
let p = UrlPattern::new("*.example.com/*").unwrap();
assert!(p.matches("https://api.example.com/foo"));
assert!(p.matches("ws://cdn.example.com/socket"));
assert!(!p.matches("https://example.org/foo"));
}
#[test]
fn invalid_pattern_errors() {
let huge = "*".repeat(50_000);
let err = UrlPattern::new(huge).expect_err("expected size-limit failure");
match err {
InterceptionError::InvalidPattern(msg) => {
assert!(
msg.contains("exceeds size limit") || msg.contains("size"),
"expected size-limit message, got: {msg}",
);
}
other => panic!("unexpected error variant: {other:?}"),
}
}
}