pub fn is_glob_pattern(pattern: &str) -> bool {
pattern.contains('*') || pattern.contains('?')
}
pub fn is_path_pattern(pattern: &str) -> bool {
pattern.contains('/') || pattern.contains('\\')
}
pub fn filename_glob_matches(pattern: &str, filename: &str) -> bool {
glob_match_bytes(pattern.as_bytes(), filename.as_bytes())
}
pub fn path_glob_matches(pattern: &str, path: &str) -> bool {
path_glob_match_bytes(pattern.as_bytes(), path.as_bytes())
}
fn glob_match_bytes(pattern: &[u8], text: &[u8]) -> bool {
let mut p = 0;
let mut t = 0;
let mut star_p = usize::MAX;
let mut star_t = 0;
while t < text.len() {
if p < pattern.len() && (pattern[p] == b'?' || pattern[p] == text[t]) {
p += 1;
t += 1;
} else if p < pattern.len() && pattern[p] == b'*' {
star_p = p;
star_t = t;
p += 1;
} else if star_p != usize::MAX {
p = star_p + 1;
star_t += 1;
t = star_t;
} else {
return false;
}
}
while p < pattern.len() && pattern[p] == b'*' {
p += 1;
}
p == pattern.len()
}
#[inline]
fn is_path_sep(b: u8) -> bool {
b == b'/' || b == b'\\'
}
#[inline]
fn path_chars_match(pattern_byte: u8, text_byte: u8) -> bool {
if pattern_byte == text_byte {
return true;
}
is_path_sep(pattern_byte) && is_path_sep(text_byte)
}
fn path_glob_match_bytes(pattern: &[u8], text: &[u8]) -> bool {
let mut p = 0;
let mut t = 0;
let mut dstar_p: Option<usize> = None;
let mut dstar_t: usize = 0;
let mut star_p: Option<usize> = None;
let mut star_t: usize = 0;
while t < text.len() {
if p + 1 < pattern.len() && pattern[p] == b'*' && pattern[p + 1] == b'*' {
let mut next_p = p + 2;
while next_p < pattern.len() && pattern[next_p] == b'*' {
next_p += 1;
}
if next_p < pattern.len() && is_path_sep(pattern[next_p]) {
next_p += 1;
}
dstar_p = Some(next_p);
dstar_t = t;
p = next_p;
star_p = None;
continue;
}
if p < pattern.len() && pattern[p] == b'*' {
star_p = Some(p + 1);
star_t = t;
p += 1;
continue;
}
if p < pattern.len() && pattern[p] == b'?' && !is_path_sep(text[t]) {
p += 1;
t += 1;
continue;
}
if p < pattern.len() && path_chars_match(pattern[p], text[t]) {
p += 1;
t += 1;
continue;
}
if let Some(sp) = star_p {
if !is_path_sep(text[star_t]) {
star_t += 1;
t = star_t;
p = sp;
continue;
}
}
if let Some(dp) = dstar_p {
dstar_t += 1;
t = dstar_t;
p = dp;
star_p = None;
continue;
}
return false;
}
while p < pattern.len() && pattern[p] == b'*' {
p += 1;
}
p == pattern.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_glob_pattern() {
assert!(is_glob_pattern("*.conf"));
assert!(is_glob_pattern("Dockerfile*"));
assert!(is_glob_pattern("file?.txt"));
assert!(is_glob_pattern("*"));
assert!(!is_glob_pattern("Makefile"));
assert!(!is_glob_pattern(".bashrc"));
assert!(!is_glob_pattern(""));
}
#[test]
fn test_is_path_pattern() {
assert!(is_path_pattern("/etc/**/rc.*"));
assert!(is_path_pattern("/etc/*.conf"));
assert!(is_path_pattern("**/rc.*"));
assert!(is_path_pattern("src/*.rs"));
assert!(!is_path_pattern("*.conf"));
assert!(!is_path_pattern("*rc"));
assert!(!is_path_pattern("Makefile"));
}
#[test]
fn test_star_prefix() {
assert!(filename_glob_matches("*.conf", "nftables.conf"));
assert!(filename_glob_matches("*.conf", "resolv.conf"));
assert!(filename_glob_matches("*.conf", ".conf"));
assert!(!filename_glob_matches("*.conf", "conf"));
assert!(!filename_glob_matches("*.conf", "nftables.txt"));
}
#[test]
fn test_star_suffix() {
assert!(filename_glob_matches("Dockerfile*", "Dockerfile"));
assert!(filename_glob_matches("Dockerfile*", "Dockerfile.dev"));
assert!(!filename_glob_matches("Dockerfile*", "dockerfile"));
}
#[test]
fn test_star_middle() {
assert!(filename_glob_matches(".env.*", ".env.local"));
assert!(filename_glob_matches(".env.*", ".env.production"));
assert!(!filename_glob_matches(".env.*", ".env"));
}
#[test]
fn test_star_suffix_pattern() {
assert!(filename_glob_matches("*rc", "lfrc"));
assert!(filename_glob_matches("*rc", ".bashrc"));
assert!(filename_glob_matches("*rc", "rc"));
assert!(!filename_glob_matches("*rc", "lfrc.bak"));
}
#[test]
fn test_question_mark() {
assert!(filename_glob_matches("file?.txt", "file1.txt"));
assert!(filename_glob_matches("file?.txt", "fileA.txt"));
assert!(!filename_glob_matches("file?.txt", "file.txt"));
assert!(!filename_glob_matches("file?.txt", "file12.txt"));
}
#[test]
fn test_bare_star() {
assert!(filename_glob_matches("*", "anything"));
assert!(filename_glob_matches("*", ""));
}
#[test]
fn test_exact_match() {
assert!(filename_glob_matches("Makefile", "Makefile"));
assert!(!filename_glob_matches("Makefile", "makefile"));
}
#[test]
fn test_multiple_stars() {
assert!(filename_glob_matches("*.*", "file.txt"));
assert!(filename_glob_matches("*.*", ".bashrc"));
assert!(!filename_glob_matches("*.*", "Makefile"));
}
#[test]
fn test_path_doublestar_middle() {
assert!(path_glob_matches("/etc/**/rc.*", "/etc/rc.conf"));
assert!(path_glob_matches("/etc/**/rc.*", "/etc/init/rc.local"));
assert!(path_glob_matches("/etc/**/rc.*", "/etc/a/b/c/rc.d"));
assert!(!path_glob_matches("/etc/**/rc.*", "/var/rc.conf"));
assert!(!path_glob_matches("/etc/**/rc.*", "/etc/init/nope"));
}
#[test]
fn test_path_single_star_no_slash_crossing() {
assert!(path_glob_matches("/etc/*.conf", "/etc/nftables.conf"));
assert!(path_glob_matches("/etc/*.conf", "/etc/resolv.conf"));
assert!(!path_glob_matches("/etc/*.conf", "/etc/sub/nftables.conf"));
}
#[test]
fn test_path_doublestar_prefix() {
assert!(path_glob_matches("**/rc.*", "/etc/rc.conf"));
assert!(path_glob_matches("**/rc.*", "/etc/init/rc.local"));
assert!(path_glob_matches("**/rc.*", "rc.conf"));
}
#[test]
fn test_path_doublestar_suffix() {
assert!(path_glob_matches("/etc/**", "/etc/foo"));
assert!(path_glob_matches("/etc/**", "/etc/foo/bar"));
assert!(path_glob_matches("/etc/**", "/etc/foo/bar/baz.conf"));
assert!(!path_glob_matches("/etc/**", "/var/foo"));
}
#[test]
fn test_path_question_mark() {
assert!(path_glob_matches("/etc/rc.?", "/etc/rc.d"));
assert!(!path_glob_matches("/etc/rc.?", "/etc/rc.dd"));
assert!(!path_glob_matches("/etc/?", "/etc/ab"));
}
#[test]
fn test_path_literal_match() {
assert!(path_glob_matches("/etc/hosts", "/etc/hosts"));
assert!(!path_glob_matches("/etc/hosts", "/etc/hostname"));
}
#[test]
fn test_path_doublestar_and_single_star() {
assert!(path_glob_matches("/etc/**/*.conf", "/etc/nftables.conf"));
assert!(path_glob_matches(
"/etc/**/*.conf",
"/etc/sub/nftables.conf"
));
assert!(path_glob_matches("/etc/**/*.conf", "/etc/a/b/c/foo.conf"));
assert!(!path_glob_matches("/etc/**/*.conf", "/etc/a/b/c/foo.txt"));
assert!(!path_glob_matches("/etc/**/*.conf", "/var/foo.conf"));
}
#[test]
fn test_path_doublestar_zero_segments() {
assert!(path_glob_matches("**/Makefile", "Makefile"));
assert!(path_glob_matches("**/Makefile", "/src/Makefile"));
assert!(path_glob_matches("/src/**/main.rs", "/src/main.rs"));
}
#[test]
fn test_path_multiple_doublestars() {
assert!(path_glob_matches(
"/**/src/**/*.rs",
"/home/user/src/main.rs"
));
assert!(path_glob_matches("/**/src/**/*.rs", "/src/lib.rs"));
assert!(path_glob_matches("/**/src/**/*.rs", "/a/b/src/c/d/foo.rs"));
}
#[test]
fn test_is_path_pattern_backslash() {
assert!(is_path_pattern("C:\\Users\\**\\rc.*"));
assert!(is_path_pattern("src\\*.rs"));
assert!(!is_path_pattern("*.conf"));
}
#[test]
fn test_path_backslash_separators() {
assert!(path_glob_matches(
"C:\\Users\\**\\rc.*",
"C:\\Users\\etc\\rc.conf"
));
assert!(path_glob_matches(
"C:\\Users\\**\\rc.*",
"C:\\Users\\etc\\init\\rc.local"
));
assert!(!path_glob_matches(
"C:\\Users\\**\\rc.*",
"D:\\Users\\etc\\rc.conf"
));
}
#[test]
fn test_path_mixed_separators() {
assert!(path_glob_matches("/etc/**/rc.*", "\\etc\\rc.conf"));
assert!(path_glob_matches("/etc/**/rc.*", "\\etc\\init\\rc.local"));
assert!(path_glob_matches("\\etc\\**\\rc.*", "/etc/rc.conf"));
assert!(path_glob_matches("\\etc\\**\\rc.*", "/etc/init/rc.local"));
}
#[test]
fn test_path_backslash_single_star_no_crossing() {
assert!(path_glob_matches(
"C:\\etc\\*.conf",
"C:\\etc\\nftables.conf"
));
assert!(!path_glob_matches(
"C:\\etc\\*.conf",
"C:\\etc\\sub\\nftables.conf"
));
}
#[test]
fn test_path_backslash_question_mark() {
assert!(path_glob_matches("C:\\etc\\rc.?", "C:\\etc\\rc.d"));
assert!(!path_glob_matches("C:\\etc\\?", "C:\\etc\\ab"));
}
}