Skip to content

Commit b63bf1b

Browse files
committed
fix: windows native path separators and nocase static pattern optimization
- disable static pattern fast path when nocase:true on case-sensitive filesystems (Linux), forcing proper directory scan for case-insensitive matching - add should_normalize_backslashes() helper to determine when to use forward slashes - on Windows with posix:false (default), keep native backslashes in output paths - update normalize_path(), build_result_path(), ensure_trailing_slash() to respect the posix option for path separators - fix path comparison functions to handle both / and \ separators
1 parent 5f0334e commit b63bf1b

1 file changed

Lines changed: 81 additions & 34 deletions

File tree

src/glob.rs

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,11 +1016,11 @@ impl Glob {
10161016
fn ensure_trailing_slash(&self, path: &str) -> String {
10171017
if path.ends_with('/') || path.ends_with('\\') {
10181018
path.to_string()
1019-
} else if self.posix || !cfg!(windows) {
1019+
} else if self.should_normalize_backslashes() {
10201020
format!("{path}/")
10211021
} else {
10221022
// On Windows without posix option, use the native separator
1023-
format!("{path}/")
1023+
format!("{path}\\")
10241024
}
10251025
}
10261026

@@ -1104,28 +1104,36 @@ impl Glob {
11041104
prefix_to_strip: &Option<String>,
11051105
is_walk_root: bool,
11061106
) -> Cow<'a, str> {
1107-
// Fast path: no prefix and no backslashes
1108-
if prefix_to_strip.is_none() && !rel_str_from_walk_root.contains('\\') {
1107+
let normalize = self.should_normalize_backslashes();
1108+
let has_backslash = rel_str_from_walk_root.contains('\\');
1109+
let needs_backslash_conversion = normalize && has_backslash;
1110+
1111+
// Fast path: no prefix and no conversion needed
1112+
if prefix_to_strip.is_none() && !needs_backslash_conversion {
11091113
return Cow::Borrowed(rel_str_from_walk_root);
11101114
}
11111115

1116+
// Determine the separator to use when building paths
1117+
let sep = if normalize { "/" } else { "\\" };
1118+
11121119
// Need to construct the path
11131120
match prefix_to_strip {
11141121
Some(prefix) => {
11151122
if is_walk_root {
11161123
Cow::Owned(prefix.clone())
1117-
} else if rel_str_from_walk_root.contains('\\') {
1124+
} else if needs_backslash_conversion {
11181125
Cow::Owned(format!(
1119-
"{}/{}",
1126+
"{}{}{}",
11201127
prefix,
1128+
sep,
11211129
rel_str_from_walk_root.replace('\\', "/")
11221130
))
11231131
} else {
1124-
Cow::Owned(format!("{prefix}/{rel_str_from_walk_root}"))
1132+
Cow::Owned(format!("{prefix}{sep}{rel_str_from_walk_root}"))
11251133
}
11261134
}
11271135
None => {
1128-
// Has backslashes, needs conversion
1136+
// Has backslashes and needs conversion
11291137
Cow::Owned(rel_str_from_walk_root.replace('\\', "/"))
11301138
}
11311139
}
@@ -1138,29 +1146,31 @@ impl Glob {
11381146
/// # Arguments
11391147
/// * `rel_str_from_walk_root` - The path relative to the walk root
11401148
/// * `prefix_to_strip` - The original prefix (without trailing slash)
1141-
/// * `prefix_with_slash` - Pre-computed "prefix/" for fast concatenation
1149+
/// * `prefix_with_slash` - Pre-computed "prefix/" or "prefix\\" for fast concatenation
11421150
/// * `is_walk_root` - True if this is the walk root entry itself
1151+
/// * `normalize_backslashes` - Whether to convert backslashes to forward slashes
11431152
/// * `buffer` - Reusable string buffer
11441153
#[inline]
11451154
fn normalize_path_buffered<'a>(
11461155
rel_str_from_walk_root: &str,
11471156
prefix_to_strip: &Option<String>,
11481157
prefix_with_slash: &Option<String>,
11491158
is_walk_root: bool,
1159+
normalize_backslashes: bool,
11501160
buffer: &'a mut String,
11511161
) -> &'a str {
1152-
// Fast path: no prefix and no backslashes - can't return borrowed reference
1153-
// from the input because we need to return from buffer for consistency
1162+
let has_backslash = rel_str_from_walk_root.contains('\\');
1163+
let needs_conversion = normalize_backslashes && has_backslash;
1164+
1165+
// Fast path: no prefix and no conversion needed
11541166
if prefix_to_strip.is_none() {
1155-
if !rel_str_from_walk_root.contains('\\') {
1156-
buffer.clear();
1157-
buffer.push_str(rel_str_from_walk_root);
1158-
return buffer.as_str();
1159-
}
1160-
// Has backslashes, needs conversion
11611167
buffer.clear();
1162-
for c in rel_str_from_walk_root.chars() {
1163-
buffer.push(if c == '\\' { '/' } else { c });
1168+
if needs_conversion {
1169+
for c in rel_str_from_walk_root.chars() {
1170+
buffer.push(if c == '\\' { '/' } else { c });
1171+
}
1172+
} else {
1173+
buffer.push_str(rel_str_from_walk_root);
11641174
}
11651175
return buffer.as_str();
11661176
}
@@ -1178,10 +1188,10 @@ impl Glob {
11781188
buffer.push_str(prefix_slash);
11791189
} else {
11801190
buffer.push_str(prefix);
1181-
buffer.push('/');
1191+
buffer.push(if normalize_backslashes { '/' } else { '\\' });
11821192
}
11831193

1184-
if rel_str_from_walk_root.contains('\\') {
1194+
if needs_conversion {
11851195
// Convert backslashes while appending
11861196
for c in rel_str_from_walk_root.chars() {
11871197
buffer.push(if c == '\\' { '/' } else { c });
@@ -1206,6 +1216,8 @@ impl Glob {
12061216
) -> String {
12071217
// When mark:true, add trailing slash to directories but NOT to symlinks
12081218
let should_mark_as_dir = is_dir && !is_symlink && self.mark;
1219+
let sep = if self.should_normalize_backslashes() { '/' } else { '\\' };
1220+
let dot_prefix = if self.should_normalize_backslashes() { "./" } else { ".\\" };
12091221

12101222
if self.absolute {
12111223
// Build absolute path
@@ -1215,16 +1227,16 @@ impl Glob {
12151227

12161228
if should_mark_as_dir && !formatted.ends_with('/') && !formatted.ends_with('\\') {
12171229
let mut result = formatted.to_string();
1218-
result.push('/');
1230+
result.push(sep);
12191231
result
12201232
} else {
12211233
formatted.to_string()
12221234
}
12231235
} else {
12241236
// Build relative path
1225-
let base = if self.dot_relative && !normalized.starts_with("../") {
1237+
let base = if self.dot_relative && !normalized.starts_with("../") && !normalized.starts_with("..\\") {
12261238
result_buffer.clear();
1227-
result_buffer.push_str("./");
1239+
result_buffer.push_str(dot_prefix);
12281240
result_buffer.push_str(normalized);
12291241
result_buffer.as_str()
12301242
} else {
@@ -1233,7 +1245,7 @@ impl Glob {
12331245

12341246
if should_mark_as_dir && !base.ends_with('/') && !base.ends_with('\\') {
12351247
let mut result = base.to_string();
1236-
result.push('/');
1248+
result.push(sep);
12371249
result
12381250
} else {
12391251
base.to_string()
@@ -1254,7 +1266,8 @@ impl Glob {
12541266
let ignored_bytes = ignored_dir.as_bytes();
12551267
normalized_bytes.starts_with(ignored_bytes)
12561268
&& (normalized_bytes.len() == ignored_bytes.len()
1257-
|| normalized_bytes.get(ignored_bytes.len()) == Some(&b'/'))
1269+
|| normalized_bytes.get(ignored_bytes.len()) == Some(&b'/')
1270+
|| normalized_bytes.get(ignored_bytes.len()) == Some(&b'\\'))
12581271
})
12591272
}
12601273

@@ -1271,7 +1284,8 @@ impl Glob {
12711284
let matched_bytes = matched_path.as_bytes();
12721285
normalized_bytes.starts_with(matched_bytes)
12731286
&& normalized_bytes.len() > matched_bytes.len()
1274-
&& normalized_bytes.get(matched_bytes.len()) == Some(&b'/')
1287+
&& (normalized_bytes.get(matched_bytes.len()) == Some(&b'/')
1288+
|| normalized_bytes.get(matched_bytes.len()) == Some(&b'\\'))
12751289
})
12761290
}
12771291

@@ -1811,6 +1825,12 @@ impl Glob {
18111825
/// resolve to a single path and can be checked with a direct stat() call
18121826
/// instead of walking the entire directory tree.
18131827
fn all_patterns_static(&self) -> bool {
1828+
// When nocase is true on a case-sensitive filesystem (Linux), we can't use
1829+
// the static pattern fast path because we need to scan directories to find
1830+
// case-insensitive matches.
1831+
if self.nocase && !self.is_case_insensitive_platform() {
1832+
return false;
1833+
}
18141834
!self.patterns.is_empty() && self.patterns.iter().all(|p| p.is_static())
18151835
}
18161836

@@ -1837,6 +1857,31 @@ impl Glob {
18371857
cfg!(target_os = "macos") || cfg!(target_os = "windows")
18381858
}
18391859

1860+
/// Check if backslashes should be normalized to forward slashes.
1861+
///
1862+
/// On Windows, when `posix: false` (the default), paths should use native backslashes
1863+
/// to match glob's behavior. On all other platforms, or when `posix: true`, we normalize
1864+
/// to forward slashes.
1865+
#[inline]
1866+
fn should_normalize_backslashes(&self) -> bool {
1867+
// On Windows with posix: false, keep backslashes
1868+
// Otherwise, normalize to forward slashes
1869+
self.posix || !cfg!(target_os = "windows")
1870+
}
1871+
1872+
/// Normalize path separators based on platform and posix option.
1873+
///
1874+
/// Returns the original string if no normalization is needed (either no backslashes,
1875+
/// or on Windows with posix: false where backslashes are kept).
1876+
#[inline]
1877+
fn normalize_separators<'a>(&self, path: &'a str) -> Cow<'a, str> {
1878+
if !self.should_normalize_backslashes() || !path.contains('\\') {
1879+
Cow::Borrowed(path)
1880+
} else {
1881+
Cow::Owned(path.replace('\\', "/"))
1882+
}
1883+
}
1884+
18401885
/// Resolve shallow patterns using direct readdir.
18411886
///
18421887
/// This is a fast path for patterns like `*.js` that only match at the root level.
@@ -1929,13 +1974,14 @@ impl Glob {
19291974
formatted
19301975
}
19311976
} else {
1977+
let sep = if self.should_normalize_backslashes() { '/' } else { '\\' };
19321978
let base = if self.dot_relative {
1933-
format!("./{file_name}")
1979+
format!(".{sep}{file_name}")
19341980
} else {
19351981
file_name.clone()
19361982
};
1937-
if self.mark && is_dir && !is_symlink && !base.ends_with('/') {
1938-
format!("{base}/")
1983+
if self.mark && is_dir && !is_symlink && !base.ends_with('/') && !base.ends_with('\\') {
1984+
format!("{base}{sep}")
19391985
} else {
19401986
base
19411987
}
@@ -2026,13 +2072,14 @@ impl Glob {
20262072
formatted
20272073
}
20282074
} else {
2029-
let base = if self.dot_relative && !base_path.starts_with("../") {
2030-
format!("./{base_path}")
2075+
let sep = if self.should_normalize_backslashes() { '/' } else { '\\' };
2076+
let base = if self.dot_relative && !base_path.starts_with("../") && !base_path.starts_with("..\\") {
2077+
format!(".{sep}{base_path}")
20312078
} else {
20322079
base_path.to_string()
20332080
};
2034-
if self.mark && is_dir && !is_symlink && !base.ends_with('/') {
2035-
format!("{base}/")
2081+
if self.mark && is_dir && !is_symlink && !base.ends_with('/') && !base.ends_with('\\') {
2082+
format!("{base}{sep}")
20362083
} else {
20372084
base
20382085
}

0 commit comments

Comments
 (0)