use ferralk_glob::PatternError;
use crate::first_metacharacter;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Rewrite {
Relative,
Rooted(Vec<u8>),
Outside,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Syntax {
Posix,
Windows,
}
impl Syntax {
pub(crate) const NATIVE: Self = if cfg!(windows) {
Self::Windows
} else {
Self::Posix
};
}
pub(crate) fn rewrite(pattern: &[u8], root: &[u8]) -> Result<Rewrite, PatternError> {
rewrite_in(pattern, root, Syntax::NATIVE)
}
pub(crate) fn rewrite_in(
pattern: &[u8],
root: &[u8],
syntax: Syntax,
) -> Result<Rewrite, PatternError> {
let Some(pattern_prefix) = absolute_prefix(pattern, syntax) else {
return Ok(Rewrite::Relative);
};
let Some(root_prefix) = absolute_prefix(root, syntax) else {
return Err(PatternError::new(
0,
"an absolute pattern needs an absolute walk root",
));
};
if !roots_agree(&pattern[..pattern_prefix], &root[..root_prefix]) {
return Ok(Rewrite::Outside);
}
if let Some(at) = dot_dot_component(pattern) {
return Err(PatternError::new(
at,
"`..` in an absolute pattern is not resolved, because resolving it lexically would be wrong across a symlink",
));
}
let magic = first_metacharacter(pattern);
let mut offset = pattern_prefix;
for root_component in components(&root[root_prefix..]) {
if root_component == b".." {
return Err(PatternError::new(
0,
"an absolute pattern needs a walk root without a `..` component",
));
}
loop {
let Some((component, next)) = next_component(pattern, offset) else {
return Ok(Rewrite::Outside);
};
if component.is_empty() || component == b"." {
offset = next;
continue;
}
if magic.is_some_and(|at| at < offset + component.len()) {
return Err(PatternError::new(
offset,
"a wildcard at or above the walk root cannot be made relative to it",
));
}
if component != root_component {
return Ok(Rewrite::Outside);
}
offset = next;
break;
}
}
while let Some((component, next)) = next_component(pattern, offset) {
if !component.is_empty() && component != b"." {
break;
}
offset = next;
}
let remainder = &pattern[offset..];
if remainder.is_empty() {
return Err(PatternError::new(
offset,
"an absolute pattern that names the walk root itself selects nothing; add `/**` to select what is inside it",
));
}
Ok(Rewrite::Rooted(remainder.to_vec()))
}
fn absolute_prefix(bytes: &[u8], syntax: Syntax) -> Option<usize> {
match syntax {
Syntax::Posix => bytes.starts_with(b"/").then_some(1),
Syntax::Windows => {
if bytes.starts_with(b"//") {
return Some(2);
}
let drive = bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& bytes[2] == b'/';
drive.then_some(3)
}
}
}
fn roots_agree(pattern_prefix: &[u8], root_prefix: &[u8]) -> bool {
pattern_prefix.eq_ignore_ascii_case(root_prefix)
}
fn components(bytes: &[u8]) -> impl Iterator<Item = &[u8]> {
bytes
.split(|byte| *byte == b'/')
.filter(|component| !component.is_empty() && *component != b".")
}
fn next_component(pattern: &[u8], offset: usize) -> Option<(&[u8], usize)> {
if offset >= pattern.len() {
return None;
}
let rest = &pattern[offset..];
match rest.iter().position(|byte| *byte == b'/') {
Some(separator) => Some((&rest[..separator], offset + separator + 1)),
None => Some((rest, pattern.len())),
}
}
fn dot_dot_component(pattern: &[u8]) -> Option<usize> {
let mut offset = 0;
while let Some((component, next)) = next_component(pattern, offset) {
if component == b".." {
return Some(offset);
}
offset = next;
}
None
}
#[cfg(test)]
mod tests {
use super::{Rewrite, Syntax, rewrite_in};
fn rooted(pattern: &str, root: &str, syntax: Syntax) -> String {
match rewrite_in(pattern.as_bytes(), root.as_bytes(), syntax) {
Ok(Rewrite::Rooted(bytes)) => String::from_utf8(bytes).expect("ASCII fixture"),
other => panic!("expected a rewrite, got {other:?}"),
}
}
fn verdict(pattern: &str, root: &str, syntax: Syntax) -> Rewrite {
rewrite_in(pattern.as_bytes(), root.as_bytes(), syntax).expect("no error expected")
}
fn message(pattern: &str, root: &str, syntax: Syntax) -> &'static str {
rewrite_in(pattern.as_bytes(), root.as_bytes(), syntax)
.expect_err("an error was expected")
.message()
}
#[test]
fn a_pattern_under_the_root_loses_exactly_the_root() {
assert_eq!(rooted("/a/b/src/*.ts", "/a/b", Syntax::Posix), "src/*.ts");
assert_eq!(rooted("/a/b/**/*.ts", "/a/b", Syntax::Posix), "**/*.ts");
assert_eq!(rooted("/a/b/**", "/a/b", Syntax::Posix), "**");
assert_eq!(
rooted("/a/b/{src,lib}/**/*.ts", "/a/b", Syntax::Posix),
"{src,lib}/**/*.ts"
);
assert_eq!(rooted("/a/b/build/", "/a/b", Syntax::Posix), "build/");
}
#[test]
fn separator_noise_on_either_side_is_ignored() {
assert_eq!(rooted("/a//b/src/*.ts", "/a/b", Syntax::Posix), "src/*.ts");
assert_eq!(rooted("/a/./b/src/*.ts", "/a/b", Syntax::Posix), "src/*.ts");
assert_eq!(rooted("/a/b/src/*.ts", "/a/b/", Syntax::Posix), "src/*.ts");
assert_eq!(
rooted("/a/b/src/*.ts", "/a//b//", Syntax::Posix),
"src/*.ts"
);
assert_eq!(rooted("/a/b//src/*.ts", "/a/b", Syntax::Posix), "src/*.ts");
assert_eq!(rooted("/a/b/./src/*.ts", "/a/b", Syntax::Posix), "src/*.ts");
assert_eq!(rooted("//a/b/src/*.ts", "/a/b", Syntax::Posix), "src/*.ts");
}
#[test]
fn a_pattern_that_cannot_reach_the_root_selects_nothing() {
assert_eq!(verdict("/a/c/**", "/a/b", Syntax::Posix), Rewrite::Outside);
assert_eq!(
verdict("/other/**", "/a/b", Syntax::Posix),
Rewrite::Outside
);
assert_eq!(verdict("/a", "/a/b", Syntax::Posix), Rewrite::Outside);
assert_eq!(verdict("/a/bb/**", "/a/b", Syntax::Posix), Rewrite::Outside);
}
#[test]
fn a_relative_pattern_is_left_alone() {
assert_eq!(
verdict("src/*.ts", "/a/b", Syntax::Posix),
Rewrite::Relative
);
assert_eq!(verdict("**/*.ts", "/a/b", Syntax::Posix), Rewrite::Relative);
assert_eq!(
verdict("/src/*.ts", "C:/a/b", Syntax::Windows),
Rewrite::Relative
);
}
#[test]
fn a_wildcard_at_or_above_the_root_is_rejected() {
assert_eq!(
message("/*/x.ts", "/a/b", Syntax::Posix),
"a wildcard at or above the walk root cannot be made relative to it"
);
assert_eq!(
message("/**/*.ts", "/a/b", Syntax::Posix),
"a wildcard at or above the walk root cannot be made relative to it"
);
assert_eq!(
message("/a/b*/x.ts", "/a/b", Syntax::Posix),
"a wildcard at or above the walk root cannot be made relative to it"
);
assert_eq!(rooted("/a/b/*/x.ts", "/a/b", Syntax::Posix), "*/x.ts");
}
#[test]
fn dot_dot_is_rejected_on_both_sides() {
assert!(message("/a/b/../b/x.ts", "/a/b", Syntax::Posix).starts_with("`..`"));
assert_eq!(
message("/a/b/x.ts", "/a/b/../b", Syntax::Posix),
"an absolute pattern needs a walk root without a `..` component"
);
assert!(message("/a/b/../c/*.ts", "/a", Syntax::Posix).starts_with("`..`"));
}
#[test]
fn naming_the_root_itself_is_rejected() {
for pattern in ["/a/b", "/a/b/", "/a/b/.", "/a//b//"] {
assert!(
message(pattern, "/a/b", Syntax::Posix)
.starts_with("an absolute pattern that names"),
"{pattern} names the root"
);
}
}
#[test]
fn an_absolute_pattern_needs_an_absolute_root() {
assert_eq!(
message("/a/b/*.ts", "relative/dir", Syntax::Posix),
"an absolute pattern needs an absolute walk root"
);
assert_eq!(
message("C:/a/*.ts", "a/b", Syntax::Windows),
"an absolute pattern needs an absolute walk root"
);
}
#[test]
fn windows_roots_are_read_the_way_windows_spells_them() {
assert_eq!(
rooted("C:/a/b/src/*.ts", "C:/a/b", Syntax::Windows),
"src/*.ts"
);
assert_eq!(
rooted("c:/a/b/src/*.ts", "C:/a/b", Syntax::Windows),
"src/*.ts"
);
assert_eq!(
verdict("C:/a/B/src/*.ts", "C:/a/b", Syntax::Windows),
Rewrite::Outside
);
assert_eq!(
verdict("D:/a/b/**", "C:/a/b", Syntax::Windows),
Rewrite::Outside
);
assert_eq!(
rooted("//host/share/a/**/*.ts", "//host/share/a", Syntax::Windows),
"**/*.ts"
);
assert_eq!(
verdict("//host/other/a/**", "//host/share/a", Syntax::Windows),
Rewrite::Outside
);
assert_eq!(
message("C:/a\\b\\src\\*.ts", "C:/a/b", Syntax::Windows),
"a wildcard at or above the walk root cannot be made relative to it"
);
}
}