Skip to content

Commit 19bde8e

Browse files
committed
fix: always output forward slashes for consistent cross-platform behavior
1 parent 60eb13a commit 19bde8e

1 file changed

Lines changed: 66 additions & 116 deletions

File tree

src/glob.rs

Lines changed: 66 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,26 +1009,29 @@ impl Glob {
10091009

10101010
/// Format a path according to options (posix, etc.)
10111011
///
1012+
/// Always converts backslashes to forward slashes for consistent output.
10121013
/// When posix: true on Windows, absolute paths are converted to UNC form
10131014
/// (e.g., `C:\foo\bar` → `//?/C:/foo/bar`) to match glob's behavior.
10141015
fn format_path(&self, path: &std::path::Path) -> String {
10151016
let path_str = path.to_string_lossy().to_string();
1017+
1018+
// On Windows with posix: true, convert absolute paths to UNC form
1019+
#[cfg(target_os = "windows")]
10161020
if self.posix {
1017-
// On Windows with posix: true, convert absolute paths to UNC form
1018-
#[cfg(target_os = "windows")]
1021+
let bytes = path_str.as_bytes();
1022+
if bytes.len() >= 2
1023+
&& bytes[0].is_ascii_alphabetic()
1024+
&& (bytes[1] == b':' || bytes[1] == b'\\' || bytes[1] == b'/')
10191025
{
1020-
let bytes = path_str.as_bytes();
1021-
if bytes.len() >= 2
1022-
&& bytes[0].is_ascii_alphabetic()
1023-
&& (bytes[1] == b':' || bytes[1] == b'\\' || bytes[1] == b'/')
1024-
{
1025-
// Convert to UNC form: //?/C:/...
1026-
let drive = bytes[0] as char;
1027-
let rest = path_str[2..].replace('\\', "/");
1028-
return format!("//?/{drive}:{rest}");
1029-
}
1026+
// Convert to UNC form: //?/C:/...
1027+
let drive = bytes[0] as char;
1028+
let rest = path_str[2..].replace('\\', "/");
1029+
return format!("//?/{drive}:{rest}");
10301030
}
1031-
// Standard POSIX conversion: backslashes to forward slashes
1031+
}
1032+
1033+
// Always convert backslashes to forward slashes for consistent output
1034+
if path_str.contains('\\') {
10321035
path_str.replace('\\', "/")
10331036
} else {
10341037
path_str
@@ -1039,11 +1042,8 @@ impl Glob {
10391042
fn ensure_trailing_slash(&self, path: &str) -> String {
10401043
if path.ends_with('/') || path.ends_with('\\') {
10411044
path.to_string()
1042-
} else if self.should_normalize_backslashes() {
1043-
format!("{path}/")
10441045
} else {
1045-
// On Windows without posix option, use the native separator
1046-
format!("{path}\\")
1046+
format!("{path}/")
10471047
}
10481048
}
10491049

@@ -1104,37 +1104,38 @@ impl Glob {
11041104
/// Format a path into the provided buffer, returning a reference to the result.
11051105
/// This avoids allocations by reusing the buffer across iterations.
11061106
///
1107+
/// Always converts backslashes to forward slashes for consistent output.
11071108
/// When posix: true on Windows, absolute paths are converted to UNC form
11081109
/// (e.g., `C:\foo\bar` → `//?/C:/foo/bar`) to match glob's behavior.
11091110
#[inline]
11101111
fn format_path_into_buffer<'a>(&self, path: &Path, buffer: &'a mut String) -> &'a str {
11111112
buffer.clear();
11121113
let path_str = path.to_string_lossy();
11131114

1115+
// On Windows with posix: true, convert absolute paths to UNC form
1116+
// e.g., C:\foo\bar → //?/C:/foo/bar
1117+
#[cfg(target_os = "windows")]
11141118
if self.posix {
1115-
// On Windows with posix: true, convert absolute paths to UNC form
1116-
// e.g., C:\foo\bar → //?/C:/foo/bar
1117-
#[cfg(target_os = "windows")]
1119+
// Check if this is a Windows absolute path (starts with drive letter)
1120+
let bytes = path_str.as_bytes();
1121+
if bytes.len() >= 2
1122+
&& bytes[0].is_ascii_alphabetic()
1123+
&& (bytes[1] == b':' || bytes[1] == b'\\' || bytes[1] == b'/')
11181124
{
1119-
// Check if this is a Windows absolute path (starts with drive letter)
1120-
let bytes = path_str.as_bytes();
1121-
if bytes.len() >= 2
1122-
&& bytes[0].is_ascii_alphabetic()
1123-
&& (bytes[1] == b':' || bytes[1] == b'\\' || bytes[1] == b'/')
1124-
{
1125-
// Convert to UNC form: //?/C:/...
1126-
buffer.push_str("//?/");
1127-
buffer.push(bytes[0] as char);
1128-
buffer.push(':');
1129-
// Skip the drive letter and colon, convert rest with forward slashes
1130-
for c in path_str[2..].chars() {
1131-
buffer.push(if c == '\\' { '/' } else { c });
1132-
}
1133-
return buffer.as_str();
1125+
// Convert to UNC form: //?/C:/...
1126+
buffer.push_str("//?/");
1127+
buffer.push(bytes[0] as char);
1128+
buffer.push(':');
1129+
// Skip the drive letter and colon, convert rest with forward slashes
1130+
for c in path_str[2..].chars() {
1131+
buffer.push(if c == '\\' { '/' } else { c });
11341132
}
1133+
return buffer.as_str();
11351134
}
1135+
}
11361136

1137-
// Standard POSIX conversion: backslashes to forward slashes
1137+
// Always convert backslashes to forward slashes for consistent output
1138+
if path_str.contains('\\') {
11381139
for c in path_str.chars() {
11391140
buffer.push(if c == '\\' { '/' } else { c });
11401141
}
@@ -1294,7 +1295,7 @@ impl Glob {
12941295
/// Uses the provided buffer to minimize allocations.
12951296
///
12961297
/// The `normalized` path always uses forward slashes (for internal pattern matching).
1297-
/// This function converts to backslashes for output on Windows when `posix: false`.
1298+
/// Output also uses forward slashes for consistent cross-platform behavior.
12981299
#[inline]
12991300
fn build_result_path(
13001301
&self,
@@ -1306,47 +1307,34 @@ impl Glob {
13061307
) -> String {
13071308
// When mark:true, add trailing slash to directories but NOT to symlinks
13081309
let should_mark_as_dir = is_dir && !is_symlink && self.mark;
1309-
let use_forward = self.should_normalize_backslashes();
1310-
let sep = if use_forward { '/' } else { '\\' };
1311-
let dot_prefix = if use_forward { "./" } else { ".\\" };
13121310

13131311
if self.absolute {
13141312
// Build absolute path
13151313
result_buffer.clear();
13161314
let abs_path = abs_cwd.join(normalized);
13171315
let formatted = self.format_path_into_buffer(&abs_path, result_buffer);
13181316

1319-
if should_mark_as_dir && !formatted.ends_with('/') && !formatted.ends_with('\\') {
1317+
if should_mark_as_dir && !formatted.ends_with('/') {
13201318
let mut result = formatted.to_string();
1321-
result.push(sep);
1319+
result.push('/');
13221320
result
13231321
} else {
13241322
formatted.to_string()
13251323
}
13261324
} else {
1327-
// 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-
1335-
let base = if self.dot_relative
1336-
&& !output_normalized.starts_with("../")
1337-
&& !output_normalized.starts_with("..\\")
1338-
{
1325+
// Build relative path (normalized already uses forward slashes)
1326+
let base = if self.dot_relative && !normalized.starts_with("../") {
13391327
result_buffer.clear();
1340-
result_buffer.push_str(dot_prefix);
1341-
result_buffer.push_str(&output_normalized);
1328+
result_buffer.push_str("./");
1329+
result_buffer.push_str(normalized);
13421330
result_buffer.clone()
13431331
} else {
1344-
output_normalized
1332+
normalized.to_string()
13451333
};
13461334

1347-
if should_mark_as_dir && !base.ends_with('/') && !base.ends_with('\\') {
1335+
if should_mark_as_dir && !base.ends_with('/') {
13481336
let mut result = base;
1349-
result.push(sep);
1337+
result.push('/');
13501338
result
13511339
} else {
13521340
base
@@ -1960,38 +1948,25 @@ impl Glob {
19601948

19611949
/// Check if backslashes should be normalized to forward slashes.
19621950
///
1963-
/// On Windows, when `posix: false` (the default), paths should use native backslashes
1964-
/// to match glob's behavior. On all other platforms, or when `posix: true`, we normalize
1965-
/// to forward slashes.
1951+
/// Always returns true - globlin always outputs forward slashes for consistent
1952+
/// cross-platform behavior. This differs slightly from glob v13 which uses
1953+
/// backslashes on Windows by default, but provides better UX for most use cases.
19661954
#[inline]
19671955
fn should_normalize_backslashes(&self) -> bool {
1968-
// On Windows with posix: false, keep backslashes
1969-
// Otherwise, normalize to forward slashes
1970-
self.posix || !cfg!(target_os = "windows")
1956+
// Always normalize to forward slashes for consistent cross-platform output
1957+
true
19711958
}
19721959

1973-
/// Normalize path separators based on platform and posix option.
1974-
///
1975-
/// When use_forward_slashes is true: converts backslashes to forward slashes
1976-
/// When use_forward_slashes is false: converts forward slashes to backslashes
1960+
/// Normalize path separators to forward slashes.
19771961
///
1962+
/// Always converts backslashes to forward slashes for consistent output.
19781963
/// Returns the original string if no conversion is needed.
19791964
#[inline]
19801965
fn normalize_separators<'a>(&self, path: &'a str) -> Cow<'a, str> {
1981-
let use_forward = self.should_normalize_backslashes();
1982-
if use_forward {
1983-
if !path.contains('\\') {
1984-
Cow::Borrowed(path)
1985-
} else {
1986-
Cow::Owned(path.replace('\\', "/"))
1987-
}
1966+
if !path.contains('\\') {
1967+
Cow::Borrowed(path)
19881968
} else {
1989-
// On Windows with posix: false, convert forward slashes to backslashes
1990-
if !path.contains('/') {
1991-
Cow::Borrowed(path)
1992-
} else {
1993-
Cow::Owned(path.replace('/', "\\"))
1994-
}
1969+
Cow::Owned(path.replace('\\', "/"))
19951970
}
19961971
}
19971972

@@ -2083,23 +2058,13 @@ impl Glob {
20832058
formatted
20842059
}
20852060
} else {
2086-
let sep = if self.should_normalize_backslashes() {
2087-
'/'
2088-
} else {
2089-
'\\'
2090-
};
20912061
let base = if self.dot_relative {
2092-
format!(".{sep}{file_name}")
2062+
format!("./{file_name}")
20932063
} else {
20942064
file_name.clone()
20952065
};
2096-
if self.mark
2097-
&& is_dir
2098-
&& !is_symlink
2099-
&& !base.ends_with('/')
2100-
&& !base.ends_with('\\')
2101-
{
2102-
format!("{base}{sep}")
2066+
if self.mark && is_dir && !is_symlink && !base.ends_with('/') {
2067+
format!("{base}/")
21032068
} else {
21042069
base
21052070
}
@@ -2186,29 +2151,14 @@ impl Glob {
21862151
formatted
21872152
}
21882153
} else {
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()
2154+
// Static path already uses forward slashes internally
2155+
let base = if self.dot_relative && !base_path.starts_with("../") {
2156+
format!("./{base_path}")
21942157
} else {
2195-
base_path.replace('/', "\\")
2196-
};
2197-
let base = if self.dot_relative
2198-
&& !output_base.starts_with("../")
2199-
&& !output_base.starts_with("..\\")
2200-
{
2201-
format!(".{sep}{output_base}")
2202-
} else {
2203-
output_base
2158+
base_path.to_string()
22042159
};
2205-
if self.mark
2206-
&& is_dir
2207-
&& !is_symlink
2208-
&& !base.ends_with('/')
2209-
&& !base.ends_with('\\')
2210-
{
2211-
format!("{base}{sep}")
2160+
if self.mark && is_dir && !is_symlink && !base.ends_with('/') {
2161+
format!("{base}/")
22122162
} else {
22132163
base
22142164
}

0 commit comments

Comments
 (0)