@@ -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
1213pub 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