diff --git a/src/lib.rs b/src/lib.rs index 3ef5091..06d7089 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -907,8 +907,13 @@ fn fill_todo( let pattern = &patterns[idx]; let is_dir = path.is_directory; let curdir = path.as_ref() == Path::new("."); - match (pattern.has_metachars, is_dir) { - (false, _) => { + let direct_literal = !pattern.has_metachars + && (options.case_sensitive + || pattern.as_str().is_empty() + || pattern.as_str() == "." + || pattern.as_str() == ".."); + match (direct_literal, is_dir) { + (true, _) => { debug_assert!( pattern .tokens @@ -938,7 +943,7 @@ fn fill_todo( add(todo, next_path); } } - (true, true) => { + (false, true) => { let dirs = fs::read_dir(path).and_then(|d| { d.map(|e| { e.map(|e| { @@ -960,9 +965,31 @@ fn fill_todo( }); match dirs { Ok(mut children) => { - if options.require_literal_leading_dot { + if pattern.has_metachars && options.require_literal_leading_dot { children.retain(|x| !x.1.to_str().unwrap().starts_with('.')); } + + #[cfg(windows)] + if !pattern.has_metachars + && !children.iter().any(|x| { + x.1.to_str() + .map(|name| pattern.matches_with(name, options)) + .unwrap_or(false) + }) + { + let s = pattern.as_str(); + let next_path = if curdir { + PathBuf::from(s) + } else { + path.join(s) + }; + let next_path = PathWrapper::from_path(next_path); + if fs::metadata(&next_path).is_ok() + || fs::symlink_metadata(&next_path).is_ok() + { + add(todo, next_path); + } + } children.sort_by(|p1, p2| p2.1.cmp(&p1.1)); todo.extend(children.into_iter().map(|x| Ok((x.0, idx)))); @@ -987,7 +1014,7 @@ fn fill_todo( } } } - (true, false) => { + (false, false) => { // not a directory, nothing more to find } } diff --git a/tests/glob-61.rs b/tests/glob-61.rs new file mode 100644 index 0000000..db23570 --- /dev/null +++ b/tests/glob-61.rs @@ -0,0 +1,155 @@ +use glob::{glob_with, MatchOptions}; +use std::fs; +use std::path::{Path, PathBuf}; + +fn options(case_sensitive: bool, require_literal_leading_dot: bool) -> MatchOptions { + MatchOptions { + case_sensitive, + require_literal_separator: true, + require_literal_leading_dot, + } +} + +fn matches(pattern: &Path, options: MatchOptions) -> Vec { + let mut paths = glob_with(pattern.to_str().unwrap(), options) + .unwrap() + .map(|result| result.unwrap()) + .collect::>(); + paths.sort(); + paths +} + +fn canonical_set_matches(pattern: &Path, options: MatchOptions, expected: &[PathBuf]) { + let actual = matches(pattern, options); + let mut actual = actual + .iter() + .map(|path| fs::canonicalize(path).unwrap()) + .collect::>(); + let mut expected = expected + .iter() + .map(|path| fs::canonicalize(path).unwrap()) + .collect::>(); + actual.sort(); + expected.sort(); + assert_eq!(actual, expected); +} + +#[test] +fn insensitive_literals_match_case_variants_and_controls() { + let temporary = tempfile::tempdir().unwrap(); + let root = temporary.path().to_path_buf(); + fs::create_dir_all(root.join("MixedParent")).unwrap(); + fs::write(root.join("test_file"), b"lowercase").unwrap(); + fs::write(root.join("MixedParent/test_file"), b"nested").unwrap(); + fs::write(root.join(".hidden"), b"hidden").unwrap(); + let upper = root.join("Test_File"); + let has_distinct_variant = if upper.exists() { + fs::read(&upper).unwrap() != fs::read(root.join("test_file")).unwrap() + } else { + fs::write(&upper, b"uppercase").unwrap(); + true + }; + + let lower = root.join("test_file"); + if has_distinct_variant { + assert_eq!( + matches(&lower, options(false, true)), + vec![root.join("Test_File"), root.join("test_file")] + ); + assert_eq!( + matches(&root.join("TEST_FILE"), options(false, true)), + vec![root.join("Test_File"), root.join("test_file")] + ); + } else { + #[cfg(windows)] + assert_eq!(matches(&lower, options(false, true)), vec![lower.clone()]); + assert_eq!( + matches(&root.join("TEST_FILE"), options(false, true)), + vec![root.join("test_file")] + ); + } + assert_eq!( + matches(&lower, options(true, true)), + vec![root.join("test_file")] + ); + assert_eq!( + matches(&root.join("mixedparent/test_file"), options(false, true)), + vec![root.join("MixedParent/test_file")] + ); + assert_eq!(matches(&root.join("*_file"), options(false, true)), { + let mut expected = vec![root.join("test_file")]; + if has_distinct_variant { + expected.push(root.join("Test_File")); + } + expected.sort(); + expected + }); + assert_eq!( + matches(&root.join("missing_file"), options(false, true)), + Vec::::new() + ); + assert_eq!( + matches(&root.join("test_file/child"), options(false, true)), + Vec::::new() + ); + assert_eq!( + matches(&root.join(".hidden"), options(false, true)), + vec![root.join(".hidden")] + ); + assert_eq!( + matches(&root.join(".hidden"), options(false, false)), + vec![root.join(".hidden")] + ); + assert_eq!( + matches(&root.join("*hidden"), options(false, true)), + Vec::::new() + ); + assert_eq!( + matches(&root.join("*hidden"), options(false, false)), + vec![root.join(".hidden")] + ); + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + let link = root.join("MixedParent/Broken_Link"); + symlink("missing-target", &link).unwrap(); + assert_eq!( + matches(&root.join("mixedparent/broken_link"), options(false, true)), + vec![link] + ); + } + let mut physical_variants = vec![lower.clone()]; + if has_distinct_variant { + physical_variants.push(upper); + } + canonical_set_matches( + &root.join("./test_file"), + options(false, true), + &physical_variants, + ); + canonical_set_matches( + &root.join("MixedParent/../test_file"), + options(false, true), + &physical_variants, + ); + let repeated_separator = PathBuf::from(format!("{}//test_file", root.display())); + canonical_set_matches( + &repeated_separator, + options(false, true), + &physical_variants, + ); + #[cfg(unix)] + assert_eq!( + matches(Path::new("/"), options(false, true)), + vec![PathBuf::from("/")] + ); +} + +#[cfg(unix)] +#[test] +fn insensitive_root_literal_returns_root() { + assert_eq!( + matches(Path::new("/"), options(false, true)), + vec![PathBuf::from("/")] + ); +}