use super::{at, isshexp, iswild, matches, namecmp, recmatch, to_lower};
fn m(p: &str, s: &str) -> bool {
matches(p.as_bytes(), s.as_bytes(), false, false)
}
#[test]
fn star_matches_any_run_including_slashes() {
assert!(m("*", ""));
assert!(m("*", "a/b/c"));
assert!(m("*.txt", "dir/a.txt"));
assert!(m("a*", "a"));
assert!(!m("*.txt", "a.txz"));
assert!(m("a*b*c", "aXXbYYc"));
assert!(!m("a*b*c", "aXXbYY"));
assert!(m("*abc", "abcabc"));
assert!(!m("*abcd", "abc"));
}
#[test]
fn a_double_star_needs_a_byte_without_wild_stop_at_dir() {
assert!(m("a*", "a"));
assert!(!m("a**", "a"));
assert!(m("a**", "ab"));
}
#[test]
fn question_mark_is_one_byte() {
assert!(m("?", "a"));
assert!(!m("?", ""));
assert!(!m("?", "ab"));
assert!(m("a?c", "a/c"));
}
#[test]
fn brackets_sets_ranges_and_complements() {
assert!(m("[abc]", "b"));
assert!(!m("[abc]", "d"));
assert!(m("[a-c]x", "bx"));
assert!(!m("[a-c]x", "dx"));
assert!(m("[!a-c]", "d"));
assert!(m("[^a-c]", "d"));
assert!(!m("[!a-c]", "b"));
assert!(m("[-a]", "-"));
assert!(m("[a\\-c]", "-"));
assert!(!m("[a\\-c]", "b"));
assert!(!m("[c-a]", "b"));
assert!(!m("[abc", "a"));
assert!(!m("[abc", "[abc"));
assert!(!m("[a]", ""));
assert!(!m("[]]", "]"));
}
#[test]
fn backslash_makes_the_next_byte_literal() {
assert!(m("\\*", "*"));
assert!(!m("\\*", "a"));
assert!(m("a\\?", "a?"));
assert!(!m("a\\?", "ab"));
assert!(!m("a\\", "a"));
assert!(!m("a\\", "a\\"));
}
#[test]
fn case_folding_is_ascii_only() {
assert!(!m("ABC", "abc"));
assert!(matches(b"ABC", b"abc", true, false));
assert!(matches(b"*.TXT", b"a.txt", true, false));
assert!(matches(b"[A-C]", b"b", true, false));
assert!(!matches(b"\xc9", b"\xe9", true, false));
}
#[test]
fn wild_stop_at_dir_keeps_star_and_question_mark_inside_a_component() {
let w = |p: &str, s: &str| matches(p.as_bytes(), s.as_bytes(), false, true);
assert!(!w("*", "a/b"));
assert!(w("*/b", "a/b"));
assert!(w("**", "a/b"));
assert!(w("**.txt", "d/e/f.txt"));
assert!(!w("*.txt", "d/f.txt"));
assert!(!w("a?b", "a/b"));
assert!(w("a?b", "axb"));
assert!(w("*\\/b", "a/b"));
assert!(m("*", "a/b"));
assert!(m("a?b", "a/b"));
}
#[test]
fn names_are_c_strings() {
assert!(m("ab", "ab\0cd"));
assert!(m("ab", "ab\0"));
}
#[test]
fn iswild_finds_unescaped_wildcards() {
assert!(iswild(b"*.zip"));
assert!(iswild(b"a?"));
assert!(iswild(b"[ab]"));
assert!(!iswild(b"a.zip"));
assert!(!iswild(b"\\*.zip"));
assert!(iswild(b"\\\\*"));
assert!(!iswild(b"a\\"));
}
fn recursive(mut p: &[u8], mut s: &[u8], ic: bool, sepc: u8) -> i32 {
let case = |x: u8| if ic { to_lower(x) } else { x };
loop {
let c = at(p, 0);
p = p.get(1..).unwrap_or(&[]);
if c == 0 {
return i32::from(at(s, 0) == 0);
}
if c == b'?' {
if at(s, 0) != 0 && (sepc == 0 || at(s, 0) != sepc) {
s = &s[1..];
continue;
}
return 0;
}
if c == b'*' {
if sepc != 0 {
if at(p, 0) != b'*' {
while at(s, 0) != 0 && at(s, 0) != sepc {
let r = recursive(p, s, ic, sepc);
if r != 0 {
return r;
}
s = &s[1..];
}
if at(p, 0) == 0 {
return i32::from(at(s, 0) == 0);
}
if at(p, 0) == sepc || (at(p, 0) == b'\\' && at(p, 1) == sepc) {
continue;
}
return 2;
}
p = &p[1..];
}
if at(p, 0) == 0 {
return 1;
}
if isshexp(p).is_none() {
if s.len() < p.len() {
return 0;
}
let srest = &s[s.len() - p.len()..];
return i32::from(if ic {
namecmp(p, srest) == 0
} else {
p == srest
});
}
while at(s, 0) != 0 {
let r = recursive(p, s, ic, sepc);
if r != 0 {
return r;
}
s = &s[1..];
}
return 2; }
if c == b'[' {
if at(s, 0) == 0 {
return 0;
}
let r = at(p, 0) == b'!' || at(p, 0) == b'^'; if r {
p = &p[1..];
}
let mut q = 0usize;
let mut e = false;
while at(p, q) != 0 {
if e {
e = false;
} else if at(p, q) == b'\\' {
e = true;
} else if at(p, q) == b']' {
break;
}
q += 1;
}
if at(p, q) != b']' {
return 0;
}
let mut cr: u32 = 0; let mut e = at(p, 0) == b'-';
let cc = u32::from(case(at(s, 0)));
for i in 0..q {
let pi = at(p, i);
if !e && pi == b'\\' {
e = true;
} else if !e && pi == b'-' {
cr = u32::from(at(p, i.wrapping_sub(1)));
} else {
if at(p, i + 1) != b'-' {
let mut c = if cr != 0 { cr } else { u32::from(pi) };
while c <= u32::from(pi) {
if u32::from(case(c as u8)) == cc {
if r {
return 0;
}
p = &p[q + 1..];
s = &s[1..];
return recursive(p, s, ic, sepc);
}
c += 1;
}
}
cr = 0; e = false;
}
}
if r {
p = &p[q + 1..];
s = &s[1..];
continue;
}
return 0;
}
let c = if c == b'\\' {
let n = at(p, 0);
if n == 0 {
return 0;
}
p = &p[1..];
n
} else {
c
};
if case(c) == case(at(s, 0)) && at(s, 0) != 0 {
s = &s[1..];
continue;
}
return 0;
}
}
#[test]
fn the_stack_free_recmatch_answers_as_the_recursive_one() {
const ALPHA: &[u8] = b"**??[]!^-\\/aabAB";
let mut x: u64 = 0x9e37_79b9_7f4a_7c15;
let mut next = || {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x
};
let mut counts = [0usize; 3];
for _ in 0..200_000 {
let pl = (next() % 9) as usize;
let sl = (next() % 7) as usize;
let p: Vec<u8> = (0..pl)
.map(|_| ALPHA[(next() % ALPHA.len() as u64) as usize])
.collect();
let s: Vec<u8> = (0..sl)
.map(|_| b"aab/AB-]"[(next() % 8) as usize])
.collect();
let ic = next() % 2 == 0;
let sepc = if next() % 2 == 0 { b'/' } else { 0 };
let want = recursive(&p, &s, ic, sepc);
assert_eq!(
recmatch(&p, &s, ic, sepc),
want,
"pattern {:?} name {:?} ic {ic} sepc {sepc}",
String::from_utf8_lossy(&p),
String::from_utf8_lossy(&s)
);
counts[want as usize] += 1;
}
assert!(counts.iter().all(|&n| n > 1000), "{counts:?}");
}
#[test]
fn sixteen_thousand_stars_need_no_stack() {
let stars = vec![b'*'; 16_000];
let ok = std::thread::Builder::new()
.stack_size(2 << 20)
.spawn(move || {
(
matches(&stars, b"a.txt", false, false),
matches(&stars, b"dir/a.txt", false, true),
matches(&[&stars[..], b"a.txt"].concat(), b"a.txt", false, false),
matches(&b"*[a]".repeat(4000), &[b'a'; 4000], false, false),
)
})
.unwrap()
.join()
.unwrap();
assert_eq!(ok, (true, true, true, true));
}