pub(crate) fn path_prefix_matches(path: &str, prefix: &str) -> bool {
let trimmed = prefix.strip_suffix('/').unwrap_or(prefix);
if trimmed.is_empty() {
return true;
}
if path == trimmed {
return true;
}
path.starts_with(trimmed) && path.as_bytes().get(trimmed.len()) == Some(&b'/')
}
pub(crate) fn path_prefix_specificity(prefix: &str) -> usize {
let trimmed = prefix.strip_suffix('/').unwrap_or(prefix);
if trimmed.is_empty() { 1 } else { trimmed.len() }
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(clippy::unwrap_used, reason = "tests")]
mod tests {
use super::*;
#[test]
fn root_matches_everything() {
assert!(path_prefix_matches("/anything", "/"));
assert!(path_prefix_matches("/", "/"));
}
#[test]
fn empty_prefix_matches_everything() {
assert!(path_prefix_matches("/foo", ""));
assert!(path_prefix_matches("/", ""));
}
#[test]
fn exact_match() {
assert!(path_prefix_matches("/api", "/api"));
}
#[test]
fn with_trailing_slash_on_path() {
assert!(path_prefix_matches("/api/", "/api"));
}
#[test]
fn subpath_match() {
assert!(path_prefix_matches("/api/v1", "/api"));
}
#[test]
fn no_segment_boundary_rejected() {
assert!(!path_prefix_matches("/apikeys", "/api"));
}
#[test]
fn prefix_with_trailing_slash_equivalent() {
assert!(path_prefix_matches("/api/v1", "/api/"));
assert!(path_prefix_matches("/api", "/api/"));
}
#[test]
fn specificity_trims_trailing_slash() {
assert_eq!(path_prefix_specificity("/api"), path_prefix_specificity("/api/"));
}
#[test]
fn specificity_root() {
assert_eq!(path_prefix_specificity("/"), 1);
assert_eq!(path_prefix_specificity(""), 1);
}
#[test]
fn specificity_value() {
assert_eq!(path_prefix_specificity("/api"), 4);
assert_eq!(path_prefix_specificity("/api/v1"), 7);
}
#[test]
fn double_slash_prefix() {
assert!(!path_prefix_matches("/foo", "//"), "/foo must not match // prefix");
assert!(path_prefix_matches("//bar", "//"), "//bar should match // prefix");
}
mod properties {
use proptest::prelude::*;
use super::*;
fn segment() -> impl Strategy<Value = String> {
"[a-z0-9]{1,8}"
}
fn path() -> impl Strategy<Value = String> {
proptest::collection::vec(segment(), 1..=4).prop_map(|segs| format!("/{}", segs.join("/")))
}
proptest! {
#[test]
fn match_implies_starts_with(p in path(), prefix in path()) {
if path_prefix_matches(&p, &prefix) {
let trimmed = prefix.strip_suffix('/').unwrap_or(&prefix);
prop_assert!(p.starts_with(trimmed));
}
}
#[test]
fn trailing_slash_equivalence(p in path(), prefix in path()) {
prop_assert_eq!(
path_prefix_matches(&p, &prefix),
path_prefix_matches(&p, &format!("{prefix}/"))
);
}
#[test]
fn prefix_matches_own_extensions(prefix in path(), seg in segment()) {
let extended = format!("{prefix}/{seg}");
prop_assert!(path_prefix_matches(&prefix, &prefix));
prop_assert!(path_prefix_matches(&extended, &prefix));
}
#[test]
fn non_boundary_extension_never_matches(prefix in path(), tail in "[a-z0-9]{1,8}") {
let extended = format!("{prefix}{tail}");
prop_assert!(!path_prefix_matches(&extended, &prefix));
}
#[test]
fn specificity_monotonic(prefix in path(), seg in segment()) {
let extended = format!("{prefix}/{seg}");
prop_assert!(path_prefix_specificity(&extended) > path_prefix_specificity(&prefix));
}
#[test]
fn never_panics(p in "\\PC{0,32}", prefix in "\\PC{0,32}") {
let _ = path_prefix_matches(&p, &prefix);
let _ = path_prefix_specificity(&prefix);
}
}
}
}