Skip to content

Commit 60eb13a

Browse files
committed
fix: always use forward slashes internally for pattern matching on windows
1 parent 75db6cc commit 60eb13a

1 file changed

Lines changed: 70 additions & 61 deletions

File tree

src/glob.rs

Lines changed: 70 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -981,15 +981,21 @@ impl Glob {
981981
if matches {
982982
// For withFileTypes, we return the relative path (no dotRelative/mark modifications)
983983
// The JavaScript wrapper handles path formatting via PathScurry
984-
let normalized_string = normalized.into_owned();
985-
if seen.insert(normalized_string.clone()) {
984+
// Convert separators for output: use backslashes on Windows without posix
985+
let output_path = if self.should_normalize_backslashes() {
986+
normalized.into_owned()
987+
} else {
988+
normalized.replace('/', "\\")
989+
};
990+
if seen.insert(output_path.clone()) {
986991
// When includeChildMatches is false, track this path to exclude its children
992+
// (use the normalized path with forward slashes for internal tracking)
987993
if !self.include_child_matches {
988-
matched_parents.insert(normalized_string.clone());
994+
matched_parents.insert(output_path.replace('\\', "/"));
989995
}
990996

991997
results.push(PathData {
992-
path: normalized_string,
998+
path: output_path,
993999
is_directory: is_dir,
9941000
is_file: entry.is_file(),
9951001
is_symlink: entry.is_symlink(),
@@ -1140,60 +1146,50 @@ impl Glob {
11401146

11411147
/// Build a normalized path from walk entry, minimizing allocations.
11421148
/// Returns Cow::Borrowed when no transformation is needed, Cow::Owned otherwise.
1149+
///
1150+
/// IMPORTANT: This function ALWAYS normalizes to forward slashes because it's used
1151+
/// for internal pattern matching. Pattern matching (`matches()`, `could_match_in_dir()`)
1152+
/// always expects forward slashes regardless of platform.
1153+
///
1154+
/// Output formatting (backslashes on Windows without posix) is handled separately
1155+
/// in `build_result_path`.
11431156
#[inline]
11441157
fn normalize_path<'a>(
11451158
&self,
11461159
rel_str_from_walk_root: &'a str,
11471160
prefix_to_strip: &Option<String>,
11481161
is_walk_root: bool,
11491162
) -> Cow<'a, str> {
1150-
let use_forward_slashes = self.should_normalize_backslashes();
11511163
let has_backslash = rel_str_from_walk_root.contains('\\');
1152-
let has_forward_slash = rel_str_from_walk_root.contains('/');
11531164

1154-
// Determine what conversions are needed
1155-
let needs_to_forward = use_forward_slashes && has_backslash;
1156-
let needs_to_backslash = !use_forward_slashes && has_forward_slash;
1157-
let needs_conversion = needs_to_forward || needs_to_backslash;
1158-
1159-
// Fast path: no prefix and no conversion needed
1160-
if prefix_to_strip.is_none() && !needs_conversion {
1165+
// Fast path: no prefix and no backslashes to convert
1166+
if prefix_to_strip.is_none() && !has_backslash {
11611167
return Cow::Borrowed(rel_str_from_walk_root);
11621168
}
11631169

1164-
// Determine the separator to use when building paths
1165-
let sep = if use_forward_slashes { "/" } else { "\\" };
1166-
1167-
// Helper to convert path separators
1168-
let convert_path = |path: &str| -> String {
1169-
if needs_to_forward {
1170+
// Helper to convert backslashes to forward slashes
1171+
let convert_to_forward = |path: &str| -> String {
1172+
if path.contains('\\') {
11701173
path.replace('\\', "/")
1171-
} else if needs_to_backslash {
1172-
path.replace('/', "\\")
11731174
} else {
11741175
path.to_string()
11751176
}
11761177
};
11771178

1178-
// Need to construct the path
1179+
// Need to construct the path with forward slashes
11791180
match prefix_to_strip {
11801181
Some(prefix) => {
1182+
let prefix_converted = convert_to_forward(prefix);
11811183
if is_walk_root {
1182-
Cow::Owned(convert_path(prefix))
1183-
} else if needs_conversion {
1184-
Cow::Owned(format!(
1185-
"{}{}{}",
1186-
convert_path(prefix),
1187-
sep,
1188-
convert_path(rel_str_from_walk_root)
1189-
))
1184+
Cow::Owned(prefix_converted)
11901185
} else {
1191-
Cow::Owned(format!("{prefix}{sep}{rel_str_from_walk_root}"))
1186+
let rel_converted = convert_to_forward(rel_str_from_walk_root);
1187+
Cow::Owned(format!("{prefix_converted}/{rel_converted}"))
11921188
}
11931189
}
11941190
None => {
1195-
// Needs separator conversion
1196-
Cow::Owned(convert_path(rel_str_from_walk_root))
1191+
// Just convert backslashes to forward slashes
1192+
Cow::Owned(convert_to_forward(rel_str_from_walk_root))
11971193
}
11981194
}
11991195
}
@@ -1296,6 +1292,9 @@ impl Glob {
12961292

12971293
/// Build the final result path from the normalized path.
12981294
/// Uses the provided buffer to minimize allocations.
1295+
///
1296+
/// The `normalized` path always uses forward slashes (for internal pattern matching).
1297+
/// This function converts to backslashes for output on Windows when `posix: false`.
12991298
#[inline]
13001299
fn build_result_path(
13011300
&self,
@@ -1307,16 +1306,9 @@ impl Glob {
13071306
) -> String {
13081307
// When mark:true, add trailing slash to directories but NOT to symlinks
13091308
let should_mark_as_dir = is_dir && !is_symlink && self.mark;
1310-
let sep = if self.should_normalize_backslashes() {
1311-
'/'
1312-
} else {
1313-
'\\'
1314-
};
1315-
let dot_prefix = if self.should_normalize_backslashes() {
1316-
"./"
1317-
} else {
1318-
".\\"
1319-
};
1309+
let use_forward = self.should_normalize_backslashes();
1310+
let sep = if use_forward { '/' } else { '\\' };
1311+
let dot_prefix = if use_forward { "./" } else { ".\\" };
13201312

13211313
if self.absolute {
13221314
// Build absolute path
@@ -1333,24 +1325,31 @@ impl Glob {
13331325
}
13341326
} else {
13351327
// Build relative path
1328+
// First, convert separators if needed (normalized always uses forward slashes)
1329+
let output_normalized = if use_forward {
1330+
normalized.to_string()
1331+
} else {
1332+
normalized.replace('/', "\\")
1333+
};
1334+
13361335
let base = if self.dot_relative
1337-
&& !normalized.starts_with("../")
1338-
&& !normalized.starts_with("..\\")
1336+
&& !output_normalized.starts_with("../")
1337+
&& !output_normalized.starts_with("..\\")
13391338
{
13401339
result_buffer.clear();
13411340
result_buffer.push_str(dot_prefix);
1342-
result_buffer.push_str(normalized);
1343-
result_buffer.as_str()
1341+
result_buffer.push_str(&output_normalized);
1342+
result_buffer.clone()
13441343
} else {
1345-
normalized
1344+
output_normalized
13461345
};
13471346

13481347
if should_mark_as_dir && !base.ends_with('/') && !base.ends_with('\\') {
1349-
let mut result = base.to_string();
1348+
let mut result = base;
13501349
result.push(sep);
13511350
result
13521351
} else {
1353-
base.to_string()
1352+
base
13541353
}
13551354
}
13561355
}
@@ -2187,18 +2186,21 @@ impl Glob {
21872186
formatted
21882187
}
21892188
} else {
2190-
let sep = if self.should_normalize_backslashes() {
2191-
'/'
2189+
let use_forward = self.should_normalize_backslashes();
2190+
let sep = if use_forward { '/' } else { '\\' };
2191+
// Convert separators for output (static_path uses forward slashes internally)
2192+
let output_base = if use_forward {
2193+
base_path.to_string()
21922194
} else {
2193-
'\\'
2195+
base_path.replace('/', "\\")
21942196
};
21952197
let base = if self.dot_relative
2196-
&& !base_path.starts_with("../")
2197-
&& !base_path.starts_with("..\\")
2198+
&& !output_base.starts_with("../")
2199+
&& !output_base.starts_with("..\\")
21982200
{
2199-
format!(".{sep}{base_path}")
2201+
format!(".{sep}{output_base}")
22002202
} else {
2201-
base_path.to_string()
2203+
output_base
22022204
};
22032205
if self.mark
22042206
&& is_dir
@@ -2623,14 +2625,21 @@ impl Glob {
26232625
};
26242626

26252627
if matches {
2626-
let normalized_string = normalized.into_owned();
2627-
if seen.insert(normalized_string.clone()) {
2628+
// Convert separators for output: use backslashes on Windows without posix
2629+
let output_path = if self.should_normalize_backslashes() {
2630+
normalized.into_owned()
2631+
} else {
2632+
normalized.replace('/', "\\")
2633+
};
2634+
if seen.insert(output_path.clone()) {
2635+
// When includeChildMatches is false, track this path to exclude its children
2636+
// (use the normalized path with forward slashes for internal tracking)
26282637
if !self.include_child_matches {
2629-
matched_parents.insert(normalized_string.clone());
2638+
matched_parents.insert(output_path.replace('\\', "/"));
26302639
}
26312640

26322641
callback(PathData {
2633-
path: normalized_string,
2642+
path: output_path,
26342643
is_directory: is_dir,
26352644
is_file: entry.is_file(),
26362645
is_symlink: entry.is_symlink(),

0 commit comments

Comments
 (0)