use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub enum ReadPathEntry {
Exact(PathBuf),
Glob {
pattern: glob::Pattern,
options: glob::MatchOptions,
},
Regex(regex::Regex),
}
impl ReadPathEntry {
fn matches(&self, canonical: &Path, normalized: &str) -> bool {
match self {
ReadPathEntry::Exact(root) => match std::fs::canonicalize(root) {
Ok(real_root) => canonical.starts_with(&real_root),
Err(_) => false,
},
ReadPathEntry::Glob { pattern, options } => pattern.matches_with(normalized, *options),
ReadPathEntry::Regex(re) => re.is_match(normalized),
}
}
pub fn sample_path(&self) -> Option<PathBuf> {
match self {
ReadPathEntry::Exact(root) => Some(root.clone()),
ReadPathEntry::Glob { pattern, options } => {
let sample = fill_glob_wildcards(pattern.as_str())?;
pattern
.matches_with(&sample, *options)
.then(|| PathBuf::from(sample))
}
ReadPathEntry::Regex(re) => {
let literal = literal_prefix(strip_regex_anchors(re.as_str()));
let in_dir = literal
.rsplit_once('/')
.map(|(dir, _)| format!("{dir}/{SAMPLE_COMPONENT}"));
[in_dir, (!literal.is_empty()).then_some(literal)]
.into_iter()
.flatten()
.find(|candidate| re.is_match(candidate))
.map(PathBuf::from)
}
}
}
}
const SAMPLE_COMPONENT: &str = "_leviath_probe";
fn fill_glob_wildcards(pattern: &str) -> Option<String> {
let mut out = String::with_capacity(pattern.len());
let mut previous_was_star = false;
for ch in pattern.chars() {
match ch {
'[' => return None,
'*' => {
if !previous_was_star {
out.push_str(SAMPLE_COMPONENT);
}
previous_was_star = true;
continue;
}
'?' => out.push('x'),
other => out.push(other),
}
previous_was_star = false;
}
Some(out)
}
fn strip_regex_anchors(pattern: &str) -> &str {
pattern
.strip_prefix("^(?:")
.and_then(|rest| rest.strip_suffix(")$"))
.unwrap_or(pattern)
}
fn literal_prefix(pattern: &str) -> String {
pattern
.chars()
.take_while(|c| {
!matches!(
c,
'.' | '[' | ']' | '(' | ')' | '{' | '}' | '*' | '+' | '?' | '|' | '^' | '$' | '\\'
)
})
.collect()
}
pub fn normalize_match_str(s: &str, windows: bool) -> String {
if !windows {
return s.to_string();
}
let stripped = if let Some(rest) = s.strip_prefix(r"\\?\UNC\") {
format!(r"\\{rest}")
} else if let Some(rest) = s.strip_prefix(r"\\?\") {
let bytes = rest.as_bytes();
if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
rest.to_string()
} else {
s.to_string()
}
} else {
s.to_string()
};
stripped.replace('\\', "/")
}
#[derive(Debug, Clone, Default)]
pub struct ReadPathSet {
entries: Vec<ReadPathEntry>,
windows: bool,
}
impl ReadPathSet {
pub fn compile(
raw: &[String],
workdir: &Path,
home: Option<&Path>,
windows: bool,
) -> Result<Self, String> {
let entries = raw
.iter()
.map(|entry| compile_entry(entry, workdir, home, windows))
.collect::<Result<Vec<_>, String>>()?;
Ok(Self { entries, windows })
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn entries(&self) -> &[ReadPathEntry] {
&self.entries
}
pub fn matches(&self, canonical: &Path) -> bool {
let normalized = normalize_match_str(&canonical.to_string_lossy(), self.windows);
self.entries
.iter()
.any(|e| e.matches(canonical, &normalized))
}
pub fn matches_lexically(&self, path: &Path) -> bool {
let normalized = normalize_match_str(&path.to_string_lossy(), self.windows);
self.entries.iter().any(|entry| match entry {
ReadPathEntry::Exact(root) => {
let root = normalize_match_str(&root.to_string_lossy(), self.windows);
covers_lexically(&normalized, &root, self.windows)
}
other => other.matches(path, &normalized),
})
}
}
fn covers_lexically(path: &str, root: &str, windows: bool) -> bool {
let fold = |s: &str| {
if windows {
s.to_lowercase()
} else {
s.to_string()
}
};
let path = fold(path);
let root = fold(root);
let trimmed = root.trim_end_matches('/');
path == trimmed || path.starts_with(&format!("{trimmed}/"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReadPathDecision {
Allowed,
NotDeclared,
NotGranted,
}
#[derive(Debug, Clone, Default)]
pub struct ReadPathPolicy {
pub agent: String,
pub blueprint: ReadPathSet,
pub grants: ReadPathSet,
pub allow_blueprint: bool,
}
impl ReadPathPolicy {
pub fn inactive() -> Self {
Self::default()
}
pub fn is_active(&self) -> bool {
!self.blueprint.is_empty()
}
pub fn decide(&self, canonical: &Path) -> ReadPathDecision {
if !self.blueprint.matches(canonical) {
return ReadPathDecision::NotDeclared;
}
if self.allow_blueprint || self.grants.matches(canonical) {
ReadPathDecision::Allowed
} else {
ReadPathDecision::NotGranted
}
}
}
pub fn validate_entry_syntax(raw: &str) -> Result<(), String> {
let workdir = Path::new("/validate/a/b/c/d/e/f/g/h");
compile_entry(raw, workdir, Some(Path::new("/validate-home")), false).map(|_| ())
}
fn compile_entry(
raw: &str,
workdir: &Path,
home: Option<&Path>,
windows: bool,
) -> Result<ReadPathEntry, String> {
if raw.trim().is_empty() {
return Err("read_paths entry is empty".to_string());
}
if let Some(rest) = raw.strip_prefix("regex:") {
compile_regex(raw, rest, home, windows)
} else if let Some(rest) = raw.strip_prefix("glob:") {
compile_glob(raw, rest, workdir, home, windows)
} else {
compile_exact(raw, workdir, home)
}
}
fn absolute_shaped(text: &str) -> bool {
let bytes = text.as_bytes();
text.starts_with('/')
|| (bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':')
}
fn compile_regex(
raw: &str,
rest: &str,
home: Option<&Path>,
windows: bool,
) -> Result<ReadPathEntry, String> {
if rest.is_empty() {
return Err(format!("read_paths entry '{raw}': regex pattern is empty"));
}
let body = if let Some(after_tilde) = rest.strip_prefix('~') {
if !(after_tilde.is_empty() || after_tilde.starts_with('/')) {
return Err(format!(
"read_paths entry '{raw}': only '~/' home expansion is supported"
));
}
let home = home.ok_or_else(|| {
format!("read_paths entry '{raw}': no home directory resolved for '~' expansion")
})?;
let prefix = regex::escape(&normalize_match_str(&home.to_string_lossy(), windows));
format!("{prefix}{after_tilde}")
} else if absolute_shaped(rest) {
rest.to_string()
} else {
return Err(format!(
"read_paths entry '{raw}': regex entries must start with '/', a drive letter, or '~/'; \
use 'glob:' for workdir-relative patterns"
));
};
regex::RegexBuilder::new(&format!("^(?:{body})$"))
.case_insensitive(windows)
.build()
.map(ReadPathEntry::Regex)
.map_err(|e| format!("read_paths entry '{raw}': invalid regex: {e}"))
}
fn compile_glob(
raw: &str,
rest: &str,
workdir: &Path,
home: Option<&Path>,
windows: bool,
) -> Result<ReadPathEntry, String> {
if rest.is_empty() {
return Err(format!("read_paths entry '{raw}': glob pattern is empty"));
}
let text = rest.replace('\\', "/");
let text = if let Some(after_tilde) = text.strip_prefix('~') {
if !(after_tilde.is_empty() || after_tilde.starts_with('/')) {
return Err(format!(
"read_paths entry '{raw}': only '~/' home expansion is supported"
));
}
let home = home.ok_or_else(|| {
format!("read_paths entry '{raw}': no home directory resolved for '~' expansion")
})?;
let prefix = glob::Pattern::escape(&normalize_match_str(&home.to_string_lossy(), windows));
format!("{prefix}{after_tilde}")
} else if absolute_shaped(&text) {
text
} else {
resolve_relative_glob(raw, &text, workdir, windows)?
};
if text.split('/').any(|c| c == "." || c == "..") {
return Err(format!(
"read_paths entry '{raw}': glob patterns cannot contain '.' or '..' components \
(relative entries fold them against the workdir at the start only)"
));
}
let pattern = glob::Pattern::new(&text)
.map_err(|e| format!("read_paths entry '{raw}': invalid glob: {e}"))?;
let options = glob::MatchOptions {
case_sensitive: !windows,
require_literal_separator: true,
require_literal_leading_dot: false,
};
Ok(ReadPathEntry::Glob { pattern, options })
}
fn resolve_relative_glob(
raw: &str,
text: &str,
workdir: &Path,
windows: bool,
) -> Result<String, String> {
let base_str = normalize_match_str(&workdir.to_string_lossy(), windows);
let mut base: Vec<&str> = base_str.split('/').collect();
while base.len() > 1 && base.last().is_some_and(|s| s.is_empty()) {
base.pop();
}
let mut rest = text;
loop {
if let Some(r) = rest.strip_prefix("./") {
rest = r;
} else if let Some(r) = rest.strip_prefix("../") {
if base.len() <= 1 {
return Err(format!(
"read_paths entry '{raw}': relative pattern escapes the filesystem root"
));
}
base.pop();
rest = r;
} else {
break;
}
}
let prefix = glob::Pattern::escape(&base.join("/"));
Ok(if rest.is_empty() {
prefix
} else {
format!("{prefix}/{rest}")
})
}
fn compile_exact(raw: &str, workdir: &Path, home: Option<&Path>) -> Result<ReadPathEntry, String> {
let path = if let Some(after_tilde) = raw.strip_prefix('~') {
let sub = after_tilde
.strip_prefix('/')
.or_else(|| after_tilde.strip_prefix('\\'));
let sub = match (sub, after_tilde.is_empty()) {
(_, true) => "",
(Some(sub), _) => sub,
(None, false) => {
return Err(format!(
"read_paths entry '{raw}': only '~/' home expansion is supported"
));
}
};
let home = home.ok_or_else(|| {
format!("read_paths entry '{raw}': no home directory resolved for '~' expansion")
})?;
if sub.is_empty() {
home.to_path_buf()
} else {
home.join(sub)
}
} else if Path::new(raw).is_absolute() {
PathBuf::from(raw)
} else {
workdir.join(raw)
};
Ok(ReadPathEntry::Exact(path))
}
#[cfg(test)]
mod tests {
use super::*;
fn set(entries: &[&str], workdir: &str, home: Option<&str>, windows: bool) -> ReadPathSet {
let raw: Vec<String> = entries.iter().map(|s| s.to_string()).collect();
ReadPathSet::compile(&raw, Path::new(workdir), home.map(Path::new), windows)
.expect("entries compile")
}
fn compile_err(entry: &str, workdir: &str, home: Option<&str>) -> String {
ReadPathSet::compile(
&[entry.to_string()],
Path::new(workdir),
home.map(Path::new),
false,
)
.expect_err("entry must be refused")
}
#[test]
fn empty_and_whitespace_entries_are_refused() {
assert!(compile_err("", "/w", None).contains("empty"));
assert!(compile_err(" ", "/w", None).contains("empty"));
assert!(compile_err("glob:", "/w", None).contains("glob pattern is empty"));
assert!(compile_err("regex:", "/w", None).contains("regex pattern is empty"));
}
#[test]
fn invalid_patterns_are_refused() {
assert!(compile_err("glob:/a/[", "/w", None).contains("invalid glob"));
assert!(compile_err("regex:/a/(", "/w", None).contains("invalid regex"));
}
#[test]
fn a_relative_regex_is_refused() {
let err = compile_err("regex:etc/passwd", "/w", None);
assert!(err.contains("must start with"), "got: {err}");
assert!(err.contains("glob:"), "got: {err}");
}
#[test]
fn tilde_user_forms_are_refused() {
for entry in ["~other/x", "glob:~other/**", "regex:~other/.*"] {
let err = compile_err(entry, "/w", Some("/home/me"));
assert!(err.contains("only '~/'"), "{entry}: {err}");
}
}
#[test]
fn tilde_without_a_home_is_refused() {
for entry in ["~/docs", "glob:~/docs/**", "regex:~/docs/.*"] {
let err = compile_err(entry, "/w", None);
assert!(err.contains("no home directory"), "{entry}: {err}");
}
}
#[test]
fn interior_dot_components_in_globs_are_refused() {
for entry in ["glob:/a/../b/**", "glob:/a/./b", "glob:a/../../b"] {
let err = compile_err(entry, "/w/x", None);
assert!(err.contains("cannot contain"), "{entry}: {err}");
}
}
#[test]
fn a_relative_glob_cannot_climb_past_the_root() {
let err = compile_err("glob:../../../x/**", "/w", None);
assert!(err.contains("escapes the filesystem root"), "got: {err}");
}
#[test]
fn an_exact_root_grants_its_subtree_and_nothing_else() {
let dir = tempfile::tempdir().unwrap();
let root = std::fs::canonicalize(dir.path()).unwrap();
std::fs::create_dir(root.join("sub")).unwrap();
std::fs::write(root.join("sub/f.txt"), b"x").unwrap();
let outside = tempfile::tempdir().unwrap();
let outside_file = outside.path().join("f.txt");
std::fs::write(&outside_file, b"x").unwrap();
let s = set(&[root.to_str().unwrap()], "/w", None, false);
assert!(s.matches(&root.join("sub/f.txt")));
assert!(!s.matches(&std::fs::canonicalize(&outside_file).unwrap()));
}
#[test]
fn an_uncanonicalized_exact_root_still_matches() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("f.txt"), b"x").unwrap();
let s = set(&[dir.path().to_str().unwrap()], "/w", None, false);
assert!(s.matches(&std::fs::canonicalize(dir.path().join("f.txt")).unwrap()));
}
#[test]
fn a_nonexistent_exact_root_never_matches() {
let dir = tempfile::tempdir().unwrap();
let real = std::fs::canonicalize(dir.path()).unwrap();
let s = set(&["/definitely/not/a/real/root"], "/w", None, false);
assert!(!s.matches(&real));
}
#[test]
fn a_relative_exact_entry_resolves_against_the_workdir() {
let parent = tempfile::tempdir().unwrap();
let workdir = parent.path().join("work");
let sibling = parent.path().join("shared");
std::fs::create_dir_all(&workdir).unwrap();
std::fs::create_dir_all(&sibling).unwrap();
std::fs::write(sibling.join("doc.md"), b"x").unwrap();
let s = set(&["../shared"], workdir.to_str().unwrap(), None, false);
assert!(s.matches(&std::fs::canonicalize(sibling.join("doc.md")).unwrap()));
}
#[test]
fn tilde_exact_entries_expand_to_the_home_argument() {
let home = tempfile::tempdir().unwrap();
std::fs::create_dir(home.path().join("docs")).unwrap();
std::fs::write(home.path().join("docs/a.md"), b"x").unwrap();
let home_str = home.path().to_str().unwrap();
let bare = set(&["~"], "/w", Some(home_str), false);
let scoped = set(&["~/docs"], "/w", Some(home_str), false);
let canonical = std::fs::canonicalize(home.path().join("docs/a.md")).unwrap();
assert!(bare.matches(&canonical));
assert!(scoped.matches(&canonical));
}
#[test]
fn star_stays_within_one_component_and_doublestar_crosses() {
let s = set(&["glob:/data/runs/*"], "/w", None, false);
assert!(s.matches(Path::new("/data/runs/r1")));
assert!(!s.matches(Path::new("/data/runs/r1/log.txt")));
let deep = set(&["glob:/data/runs/**"], "/w", None, false);
assert!(deep.matches(Path::new("/data/runs/r1/log.txt")));
assert!(!deep.matches(Path::new("/data/other/x")));
}
#[test]
fn a_relative_glob_is_anchored_at_the_workdir() {
let s = set(&["glob:../shared/**"], "/w/agent", None, false);
assert!(s.matches(Path::new("/w/shared/notes/a.md")));
assert!(!s.matches(Path::new("/w/agent/own.md")));
assert!(!s.matches(Path::new("/elsewhere/shared/a.md")));
}
#[test]
fn a_relative_glob_works_from_a_root_workdir() {
let s = set(&["glob:docs/**"], "/", None, false);
assert!(s.matches(Path::new("/docs/a.md")));
assert!(!s.matches(Path::new("/other/a.md")));
}
#[test]
fn a_dots_only_glob_matches_the_folded_directory_itself() {
let s = set(&["glob:../"], "/a/b", None, false);
assert!(s.matches(Path::new("/a")));
assert!(!s.matches(Path::new("/a/b")));
}
#[test]
fn a_metachar_workdir_is_escaped_in_relative_globs() {
let s = set(&["glob:./docs/**"], "/we[ird]/w", None, false);
assert!(s.matches(Path::new("/we[ird]/w/docs/a.md")));
assert!(!s.matches(Path::new("/wei/w/docs/a.md")));
}
#[test]
fn a_metachar_home_is_escaped_in_tilde_globs() {
let s = set(&["glob:~/docs/**"], "/w", Some("/ho[me]"), false);
assert!(s.matches(Path::new("/ho[me]/docs/a.md")));
assert!(!s.matches(Path::new("/hom/docs/a.md")));
}
#[test]
fn backslash_glob_patterns_are_normalized() {
let s = set(&[r"glob:C:\data\runs\**"], "/w", None, true);
assert!(s.matches(Path::new(r"C:\data\runs\r1\log.txt")));
}
#[test]
fn glob_case_sensitivity_follows_the_platform_flag() {
let insensitive = set(&["glob:/Data/**"], "/w", None, true);
assert!(insensitive.matches(Path::new("/data/x")));
let sensitive = set(&["glob:/Data/**"], "/w", None, false);
assert!(!sensitive.matches(Path::new("/data/x")));
}
#[test]
fn regexes_are_anchored_to_the_whole_path() {
let s = set(&["regex:/etc/runs"], "/w", None, false);
assert!(s.matches(Path::new("/etc/runs")));
assert!(!s.matches(Path::new("/etc/runs-anything")));
assert!(!s.matches(Path::new("/prefix/etc/runs")));
let subtree = set(&["regex:/etc/runs/.*"], "/w", None, false);
assert!(subtree.matches(Path::new("/etc/runs/deep/file")));
}
#[test]
fn regex_case_sensitivity_follows_the_platform_flag() {
let insensitive = set(&["regex:/Data/.*"], "/w", None, true);
assert!(insensitive.matches(Path::new("/data/x")));
let sensitive = set(&["regex:/Data/.*"], "/w", None, false);
assert!(!sensitive.matches(Path::new("/data/x")));
}
#[test]
fn a_metachar_home_is_escaped_in_tilde_regexes() {
let s = set(&["regex:~/docs/.*"], "/w", Some("/ho.me"), false);
assert!(s.matches(Path::new("/ho.me/docs/a")));
assert!(!s.matches(Path::new("/hoXme/docs/a")));
}
#[test]
fn a_drive_letter_regex_is_accepted() {
let s = set(&["regex:C:/data/.*"], "/w", None, true);
assert!(s.matches(Path::new(r"C:\data\x")));
}
#[test]
fn unix_strings_pass_through_untouched() {
assert_eq!(
normalize_match_str(r"/a/weird\name", false),
r"/a/weird\name"
);
}
#[test]
fn windows_verbatim_prefixes_are_stripped_for_matching() {
assert_eq!(normalize_match_str(r"\\?\C:\Users\x", true), "C:/Users/x");
assert_eq!(
normalize_match_str(r"\\?\UNC\srv\share\x", true),
"//srv/share/x"
);
assert_eq!(
normalize_match_str(r"\\?\Volume{abc}\x", true),
"//?/Volume{abc}/x"
);
assert_eq!(normalize_match_str(r"C:\plain\x", true), "C:/plain/x");
}
fn policy(blueprint: &[&str], grants: &[&str], allow_blueprint: bool) -> ReadPathPolicy {
ReadPathPolicy {
agent: "tester".into(),
blueprint: set(blueprint, "/w", None, false),
grants: set(grants, "/w", None, false),
allow_blueprint,
}
}
#[test]
fn an_inactive_policy_declares_nothing() {
let p = ReadPathPolicy::inactive();
assert!(!p.is_active());
assert_eq!(
p.decide(Path::new("/anything")),
ReadPathDecision::NotDeclared
);
}
#[test]
fn a_path_the_blueprint_never_declared_is_not_declared() {
let p = policy(&["glob:/data/**"], &["glob:/data/**"], false);
assert!(p.is_active());
assert_eq!(
p.decide(Path::new("/etc/passwd")),
ReadPathDecision::NotDeclared
);
}
#[test]
fn a_declared_but_ungranted_path_is_not_granted() {
let p = policy(&["glob:/data/**"], &[], false);
assert_eq!(p.decide(Path::new("/data/x")), ReadPathDecision::NotGranted);
}
#[test]
fn a_granted_path_is_allowed() {
let p = policy(&["glob:/data/**"], &["glob:/data/**"], false);
assert_eq!(p.decide(Path::new("/data/x")), ReadPathDecision::Allowed);
}
#[test]
fn a_broader_grant_covers_a_narrow_declaration() {
let p = policy(&["glob:/data/runs/**"], &["glob:/data/**"], false);
assert_eq!(
p.decide(Path::new("/data/runs/r1")),
ReadPathDecision::Allowed
);
}
#[test]
fn a_nonoverlapping_grant_does_not_help() {
let p = policy(&["glob:/data/**"], &["glob:/other/**"], false);
assert_eq!(p.decide(Path::new("/data/x")), ReadPathDecision::NotGranted);
}
#[test]
fn the_blanket_override_honors_declarations_without_grants() {
let p = policy(&["glob:/data/**"], &[], true);
assert_eq!(p.decide(Path::new("/data/x")), ReadPathDecision::Allowed);
assert_eq!(p.decide(Path::new("/etc/x")), ReadPathDecision::NotDeclared);
}
#[test]
fn an_empty_set_matches_nothing() {
let s = set(&[], "/w", None, false);
assert!(s.is_empty());
assert!(s.entries().is_empty());
assert!(!s.matches(Path::new("/anything")));
}
#[test]
fn syntax_validation_accepts_well_formed_entries() {
for entry in [
"/abs/dir",
"relative/dir",
"~/docs",
"glob:~/runs/**",
"glob:../shared/**",
"regex:/data/.*",
r"C:\Users\me\docs",
] {
assert!(validate_entry_syntax(entry).is_ok(), "{entry}");
}
}
#[test]
fn syntax_validation_refuses_malformed_entries() {
for entry in ["", "glob:[", "regex:(", "regex:relative/.*", "~oops"] {
assert!(validate_entry_syntax(entry).is_err(), "{entry}");
}
}
fn sample_path_of(entry: &str, workdir: &str, home: Option<&str>) -> Option<PathBuf> {
set(&[entry], workdir, home, false).entries()[0].sample_path()
}
fn sample(entry: &str, workdir: &str, home: Option<&str>) -> Option<String> {
sample_path_of(entry, workdir, home).map(|p| p.to_string_lossy().into_owned())
}
#[test]
fn an_exact_entry_samples_as_its_own_root() {
assert_eq!(
sample("/data/runs", "/w", None).as_deref(),
Some("/data/runs")
);
assert_eq!(
sample_path_of("~/docs", "/w", Some("/home/me")),
Some(Path::new("/home/me").join("docs"))
);
assert_eq!(
sample_path_of("../shared", "/w/run", None),
Some(Path::new("/w/run").join("../shared"))
);
}
#[test]
fn glob_wildcards_are_filled_with_a_literal_component() {
assert_eq!(
sample("glob:/data/**", "/w", None).as_deref(),
Some("/data/_leviath_probe")
);
assert_eq!(
sample("glob:/data/*/notes", "/w", None).as_deref(),
Some("/data/_leviath_probe/notes")
);
assert_eq!(
sample("glob:/data/log?", "/w", None).as_deref(),
Some("/data/logx")
);
assert_eq!(
sample("glob:/data/notes", "/w", None).as_deref(),
Some("/data/notes")
);
}
#[test]
fn a_glob_character_class_has_no_sample() {
assert_eq!(sample("glob:/data/[abc]/x", "/w", None), None);
}
#[test]
fn a_sample_that_fails_its_own_pattern_is_refused() {
let entry = ReadPathEntry::Glob {
pattern: glob::Pattern::new("/data/*").expect("pattern compiles"),
options: glob::MatchOptions {
case_sensitive: true,
require_literal_separator: true,
require_literal_leading_dot: false,
},
};
let literal_star = ReadPathEntry::Glob {
pattern: glob::Pattern::new("/data/[*]").expect("pattern compiles"),
options: glob::MatchOptions {
case_sensitive: true,
require_literal_separator: true,
require_literal_leading_dot: false,
},
};
assert!(entry.sample_path().is_some());
assert_eq!(literal_star.sample_path(), None);
}
#[test]
fn a_regex_samples_a_file_inside_its_literal_prefix() {
assert_eq!(
sample("regex:/data/archives/.*", "/w", None).as_deref(),
Some("/data/archives/_leviath_probe")
);
assert_eq!(
sample("regex:/data/archives", "/w", None).as_deref(),
Some("/data/archives")
);
assert_eq!(
sample("regex:~/runs/.*", "/w", Some("/home/me")).as_deref(),
Some("/home/me/runs/_leviath_probe")
);
}
#[test]
fn a_regex_with_no_usable_literal_prefix_has_no_sample() {
let entry =
ReadPathEntry::Regex(regex::Regex::new("^(?:[/a-z]+)$").expect("regex compiles"));
assert_eq!(entry.sample_path(), None);
}
#[test]
fn an_unanchored_regex_is_read_as_written() {
let entry = ReadPathEntry::Regex(regex::Regex::new("/data/x.*").expect("regex compiles"));
assert_eq!(entry.sample_path(), Some(PathBuf::from("/data/x")));
}
#[test]
fn lexical_matching_covers_a_root_and_its_subtree() {
let s = set(&["/data/runs"], "/w", None, false);
assert!(s.matches_lexically(Path::new("/data/runs")));
assert!(s.matches_lexically(Path::new("/data/runs/june/1")));
assert!(!s.matches_lexically(Path::new("/data/runs-old/1")));
assert!(!s.matches_lexically(Path::new("/data")));
}
#[test]
fn lexical_matching_does_not_need_the_root_to_exist() {
let s = set(&["/definitely/not/here"], "/w", None, false);
assert!(s.matches_lexically(Path::new("/definitely/not/here/x")));
assert!(!s.matches(Path::new("/definitely/not/here/x")));
}
#[test]
fn lexical_matching_folds_case_under_windows_semantics() {
let windows = set(&["/Users/Me/docs"], "/w", None, true);
assert!(windows.matches_lexically(Path::new("/users/me/docs/notes.md")));
let unix = set(&["/Users/Me/docs"], "/w", None, false);
assert!(!unix.matches_lexically(Path::new("/users/me/docs/notes.md")));
}
#[test]
fn lexical_matching_handles_a_root_entry() {
let s = set(&["/"], "/w", None, false);
assert!(s.matches_lexically(Path::new("/etc/passwd")));
assert!(s.matches_lexically(Path::new("/")));
}
#[test]
fn lexical_matching_uses_the_pattern_entries_unchanged() {
let s = set(&["glob:/data/**", "regex:/logs/.*"], "/w", None, false);
assert!(s.matches_lexically(Path::new("/data/x/y")));
assert!(s.matches_lexically(Path::new("/logs/today")));
assert!(!s.matches_lexically(Path::new("/elsewhere/x")));
}
}