Skip to content

Commit c8cf881

Browse files
committed
fix: windows path separators and includeChildMatches ordering
- Use forward slashes by default on Windows (glob v13 behavior) - Only use backslashes when posix is explicitly set to false - Fix includeChildMatches filtering to work with arbitrary filesystem order - Post-process matches to ensure parents are filtered before children (fixes Linux where readdir order is non-deterministic)
1 parent b08fa2a commit c8cf881

1 file changed

Lines changed: 93 additions & 45 deletions

File tree

src/glob.rs

Lines changed: 93 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ pub struct Glob {
3636
/// Patterns stored in Arc for cheap cloning into closures
3737
patterns: Arc<[Pattern]>,
3838
absolute: bool,
39-
posix: bool,
39+
posix_explicit_true: bool,
40+
posix_explicit_false: bool,
4041
#[allow(dead_code)]
4142
nobrace: bool,
4243
#[allow(dead_code)]
@@ -242,7 +243,8 @@ impl Glob {
242243
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
243244

244245
let absolute = options.absolute.unwrap_or(false);
245-
let posix = options.posix.unwrap_or(false);
246+
let posix_explicit_true = options.posix == Some(true);
247+
let posix_explicit_false = options.posix == Some(false);
246248
let nobrace = options.nobrace.unwrap_or(false);
247249
let noext = options.noext.unwrap_or(false);
248250
let dot = options.dot.unwrap_or(false);
@@ -435,7 +437,8 @@ impl Glob {
435437
cwd,
436438
patterns,
437439
absolute,
438-
posix,
440+
posix_explicit_true,
441+
posix_explicit_false,
439442
nobrace,
440443
noext,
441444
dot,
@@ -499,11 +502,12 @@ impl Glob {
499502
let mut seen: AHashSet<String> = AHashSet::with_capacity(estimated_capacity);
500503
let mut ignored_dirs: AHashSet<String> = AHashSet::with_capacity(8); // Most globs have few ignored dirs
501504

502-
// When includeChildMatches is false, track matched paths to exclude their children
503-
let mut matched_parents: AHashSet<String> = if self.include_child_matches {
504-
AHashSet::new() // Empty, won't be used
505+
// When includeChildMatches is false, track (result, normalized) pairs for post-filtering
506+
// This is needed because filesystem order may cause children to be seen before parents on Linux
507+
let mut matched_with_normalized: Vec<(String, String)> = if self.include_child_matches {
508+
Vec::new() // Empty, won't be used
505509
} else {
506-
AHashSet::with_capacity(estimated_capacity / 4)
510+
Vec::with_capacity(estimated_capacity)
507511
};
508512

509513
// Pre-allocate a reusable buffer for path formatting
@@ -716,13 +720,6 @@ impl Glob {
716720
continue;
717721
}
718722

719-
// When includeChildMatches is false, skip paths that are children of already-matched paths
720-
if !self.include_child_matches
721-
&& self.is_child_of_matched(&normalized, &matched_parents)
722-
{
723-
continue;
724-
}
725-
726723
// Check if any pattern matches
727724
// For patterns that end with /, only match if entry is a directory
728725
let is_dir = entry.is_dir();
@@ -766,15 +763,45 @@ impl Glob {
766763

767764
// Deduplicate results (important for overlapping brace expansions)
768765
if seen.insert(result.clone()) {
769-
// When includeChildMatches is false, track this path to exclude its children
766+
// When includeChildMatches is false, track (result, normalized) for post-filtering
770767
if !self.include_child_matches {
771-
matched_parents.insert(normalized.into_owned());
768+
matched_with_normalized.push((result.clone(), normalized.into_owned()));
772769
}
773770
results.push(result);
774771
}
775772
}
776773
}
777774

775+
// When includeChildMatches is false, post-process to filter out children
776+
// This handles cases where filesystem order causes children to be seen before parents
777+
if !self.include_child_matches && !matched_with_normalized.is_empty() {
778+
// Sort by path depth (number of segments) - shorter paths first
779+
matched_with_normalized.sort_by_key(|(_, norm)| norm.matches('/').count());
780+
781+
// Filter out children using a set of matched parents
782+
let mut parents: AHashSet<&str> = AHashSet::with_capacity(matched_with_normalized.len());
783+
let mut filtered_results: Vec<String> =
784+
Vec::with_capacity(matched_with_normalized.len());
785+
786+
for (result, normalized) in &matched_with_normalized {
787+
// Check if this path is a child of any already-matched parent
788+
let is_child = parents.iter().any(|parent| {
789+
let parent_bytes = parent.as_bytes();
790+
let norm_bytes = normalized.as_bytes();
791+
norm_bytes.starts_with(parent_bytes)
792+
&& norm_bytes.len() > parent_bytes.len()
793+
&& norm_bytes.get(parent_bytes.len()) == Some(&b'/')
794+
});
795+
796+
if !is_child {
797+
parents.insert(normalized.as_str());
798+
filtered_results.push(result.clone());
799+
}
800+
}
801+
802+
return filtered_results;
803+
}
804+
778805
results
779806
}
780807

@@ -795,11 +822,11 @@ impl Glob {
795822
let mut seen: AHashSet<String> = AHashSet::with_capacity(estimated_capacity);
796823
let mut ignored_dirs: AHashSet<String> = AHashSet::with_capacity(8);
797824

798-
// When includeChildMatches is false, track matched paths to exclude their children
799-
let mut matched_parents: AHashSet<String> = if self.include_child_matches {
800-
AHashSet::new()
825+
// When includeChildMatches is false, track (result, normalized) pairs for post-filtering
826+
let mut matched_with_normalized: Vec<(PathData, String)> = if self.include_child_matches {
827+
Vec::new()
801828
} else {
802-
AHashSet::with_capacity(estimated_capacity / 4)
829+
Vec::with_capacity(estimated_capacity)
803830
};
804831

805832
// Check if any pattern matches the cwd itself ("**" or ".").
@@ -954,13 +981,6 @@ impl Glob {
954981
continue;
955982
}
956983

957-
// When includeChildMatches is false, skip paths that are children of already-matched paths
958-
if !self.include_child_matches
959-
&& self.is_child_of_matched(&normalized, &matched_parents)
960-
{
961-
continue;
962-
}
963-
964984
// Check if any pattern matches
965985
let is_dir = entry.is_dir();
966986

@@ -995,20 +1015,51 @@ impl Glob {
9951015
normalized.replace('/', "\\")
9961016
};
9971017
if seen.insert(output_path.clone()) {
998-
// When includeChildMatches is false, track this path to exclude its children
999-
// (use the normalized path with forward slashes for internal tracking)
1000-
if !self.include_child_matches {
1001-
matched_parents.insert(output_path.replace('\\', "/"));
1002-
}
1003-
1004-
results.push(PathData {
1005-
path: output_path,
1018+
let path_data = PathData {
1019+
path: output_path.clone(),
10061020
is_directory: is_dir,
10071021
is_file: entry.is_file(),
10081022
is_symlink: entry.is_symlink(),
1009-
});
1023+
};
1024+
1025+
// When includeChildMatches is false, track for post-filtering
1026+
if !self.include_child_matches {
1027+
let norm_path = output_path.replace('\\', "/");
1028+
matched_with_normalized.push((path_data.clone(), norm_path));
1029+
}
1030+
1031+
results.push(path_data);
1032+
}
1033+
}
1034+
}
1035+
1036+
// When includeChildMatches is false, post-process to filter out children
1037+
if !self.include_child_matches && !matched_with_normalized.is_empty() {
1038+
// Sort by path depth (number of segments) - shorter paths first
1039+
matched_with_normalized.sort_by_key(|(_, norm)| norm.matches('/').count());
1040+
1041+
// Filter out children using a set of matched parents
1042+
let mut parents: AHashSet<&str> = AHashSet::with_capacity(matched_with_normalized.len());
1043+
let mut filtered_results: Vec<PathData> =
1044+
Vec::with_capacity(matched_with_normalized.len());
1045+
1046+
for (path_data, normalized) in &matched_with_normalized {
1047+
// Check if this path is a child of any already-matched parent
1048+
let is_child = parents.iter().any(|parent| {
1049+
let parent_bytes = parent.as_bytes();
1050+
let norm_bytes = normalized.as_bytes();
1051+
norm_bytes.starts_with(parent_bytes)
1052+
&& norm_bytes.len() > parent_bytes.len()
1053+
&& norm_bytes.get(parent_bytes.len()) == Some(&b'/')
1054+
});
1055+
1056+
if !is_child {
1057+
parents.insert(normalized.as_str());
1058+
filtered_results.push(path_data.clone());
10101059
}
10111060
}
1061+
1062+
return filtered_results;
10121063
}
10131064

10141065
results
@@ -1020,7 +1071,7 @@ impl Glob {
10201071
/// (e.g., `C:\foo\bar` → `//?/C:/foo/bar`) to match glob's behavior.
10211072
fn format_path(&self, path: &std::path::Path) -> String {
10221073
let path_str = path.to_string_lossy().to_string();
1023-
if self.posix {
1074+
if self.posix_explicit_true {
10241075
// On Windows with posix: true, convert absolute paths to UNC form
10251076
#[cfg(target_os = "windows")]
10261077
{
@@ -1118,7 +1169,7 @@ impl Glob {
11181169
buffer.clear();
11191170
let path_str = path.to_string_lossy();
11201171

1121-
if self.posix {
1172+
if self.posix_explicit_true {
11221173
// On Windows with posix: true, convert absolute paths to UNC form
11231174
// e.g., C:\foo\bar → //?/C:/foo/bar
11241175
#[cfg(target_os = "windows")]
@@ -1970,15 +2021,12 @@ impl Glob {
19702021

19712022
/// Check if backslashes should be normalized to forward slashes.
19722023
///
1973-
/// On Windows with posix: false (the default), glob v13 outputs backslashes.
1974-
/// On Windows with posix: true, glob v13 outputs forward slashes.
1975-
/// On Unix, glob v13 always outputs forward slashes.
2024+
/// glob v13 uses forward slashes by default on all platforms.
2025+
/// Only when posix is explicitly set to false on Windows are backslashes used.
19762026
#[inline]
19772027
fn should_normalize_backslashes(&self) -> bool {
1978-
// Use forward slashes when:
1979-
// - On Unix (always)
1980-
// - On Windows with posix: true
1981-
self.posix || !cfg!(target_os = "windows")
2028+
// Use forward slashes unless posix was explicitly set to false on Windows
2029+
!cfg!(target_os = "windows") || !self.posix_explicit_false
19822030
}
19832031

19842032
/// Normalize path separators based on platform and posix option.

0 commit comments

Comments
 (0)