fix(windows): fallback for bracketed directory literals in glob patterns
This commit is contained in:
parent
833b9a5065
commit
862cab20bb
1 changed files with 84 additions and 9 deletions
93
src/main.rs
93
src/main.rs
|
|
@ -221,18 +221,73 @@ fn collect_files(
|
||||||
in_out_pairs
|
in_out_pairs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(windows, test))]
|
||||||
|
fn escape_square_brackets_in_dir_prefix(pattern: &str) -> Option<String> {
|
||||||
|
// We only attempt fallback escaping when there is an obvious wildcard segment.
|
||||||
|
// This avoids changing behavior for plain literal paths.
|
||||||
|
let first_wildcard = pattern
|
||||||
|
.char_indices()
|
||||||
|
.find(|(_, c)| matches!(c, '*' | '?' | '{'))
|
||||||
|
.map(|(idx, _)| idx)?;
|
||||||
|
|
||||||
|
// Only escape directory components (up to the last separator before wildcard).
|
||||||
|
// This preserves intended character class patterns in file names.
|
||||||
|
let dir_end = pattern[..first_wildcard]
|
||||||
|
.char_indices()
|
||||||
|
.rfind(|(_, c)| matches!(c, '/' | '\\'))
|
||||||
|
.map(|(idx, _)| idx + 1)?;
|
||||||
|
|
||||||
|
let (dir_prefix, remainder) = pattern.split_at(dir_end);
|
||||||
|
if !dir_prefix.contains('[') && !dir_prefix.contains(']') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut escaped_prefix = String::with_capacity(dir_prefix.len());
|
||||||
|
for ch in dir_prefix.chars() {
|
||||||
|
match ch {
|
||||||
|
'[' => escaped_prefix.push_str("[[]"),
|
||||||
|
']' => escaped_prefix.push_str("[]]"),
|
||||||
|
_ => escaped_prefix.push(ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if escaped_prefix == dir_prefix {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(format!("{escaped_prefix}{remainder}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn glob_matches(pattern: &str) -> Option<Vec<PathBuf>> {
|
||||||
|
// Use MatchOptions::default() to disable case-sensitivity.
|
||||||
|
glob::glob_with(pattern, glob::MatchOptions::default())
|
||||||
|
.ok()
|
||||||
|
.map(|paths| paths.flatten().collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn apply_glob_pattern(path: PathBuf) -> Vec<PathBuf> {
|
fn apply_glob_pattern(path: PathBuf) -> Vec<PathBuf> {
|
||||||
let matches = path
|
let Some(pattern) = path.to_str() else {
|
||||||
.to_str()
|
return vec![path];
|
||||||
// Use MatchOptions::default() to disable case-sensitivity
|
};
|
||||||
.and_then(|pattern| glob::glob_with(pattern, glob::MatchOptions::default()).ok())
|
|
||||||
.map(|paths| paths.flatten().collect::<Vec<_>>());
|
|
||||||
|
|
||||||
match matches {
|
if let Some(paths) = glob_matches(pattern) {
|
||||||
Some(paths) if !paths.is_empty() => paths,
|
if !paths.is_empty() {
|
||||||
_ => vec![path],
|
return paths;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(escaped_pattern) = escape_square_brackets_in_dir_prefix(pattern)
|
||||||
|
&& let Some(paths) = glob_matches(&escaped_pattern)
|
||||||
|
&& !paths.is_empty()
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
"No matches for glob pattern `{pattern}`; retrying with escaped directory brackets: `{escaped_pattern}`"
|
||||||
|
);
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![path]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_opts_into_struct(
|
fn parse_opts_into_struct(
|
||||||
|
|
@ -631,7 +686,7 @@ fn format_bytes(count: i64, include_raw: bool) -> String {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::format_bytes;
|
use super::{escape_square_brackets_in_dir_prefix, format_bytes};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_bytes() {
|
fn test_format_bytes() {
|
||||||
|
|
@ -641,4 +696,24 @@ mod tests {
|
||||||
assert_eq!(format_bytes(2_000_000_000, false), "1.86 GiB");
|
assert_eq!(format_bytes(2_000_000_000, false), "1.86 GiB");
|
||||||
assert_eq!(format_bytes(-1024, false), "-1.00 KiB");
|
assert_eq!(format_bytes(-1024, false), "-1.00 KiB");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_square_brackets_in_dir_prefix() {
|
||||||
|
let pattern = r"F:\[a]bug test\*.png";
|
||||||
|
let escaped = escape_square_brackets_in_dir_prefix(pattern)
|
||||||
|
.expect("should escape bracket literals in directory segment");
|
||||||
|
assert_eq!(escaped, r"F:\[[]a[]]bug test\*.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_does_not_change_filename_character_class() {
|
||||||
|
let pattern = r"F:\images\file[0-9]*.png";
|
||||||
|
assert!(escape_square_brackets_in_dir_prefix(pattern).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_requires_wildcard_segment() {
|
||||||
|
let pattern = r"F:\[a]bug test\image.png";
|
||||||
|
assert!(escape_square_brackets_in_dir_prefix(pattern).is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue