pub fn scope_contains(scope: &str, candidate: &str) -> bool {
let Some(scope_segments) = well_formed_segments(scope) else {
return false;
};
let Some(candidate_segments) = well_formed_segments(candidate) else {
return false;
};
candidate_segments.len() >= scope_segments.len()
&& scope_segments == candidate_segments[..scope_segments.len()]
}
fn well_formed_segments(path: &str) -> Option<Vec<&str>> {
let segments: Vec<&str> = path.split('/').collect();
if segments
.iter()
.any(|segment| segment.is_empty() || *segment == "." || *segment == "..")
{
return None;
}
Some(segments)
}
#[cfg(test)]
mod tests {
use super::scope_contains;
#[test]
fn exact_match_allowed() {
assert!(scope_contains("a/b", "a/b"));
}
#[test]
fn descendant_allowed() {
assert!(scope_contains("a/b", "a/b/c"));
assert!(scope_contains("a/b", "a/b/c/d"));
}
#[test]
fn dotdot_traversal_denied() {
assert!(!scope_contains("a/b", "a/b/../c"));
assert!(!scope_contains("a/b", "a/b/.."));
assert!(!scope_contains("a/b", "a/b/c/../../b/c"));
}
#[test]
fn single_dot_denied() {
assert!(!scope_contains("a/b", "a/b/./c"));
assert!(!scope_contains("a/b", "a/b/."));
}
#[test]
fn empty_segments_denied() {
assert!(!scope_contains("a/b", "a//b"));
assert!(!scope_contains("a/b", "a/b//c"));
assert!(!scope_contains("a/b", "a/b/"));
assert!(!scope_contains("a/b", "/a/b"));
assert!(!scope_contains("a/b", ""));
}
#[test]
fn sibling_denied() {
assert!(!scope_contains("a/b", "a/c"));
}
#[test]
fn upward_access_denied() {
assert!(!scope_contains("a/b", "a"));
assert!(!scope_contains("a/b/c", "a/b"));
}
#[test]
fn non_boundary_prefix_denied() {
assert!(!scope_contains("a/b", "a/bc"));
assert!(!scope_contains("a/bc", "a/b"));
}
#[test]
fn malformed_scope_denies_everything() {
assert!(!scope_contains("a/..", "a"));
assert!(!scope_contains("a/../b", "a/../b"));
assert!(!scope_contains("a//b", "a/b/c"));
assert!(!scope_contains("", "a"));
assert!(!scope_contains("", ""));
}
}