use alloc::vec::Vec;
#[derive(Debug, Clone, Default)]
pub struct Ignore {
patterns: Vec<Pattern>,
}
#[derive(Debug, Clone)]
struct Pattern {
negated: bool,
dir_only: bool,
anchored: bool,
toks: Vec<Vec<u8>>,
}
impl Ignore {
pub fn new() -> Self {
Ignore {
patterns: Vec::new(),
}
}
pub fn add_bytes(&mut self, data: &[u8]) {
for line in data.split(|&b| b == b'\n') {
self.add_line(line);
}
}
fn add_line(&mut self, raw: &[u8]) {
let mut line = raw;
if line.last() == Some(&b'\r') {
line = &line[..line.len() - 1];
}
while line.last() == Some(&b' ') {
line = &line[..line.len() - 1];
}
if line.is_empty() || line[0] == b'#' {
return;
}
let mut negated = false;
if line[0] == b'!' {
negated = true;
line = &line[1..];
}
let mut dir_only = false;
if line.last() == Some(&b'/') {
dir_only = true;
line = &line[..line.len() - 1];
}
let leading_slash = line.first() == Some(&b'/');
if leading_slash {
line = &line[1..];
}
let anchored = leading_slash || line.contains(&b'/');
let toks: Vec<Vec<u8>> = line
.split(|&b| b == b'/')
.filter(|s| !s.is_empty())
.map(|s| s.to_vec())
.collect();
if toks.is_empty() {
return;
}
self.patterns.push(Pattern {
negated,
dir_only,
anchored,
toks,
});
}
pub fn is_empty(&self) -> bool {
self.patterns.is_empty()
}
pub fn is_ignored(&self, path: &[u8]) -> bool {
let comps: Vec<&[u8]> = path
.split(|&b| b == b'/')
.filter(|s| !s.is_empty())
.collect();
if comps.is_empty() {
return false;
}
let mut ignored = false;
for p in &self.patterns {
if p.matches(&comps) {
ignored = !p.negated;
}
}
ignored
}
}
impl Pattern {
fn matches(&self, comps: &[&[u8]]) -> bool {
if self.anchored {
self.match_anchored(comps)
} else {
let tok = &self.toks[0];
let limit = if self.dir_only {
comps.len().saturating_sub(1)
} else {
comps.len()
};
comps[..limit].iter().any(|c| glob_seg(tok, c))
}
}
fn match_anchored(&self, comps: &[&[u8]]) -> bool {
let t = self.toks.len();
let c = comps.len();
let width = c + 1;
let mut reach = alloc::vec![false; (t + 1) * width];
reach[0] = true; for i in 0..=t {
for j in 0..=c {
if !reach[i * width + j] {
continue;
}
if i < t {
if self.toks[i] == b"**" {
reach[(i + 1) * width + j] = true; if j < c {
reach[i * width + (j + 1)] = true; }
} else if j < c && glob_seg(&self.toks[i], comps[j]) {
reach[(i + 1) * width + (j + 1)] = true;
}
}
}
}
for j in 0..=c {
if reach[t * width + j] && (!self.dir_only || j < c) {
return true;
}
}
false
}
}
fn glob_seg(pat: &[u8], text: &[u8]) -> bool {
let (mut p, mut t) = (0usize, 0usize);
let (mut star_p, mut star_t): (Option<usize>, usize) = (None, 0);
while t < text.len() {
if p < pat.len() && (pat[p] == b'?' || pat[p] == text[t]) {
p += 1;
t += 1;
} else if p < pat.len() && pat[p] == b'*' {
star_p = Some(p);
star_t = t;
p += 1;
} else if let Some(sp) = star_p {
p = sp + 1;
star_t += 1;
t = star_t;
} else {
return false;
}
}
while p < pat.len() && pat[p] == b'*' {
p += 1;
}
p == pat.len()
}
#[cfg(test)]
mod tests {
use super::*;
fn ig(rules: &str) -> Ignore {
let mut i = Ignore::new();
i.add_bytes(rules.as_bytes());
i
}
#[test]
fn floating_basename_and_extension() {
let i = ig("*.o\nfoo\n");
assert!(i.is_ignored(b"a.o"));
assert!(i.is_ignored(b"sub/dir/a.o"));
assert!(i.is_ignored(b"foo"));
assert!(i.is_ignored(b"x/foo"));
assert!(!i.is_ignored(b"a.c"));
assert!(!i.is_ignored(b"foobar"));
}
#[test]
fn anchored_and_directory_only() {
let i = ig("/build\ntarget/\n");
assert!(i.is_ignored(b"build")); assert!(i.is_ignored(b"build/out.bin"));
assert!(!i.is_ignored(b"src/build")); assert!(i.is_ignored(b"target/debug/app")); assert!(!i.is_ignored(b"target")); }
#[test]
fn double_star_and_interior_slash() {
let i = ig("**/gen\na/**/z\n");
assert!(i.is_ignored(b"gen"));
assert!(i.is_ignored(b"x/y/gen"));
assert!(i.is_ignored(b"a/z"));
assert!(i.is_ignored(b"a/b/c/z"));
assert!(i.is_ignored(b"a/z/keep"));
assert!(!i.is_ignored(b"a/b/y"));
}
#[test]
fn negation_reincludes() {
let i = ig("*.log\n!keep.log\n");
assert!(i.is_ignored(b"debug.log"));
assert!(!i.is_ignored(b"keep.log"));
}
#[test]
fn comments_and_blanks_skipped() {
let i = ig("# a comment\n\n \n*.tmp\n");
assert!(i.is_ignored(b"x.tmp"));
assert!(!i.is_ignored(b"x.txt"));
}
}