#![cfg_attr(not(test), forbid(unsafe_code))]
use std::{borrow::Cow, cmp::Ordering};
use memchr::{
arch::all::{is_equal, is_prefix},
memchr, memchr2, memchr3, memmem,
};
use nix::NixPath;
use crate::{
likely,
path::{XPath, XPathBuf},
unlikely,
};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum MatchMethod {
Literal,
Prefix,
Glob,
}
pub fn contains(haystack: &[u8], needle: &[u8]) -> bool {
memmem::find(haystack, needle).is_some()
}
pub fn globmatch(pattern: &[u8], path: &[u8], method: MatchMethod) -> bool {
match method {
MatchMethod::Literal => litmatch(pattern, path),
MatchMethod::Prefix => prematch(pattern, path),
MatchMethod::Glob => wildmatch(pattern, path),
}
}
pub fn inamematch(pattern: &str, name: &str) -> bool {
let glob = if !is_literal(pattern.as_bytes()) {
Cow::Borrowed(pattern)
} else {
Cow::Owned(format!("*{pattern}*"))
};
wildmatch(
glob.to_ascii_lowercase().as_bytes(),
name.to_ascii_lowercase().as_bytes(),
)
}
#[inline]
pub fn is_literal(pattern: &[u8]) -> bool {
memchr3(b'*', b'?', b'[', pattern).is_none()
}
pub fn get_prefix(pattern: &XPath) -> Option<XPathBuf> {
if pattern.ends_with(b"/***") {
let len = pattern.len();
let pre = &pattern.as_bytes()[..len - "/***".len()];
if is_literal(pre) {
return Some(pre.into());
}
} else if pattern.ends_with(b"/**") {
let len = pattern.len();
let pre = &pattern.as_bytes()[..len - "**".len()];
if is_literal(pre) {
return Some(pre.into());
}
}
None
}
#[expect(clippy::disallowed_methods)]
pub fn get_match_method(pat: &mut XPathBuf) -> (MatchMethod, Option<XPathBuf>) {
if let Some(prefix) = get_prefix(pat) {
*pat = prefix;
(MatchMethod::Prefix, None)
} else if is_literal(pat.as_bytes()) {
(MatchMethod::Literal, None)
} else if pat.ends_with(b"/***") {
let len = pat.len();
let len0 = len.checked_sub(b"*".len()).unwrap();
let len1 = len.checked_sub(b"/***".len()).unwrap();
pat.truncate(len0); let split = pat.clone();
pat.truncate(len1); (MatchMethod::Glob, Some(split))
} else {
(MatchMethod::Glob, None)
}
}
pub fn litmatch(pattern: &[u8], path: &[u8]) -> bool {
is_equal(path, pattern)
}
pub fn prematch(pattern: &[u8], path: &[u8]) -> bool {
let len = pattern.len();
let ord = path.len().cmp(&len);
(ord == Ordering::Equal
|| (ord == Ordering::Greater && (pattern.last() == Some(&b'/') || path[len] == b'/')))
&& is_prefix(path, pattern)
}
#[expect(clippy::cognitive_complexity)]
pub fn wildmatch(pattern: &[u8], text: &[u8]) -> bool {
let mut idx = 0;
for (&p_ch, &t_ch) in pattern.iter().zip(text.iter()) {
if unlikely(matches!(p_ch, b'*' | b'[' | b'\\')) {
break;
}
if unlikely((p_ch != b'?' && p_ch != t_ch) || (p_ch != b'/' && t_ch == b'/')) {
return false;
}
idx += 1;
}
let p_len = pattern.len();
let t_len = text.len();
if unlikely(idx >= p_len) {
return idx >= t_len;
}
if likely(idx >= t_len) {
let mut p_idx = idx;
while let Some(p_ch) = pattern.get(p_idx) {
if p_ch == &b'*' {
p_idx += 1;
while pattern.get(p_idx) == Some(&b'*') {
p_idx += 1;
}
} else {
return false;
}
if pattern.get(p_idx) == Some(&b'/') {
for n in 1..=2 {
if p_idx
.checked_sub(n)
.map(|idx| pattern.get(idx) != Some(&b'*'))
.unwrap_or(false)
{
return false;
}
}
p_idx += 1;
}
}
return true;
}
let mut p_idx = idx;
let mut t_idx = idx;
struct BackupPoint {
p_idx: usize,
t_idx: usize,
}
let mut star_p: Option<BackupPoint> = None;
let mut globstar_p: Option<BackupPoint> = None;
let mut globstar_anchored = false;
loop {
if let Some(&p_ch) = pattern.get(p_idx) {
match p_ch {
b'*' => {
let is_double = pattern.get(p_idx + 1).map(|&b| b == b'*').unwrap_or(false);
if is_double {
p_idx += 2;
let anchored = p_idx
.checked_sub(3)
.map(|idx| {
pattern.get(idx) == Some(&b'/') && pattern.get(p_idx) == Some(&b'/')
})
.unwrap_or(false);
if anchored {
p_idx += 1; globstar_anchored = true;
} else {
globstar_anchored = false;
}
globstar_p = Some(BackupPoint { p_idx, t_idx });
star_p = None;
} else {
p_idx += 1;
match pattern.get(p_idx).copied() {
None | Some(b'*' | b'?' | b'[' | b'\\') => {
star_p = Some(BackupPoint { p_idx, t_idx });
}
Some(next_p) => {
star_p = if let Some(skip) = memchr2(next_p, b'/', &text[t_idx..]) {
if text[t_idx + skip] != b'/' {
t_idx += skip;
}
Some(BackupPoint { p_idx, t_idx })
} else if globstar_p.is_some() {
Some(BackupPoint { p_idx, t_idx })
} else {
return false;
};
continue;
}
}
}
if p_idx < p_len {
continue;
}
if is_double {
return true;
}
if memchr(b'/', &text[t_idx..]).is_none() {
return true;
}
if globstar_p.is_none() {
return false;
}
}
b'?' => {
if text.get(t_idx).map(|&b| b != b'/').unwrap_or(false) {
p_idx += 1;
t_idx += 1;
continue;
}
}
b'[' => match text.get(t_idx) {
None | Some(&b'/') => {}
Some(&t_ch) => {
if let Some(new_p) = classmatch(pattern, p_idx + 1, t_ch) {
p_idx = new_p;
t_idx += 1;
continue;
}
}
},
b'\\' => {
p_idx += 1;
if pattern
.get(p_idx)
.map(|p_ch| text.get(t_idx) == Some(p_ch))
.unwrap_or(false)
{
p_idx += 1;
t_idx += 1;
continue;
}
}
_ => {
if text.get(t_idx) == Some(&p_ch) {
p_idx += 1;
t_idx += 1;
continue;
}
}
}
}
if p_idx >= p_len && t_idx >= t_len {
return true;
}
if t_idx >= t_len {
while matches!(pattern.get(p_idx), Some(&b'*')) {
p_idx += 1;
}
return p_idx >= p_len;
}
if let Some(BackupPoint {
p_idx: sp,
t_idx: st,
}) = star_p
{
if text.get(st).map(|&b| b != b'/').unwrap_or(false) {
p_idx = sp;
t_idx = st + 1;
star_p = Some(BackupPoint { p_idx, t_idx });
continue;
}
}
if let Some(BackupPoint {
p_idx: gsp,
t_idx: gst,
}) = globstar_p
{
if gst < t_len {
if globstar_anchored {
if let Some(pos) = memchr(b'/', &text[gst..]) {
p_idx = gsp;
t_idx = gst + pos + 1;
star_p = None;
globstar_p = Some(BackupPoint { p_idx, t_idx });
continue;
}
} else {
p_idx = gsp;
t_idx = gst + 1;
star_p = None;
globstar_p = Some(BackupPoint { p_idx, t_idx });
continue;
}
}
}
return false;
}
}
#[expect(clippy::cognitive_complexity)]
#[inline]
fn classmatch(pattern: &[u8], mut p_idx: usize, t_ch: u8) -> Option<usize> {
let mut matched = false;
let mut negated = false;
let mut prev_ch: u8 = 0;
let mut first = true;
loop {
let p_ch = if let Some(&p_ch) = pattern.get(p_idx) {
p_ch
} else {
return None;
};
if unlikely(first && !negated && matches!(p_ch, NEGATE_CLASS | NEGATE_CLASS2)) {
negated = true;
p_idx += 1;
continue;
}
if unlikely(p_ch == b']' && !first) {
break;
}
first = false;
match p_ch {
b'\\' => {
p_idx += 1;
let escaped = if let Some(&escaped) = pattern.get(p_idx) {
escaped
} else {
return None;
};
if escaped == t_ch {
matched = true;
}
prev_ch = escaped;
p_idx += 1;
}
b'-' if prev_ch != 0 && pattern.get(p_idx + 1).map(|&b| b != b']').unwrap_or(false) => {
p_idx += 1;
let mut range_end = pattern[p_idx];
if range_end == b'\\' {
p_idx += 1;
range_end = if let Some(&ch) = pattern.get(p_idx) {
ch
} else {
return None;
};
}
if t_ch >= prev_ch && t_ch <= range_end {
matched = true;
}
p_idx += 1;
prev_ch = 0; }
b'[' if pattern.get(p_idx + 1).map(|&b| b == b':').unwrap_or(false) => {
p_idx += 2;
let class_start = p_idx;
while let Some(ch) = pattern.get(p_idx) {
if ch == &b':' && pattern.get(p_idx + 1) == Some(&b']') {
break;
}
p_idx += 1;
}
if unlikely(pattern.get(p_idx).map(|&b| b != b':').unwrap_or(true)) {
p_idx = class_start - 2;
if pattern[p_idx] == t_ch {
matched = true;
}
prev_ch = b'[';
p_idx += 1;
continue;
}
let class_name = &pattern[class_start..p_idx];
if let Ok(pos) = POSIX_CLASSES.binary_search_by(|(name, _)| name.cmp(&class_name)) {
if POSIX_CLASSES[pos].1(t_ch) {
matched = true;
}
} else {
return None;
}
p_idx += 2; prev_ch = 0; }
_ => {
if p_ch == t_ch {
matched = true;
}
p_idx += 1;
prev_ch = p_ch;
}
}
}
if matched != negated {
Some(p_idx + 1)
} else {
None
}
}
const NEGATE_CLASS: u8 = b'!';
const NEGATE_CLASS2: u8 = b'^';
#[expect(clippy::type_complexity)]
const POSIX_CLASSES: &[(&[u8], fn(u8) -> bool)] = &[
(b"alnum", |c| c.is_ascii_alphanumeric()),
(b"alpha", |c| c.is_ascii_alphabetic()),
(b"blank", |c| matches!(c, b' ' | b'\t')),
(b"cntrl", |c| c.is_ascii_control()),
(b"digit", |c| c.is_ascii_digit()),
(b"graph", |c| c.is_ascii_graphic()),
(b"lower", |c| c.is_ascii_lowercase()),
(b"print", |c| c.is_ascii() && !c.is_ascii_control()),
(b"punct", |c| c.is_ascii_punctuation()),
(b"space", |c| c.is_ascii_whitespace()),
(b"upper", |c| c.is_ascii_uppercase()),
(b"xdigit", |c| c.is_ascii_hexdigit()),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_litmatch_1() {
assert!(litmatch(b"", b""));
assert!(litmatch(b"p", b"p"));
assert!(!litmatch(b"p", b"P"));
assert!(litmatch(b"/usr", b"/usr"));
assert!(!litmatch(b"/usr", b"/usr/"));
}
#[test]
fn test_contains_1() {
assert!(contains(b"hello world", b"world"));
assert!(contains(b"hello world", b"hello"));
assert!(!contains(b"hello world", b"xyz"));
assert!(contains(b"hello", b""));
assert!(!contains(b"", b"x"));
}
#[test]
fn test_is_literal_1() {
assert!(is_literal(b"hello"));
assert!(is_literal(b"/usr/bin/bash"));
assert!(is_literal(b""));
assert!(!is_literal(b"*.txt"));
assert!(!is_literal(b"file?.log"));
assert!(!is_literal(b"[abc]"));
}
#[test]
fn test_globmatch_1() {
assert!(globmatch(b"/usr", b"/usr", MatchMethod::Literal));
assert!(!globmatch(b"/usr", b"/usr/bin", MatchMethod::Literal));
}
#[test]
fn test_globmatch_2() {
assert!(globmatch(b"/usr", b"/usr/bin", MatchMethod::Prefix));
assert!(!globmatch(b"/usr", b"/usrlocal", MatchMethod::Prefix));
}
#[test]
fn test_globmatch_3() {
assert!(globmatch(
b"/usr/*/bash",
b"/usr/bin/bash",
MatchMethod::Glob
));
assert!(!globmatch(
b"/usr/*/bash",
b"/usr/local/bin/bash",
MatchMethod::Glob
));
}
#[test]
fn test_inamematch_1() {
assert!(inamematch("hello", "HELLO"));
assert!(inamematch("hello", "say hello world"));
assert!(!inamematch("xyz", "hello"));
}
#[test]
fn test_inamematch_2() {
assert!(inamematch("*.TXT", "file.txt"));
assert!(!inamematch("*.TXT", "file.log"));
}
#[test]
fn test_get_prefix_1() {
let pat = XPath::from_bytes(b"/usr/***");
let result = get_prefix(pat);
assert_eq!(result.unwrap().as_bytes(), b"/usr");
}
#[test]
fn test_get_prefix_2() {
let pat = XPath::from_bytes(b"/usr/**");
let result = get_prefix(pat);
assert_eq!(result.unwrap().as_bytes(), b"/usr/");
}
#[test]
fn test_get_prefix_3() {
let pat = XPath::from_bytes(b"/usr/*");
assert!(get_prefix(pat).is_none());
}
#[test]
fn test_get_prefix_4() {
let pat = XPath::from_bytes(b"/usr/[ab]/***");
assert!(get_prefix(pat).is_none());
}
#[test]
fn test_get_prefix_5() {
let pat = XPath::from_bytes(b"/usr/[ab]/**");
assert!(get_prefix(pat).is_none());
}
#[test]
fn test_get_match_method_1() {
let mut pat = XPathBuf::from("/usr/**");
let (method, split) = get_match_method(&mut pat);
assert_eq!(method, MatchMethod::Prefix);
assert!(split.is_none());
assert_eq!(pat.as_bytes(), b"/usr/");
}
#[test]
fn test_get_match_method_2() {
let mut pat = XPathBuf::from("/usr/bin");
let (method, split) = get_match_method(&mut pat);
assert_eq!(method, MatchMethod::Literal);
assert!(split.is_none());
}
#[test]
fn test_get_match_method_3() {
let mut pat = XPathBuf::from("/usr/*.so");
let (method, split) = get_match_method(&mut pat);
assert_eq!(method, MatchMethod::Glob);
assert!(split.is_none());
}
#[test]
fn test_get_match_method_4() {
let mut pat = XPathBuf::from("/usr/[ab]/***");
let (method, split) = get_match_method(&mut pat);
assert_eq!(method, MatchMethod::Glob);
assert!(split.is_some());
assert_eq!(split.unwrap().as_bytes(), b"/usr/[ab]/**");
assert_eq!(pat.as_bytes(), b"/usr/[ab]");
}
#[test]
fn test_prematch_1() {
assert!(prematch(b"", b""));
assert!(prematch(b"p", b"p"));
assert!(!prematch(b"p", b"P"));
assert!(prematch(b"/usr", b"/usr"));
assert!(prematch(b"/usr", b"/usr/"));
assert!(prematch(b"/usr", b"/usr/bin"));
assert!(!prematch(b"/usr", b"/usra"));
assert!(!prematch(b"/usr", b"/usra/bin"));
}
#[test]
fn test_prematch_2() {
assert!(!prematch(b"/usr/bin", b"/usr"));
}
#[test]
fn test_prematch_3() {
assert!(prematch(b"/usr/", b"/usr/bin"));
}
#[test]
fn test_wildmatch_1() {
assert!(wildmatch(b"\\a", b"a"));
assert!(!wildmatch(b"\\a", b"b"));
}
#[test]
fn test_wildmatch_2() {
assert!(!wildmatch(b"\\", b"a"));
}
#[test]
fn test_wildmatch_3() {
assert!(wildmatch(b"[[:alpha:]]", b"a"));
assert!(!wildmatch(b"[[:alpha:]]", b"1"));
}
#[test]
fn test_wildmatch_4() {
assert!(wildmatch(b"[[:digit:]]", b"5"));
assert!(!wildmatch(b"[[:digit:]]", b"x"));
}
#[test]
fn test_wildmatch_5() {
assert!(wildmatch(b"[[:upper:]]", b"Z"));
assert!(!wildmatch(b"[[:upper:]]", b"z"));
}
#[test]
fn test_wildmatch_6() {
assert!(wildmatch(b"[[:lower:]]", b"z"));
assert!(!wildmatch(b"[[:lower:]]", b"Z"));
}
#[test]
fn test_wildmatch_7() {
assert!(wildmatch(b"[[:alnum:]]", b"a"));
assert!(wildmatch(b"[[:alnum:]]", b"5"));
assert!(!wildmatch(b"[[:alnum:]]", b"!"));
}
#[test]
fn test_wildmatch_8() {
assert!(wildmatch(b"[[:space:]]", b" "));
assert!(wildmatch(b"[[:space:]]", b"\t"));
assert!(!wildmatch(b"[[:space:]]", b"a"));
}
#[test]
fn test_wildmatch_9() {
assert!(wildmatch(b"[[:xdigit:]]", b"f"));
assert!(wildmatch(b"[[:xdigit:]]", b"A"));
assert!(!wildmatch(b"[[:xdigit:]]", b"g"));
}
#[test]
fn test_wildmatch_10() {
assert!(wildmatch(b"[[:print:]]", b"a"));
assert!(!wildmatch(b"[[:print:]]", b"\x01"));
}
#[test]
fn test_wildmatch_11() {
assert!(wildmatch(b"[[:punct:]]", b"!"));
assert!(!wildmatch(b"[[:punct:]]", b"a"));
}
#[test]
fn test_wildmatch_12() {
assert!(wildmatch(b"[[:graph:]]", b"a"));
assert!(!wildmatch(b"[[:graph:]]", b" "));
}
#[test]
fn test_wildmatch_13() {
assert!(wildmatch(b"[[:cntrl:]]", b"\x01"));
assert!(!wildmatch(b"[[:cntrl:]]", b"a"));
}
#[test]
fn test_wildmatch_14() {
assert!(wildmatch(b"[[:blank:]]", b" "));
assert!(wildmatch(b"[[:blank:]]", b"\t"));
assert!(!wildmatch(b"[[:blank:]]", b"a"));
}
#[test]
fn test_wildmatch_15() {
assert!(!wildmatch(b"[[:bogus:]]", b"a"));
}
#[test]
fn test_wildmatch_16() {
assert!(wildmatch(b"[!a]", b"b"));
assert!(!wildmatch(b"[!a]", b"a"));
}
#[test]
fn test_wildmatch_17() {
assert!(wildmatch(b"[^a]", b"b"));
assert!(!wildmatch(b"[^a]", b"a"));
}
#[test]
fn test_wildmatch_18() {
assert!(wildmatch(b"[a-z]", b"m"));
assert!(!wildmatch(b"[a-z]", b"M"));
}
#[test]
fn test_wildmatch_19() {
assert!(wildmatch(b"[\\a-\\z]", b"m"));
}
#[test]
fn test_wildmatch_20() {
assert!(wildmatch(b"[\\]]", b"]"));
assert!(!wildmatch(b"[\\]]", b"a"));
}
#[test]
fn test_wildmatch_21() {
assert!(!wildmatch(b"[abc", b"a"));
}
#[test]
fn test_wildmatch_22() {
assert!(wildmatch(b"[]]", b"]"));
}
#[test]
fn test_wildmatch_23() {
assert!(!wildmatch(b"?", b"/"));
}
#[test]
fn test_wildmatch_24() {
assert!(wildmatch(b"/usr/*", b"/usr/bin"));
assert!(!wildmatch(b"/usr/*", b"/usr/bin/bash"));
}
#[test]
fn test_wildmatch_25() {
assert!(wildmatch(b"/usr/**", b"/usr/bin/bash"));
assert!(wildmatch(b"**", b"anything/at/all"));
}
#[test]
fn test_wildmatch_26() {
assert!(wildmatch(b"/usr/**/bash", b"/usr/bin/bash"));
assert!(wildmatch(b"/usr/**/bash", b"/usr/bash"));
assert!(wildmatch(b"/usr/**/bash", b"/usr/local/bin/bash"));
}
#[test]
fn test_wildmatch_27() {
assert!(wildmatch(b"/**/lib/*.so", b"/usr/lib/libc.so"));
assert!(!wildmatch(b"/**/lib/*.so", b"/usr/lib/sub/libc.so"));
}
#[test]
fn test_wildmatch_28() {
assert!(wildmatch(b"abc*", b"abc"));
assert!(wildmatch(b"abc**", b"abc"));
}
#[test]
fn test_wildmatch_29() {
assert!(wildmatch(b"", b""));
assert!(!wildmatch(b"", b"a"));
assert!(!wildmatch(b"a", b""));
}
#[test]
fn test_wildmatch_30() {
assert!(wildmatch(b"[[.a.]", b"["));
}
#[test]
fn test_wildmatch_31() {
assert!(!wildmatch(b"*", b"a/b"));
}
#[test]
fn test_wildmatch_32() {
assert!(!wildmatch(b"[abc]", b"/"));
}
#[test]
fn test_wildmatch_33() {
assert!(!wildmatch(b"a?", b"a"));
}
#[test]
fn test_wildmatch_34() {
assert!(!wildmatch(b"a\\", b"ab"));
}
#[test]
fn test_wildmatch_35() {
assert!(!wildmatch(b"*z", b"abc"));
}
#[test]
fn test_wildmatch_36() {
assert!(wildmatch(b"a/**/*", b"a/b"));
assert!(wildmatch(b"a/**/*", b"a/b/c"));
}
#[test]
fn test_wildmatch_blob() {
use std::io::BufRead;
let data = include_bytes!("wildtest.txt.xz");
let decoder = xz2::read::XzDecoder::new(&data[..]);
let reader = std::io::BufReader::new(decoder);
let mut failures = 0;
let mut test_cnt = 0;
for (index, line) in reader.lines().enumerate() {
let line = line.expect("Failed to read line from wildtest.txt.xz");
let line_bytes = line.as_bytes();
let line_num = index + 1;
if line_bytes.starts_with(&[b'#'])
|| line_bytes.iter().all(|&b| b == b' ' || b == b'\t')
|| line.is_empty()
{
continue;
}
if let Some((expected, fnmatch_same, text, pattern)) = parse_test_line(line_bytes) {
test_cnt += 1;
if let Err(err) = run_wildtest(line_num, expected, fnmatch_same, text, pattern) {
eprintln!("FAIL[{test_cnt}]\t{err}");
if !err.contains("fnmatch") {
failures += 1;
}
}
} else {
unreachable!("BUG: Fix test at line {test_cnt}: {line}!");
}
}
if failures > 0 {
panic!("{failures} out of {test_cnt} tests failed.");
}
}
fn parse_test_line(line: &[u8]) -> Option<(bool, bool, &[u8], &[u8])> {
let mut parts = [&b""[..]; 4];
let mut part_idx = 0;
let mut i = 0;
while i < line.len() && part_idx < 4 {
while i < line.len() && matches!(line[i], b' ' | b'\t') {
i += 1;
}
if i >= line.len() {
break;
}
if matches!(line[i], b'\'' | b'"' | b'`') {
let quote = line[i];
i += 1;
let start = i;
while i < line.len() && line[i] != quote {
i += 1;
}
parts[part_idx] = &line[start..i];
if i < line.len() {
i += 1; }
} else {
let start = i;
while i < line.len() && !matches!(line[i], b' ' | b'\t') {
i += 1;
}
parts[part_idx] = &line[start..i];
}
part_idx += 1;
}
if part_idx >= 4 {
let expected = parts[0].first() == Some(&b'1');
let fnmatch_same = parts[1].first() == Some(&b'1');
Some((expected, fnmatch_same, parts[2], parts[3]))
} else {
None
}
}
fn run_wildtest(
line: usize,
expected: bool,
fnmatch_same: bool,
text: &[u8],
pattern: &[u8],
) -> Result<(), String> {
let result = wildmatch(pattern, text);
if result != expected {
let text = String::from_utf8_lossy(text);
let pattern = String::from_utf8_lossy(pattern);
let msg = format!(
"[!] Test failed on line {line}: text='{text}', pattern='{pattern}', expected={expected}, got={result}",
);
return Err(msg);
}
let fn_result = fnmatch(pattern, text);
let same = fn_result == result;
if same != fnmatch_same {
let text = String::from_utf8_lossy(text);
let pattern = String::from_utf8_lossy(pattern);
let msg = format!(
"[!] fnmatch divergence on line {line}: text='{text}', pattern='{pattern}', wildmatch={result}, fnmatch={fn_result}, expected_same={fnmatch_same}",
);
return Err(msg);
}
Ok(())
}
fn fnmatch(pat: &[u8], input: &[u8]) -> bool {
pat.with_nix_path(|pat_cstr| {
input.with_nix_path(|input_cstr| {
let flags = libc::FNM_PATHNAME | libc::FNM_NOESCAPE | libc::FNM_PERIOD;
unsafe { libc::fnmatch(pat_cstr.as_ptr(), input_cstr.as_ptr(), flags) == 0 }
})
})
.map(|res| res.unwrap())
.unwrap()
}
}