Skip to content

Commit aea2de4

Browse files
committed
fix Windows-specific path normalization using GetLongPathNameW
1 parent 7b6a321 commit aea2de4

3 files changed

Lines changed: 67 additions & 18 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pet-fs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ license = "MIT"
66

77
[target.'cfg(target_os = "windows")'.dependencies]
88
msvc_spectre_libs = { version = "0.1.1", features = ["error"] }
9+
windows-sys = { version = "0.59", features = ["Win32_Storage_FileSystem", "Win32_Foundation"] }
910

1011
[dependencies]
1112
log = "0.4.21"

crates/pet-fs/src/path.rs

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ use std::{
66
path::{Path, PathBuf},
77
};
88

9-
// Similar to fs::canonicalize, but ignores UNC paths and returns the path as is (for windows).
10-
// Usefulfor windows to ensure we have the paths in the right casing.
9+
// Normalizes the case of a path on Windows without resolving junctions/symlinks.
10+
// Uses GetLongPathNameW which normalizes case but preserves junction paths.
1111
// For unix, this is a noop.
12+
// See: https://github.com/microsoft/python-environment-tools/issues/186
1213
pub fn norm_case<P: AsRef<Path>>(path: P) -> PathBuf {
1314
// On unix do not use canonicalize, results in weird issues with homebrew paths
1415
// Even readlink does the same thing
@@ -18,24 +19,70 @@ pub fn norm_case<P: AsRef<Path>>(path: P) -> PathBuf {
1819
return path.as_ref().to_path_buf();
1920

2021
#[cfg(windows)]
21-
use std::fs;
22-
23-
#[cfg(windows)]
24-
if let Ok(resolved) = fs::canonicalize(&path) {
25-
if cfg!(unix) {
26-
return resolved;
27-
}
28-
// Windows specific handling, https://github.com/rust-lang/rust/issues/42869
29-
let has_unc_prefix = path.as_ref().to_string_lossy().starts_with(r"\\?\");
30-
if resolved.to_string_lossy().starts_with(r"\\?\") && !has_unc_prefix {
31-
// If the resolved path has a UNC prefix, but the original path did not,
32-
// we need to remove the UNC prefix.
33-
PathBuf::from(resolved.to_string_lossy().trim_start_matches(r"\\?\"))
22+
{
23+
// First, convert to absolute path if relative, without resolving symlinks/junctions
24+
let absolute_path = if path.as_ref().is_absolute() {
25+
path.as_ref().to_path_buf()
26+
} else if let Ok(abs) = std::env::current_dir() {
27+
abs.join(path.as_ref())
3428
} else {
35-
resolved
36-
}
29+
path.as_ref().to_path_buf()
30+
};
31+
32+
// Use GetLongPathNameW to normalize case without resolving junctions
33+
normalize_case_windows(&absolute_path).unwrap_or_else(|| path.as_ref().to_path_buf())
34+
}
35+
}
36+
37+
/// Windows-specific path case normalization using GetLongPathNameW.
38+
/// This normalizes the case of path components but does NOT resolve junctions or symlinks.
39+
#[cfg(windows)]
40+
fn normalize_case_windows(path: &Path) -> Option<PathBuf> {
41+
use std::ffi::OsString;
42+
use std::os::windows::ffi::{OsStrExt, OsStringExt};
43+
use windows_sys::Win32::Storage::FileSystem::GetLongPathNameW;
44+
45+
// Convert path to wide string (UTF-16) with null terminator
46+
let wide_path: Vec<u16> = path
47+
.as_os_str()
48+
.encode_wide()
49+
.chain(std::iter::once(0))
50+
.collect();
51+
52+
// First call to get required buffer size
53+
let required_len = unsafe { GetLongPathNameW(wide_path.as_ptr(), std::ptr::null_mut(), 0) };
54+
55+
if required_len == 0 {
56+
// GetLongPathNameW failed, return None
57+
return None;
58+
}
59+
60+
// Allocate buffer and get the normalized path
61+
let mut buffer: Vec<u16> = vec![0; required_len as usize];
62+
let actual_len =
63+
unsafe { GetLongPathNameW(wide_path.as_ptr(), buffer.as_mut_ptr(), required_len) };
64+
65+
if actual_len == 0 || actual_len > required_len {
66+
// Call failed or buffer too small
67+
return None;
68+
}
69+
70+
// Truncate buffer to actual length (excluding null terminator)
71+
buffer.truncate(actual_len as usize);
72+
73+
// Convert back to PathBuf
74+
let os_string = OsString::from_wide(&buffer);
75+
let result = PathBuf::from(os_string);
76+
77+
// Remove UNC prefix if original path didn't have it
78+
// GetLongPathNameW may add \\?\ prefix in some cases
79+
let result_str = result.to_string_lossy();
80+
let original_has_unc = path.to_string_lossy().starts_with(r"\\?\");
81+
82+
if result_str.starts_with(r"\\?\") && !original_has_unc {
83+
Some(PathBuf::from(result_str.trim_start_matches(r"\\?\")))
3784
} else {
38-
path.as_ref().to_path_buf()
85+
Some(result)
3986
}
4087
}
4188

0 commit comments

Comments
 (0)