@@ -152,11 +152,26 @@ pub(crate) fn simple_normalize_path(path: &std::path::Path) -> PathBuf {
152152 return ext;
153153 }
154154 Anchor :: Drive ( ref drive) => {
155- let mut ext = PathBuf :: from ( r"\\?\" ) ;
156- ext. push ( drive) ;
157155 if has_root_dir {
158- ext. push ( Component :: RootDir . as_os_str ( ) ) ;
156+ // Build `\\?\C:\...` in a single `PathBuf::from` so it parses
157+ // as `Prefix::VerbatimDisk`. The naive form —
158+ // `PathBuf::from(r"\\?\")` then `push(drive)` — silently
159+ // drops the verbatim marker and the result parses as
160+ // `Prefix::Disk`. That mismatch made the clamp output fail
161+ // `starts_with` against a true-verbatim anchor floor.
162+ let drive_str = drive. to_string_lossy ( ) ;
163+ let mut ext = PathBuf :: from ( format ! ( r"\\?\{drive_str}\" ) ) ;
164+ for seg in stack {
165+ ext. push ( seg) ;
166+ }
167+ return ext;
159168 }
169+ // Drive-relative (`C:foo`): preserve drive-relative semantics so
170+ // downstream `fs::canonicalize` can resolve against the
171+ // per-drive CWD. Adding a verbatim prefix here would turn it
172+ // into an absolute path rooted at `C:\`.
173+ let mut ext = PathBuf :: new ( ) ;
174+ ext. push ( drive) ;
160175 for seg in stack {
161176 ext. push ( seg) ;
162177 }
@@ -196,3 +211,65 @@ pub(crate) fn simple_normalize_path(path: &std::path::Path) -> PathBuf {
196211 result
197212 }
198213}
214+
215+ #[ cfg( all( test, windows) ) ]
216+ mod verbatim_prefix_regression {
217+ use super :: simple_normalize_path;
218+ use std:: path:: { Component , Path , Prefix } ;
219+
220+ fn prefix_kind ( p : & Path ) -> Option < Prefix < ' _ > > {
221+ p. components ( ) . next ( ) . and_then ( |c| match c {
222+ Component :: Prefix ( pc) => Some ( pc. kind ( ) ) ,
223+ _ => None ,
224+ } )
225+ }
226+
227+ #[ test]
228+ fn verbatim_disk_input_yields_verbatim_disk_output ( ) {
229+ let input = Path :: new ( r"\\?\C:\Users\runneradmin\AppData\Local\Temp\.tmpAAAA\home\jail" ) ;
230+ let out = simple_normalize_path ( input) ;
231+ match prefix_kind ( & out) {
232+ Some ( Prefix :: VerbatimDisk ( b'C' ) ) => { }
233+ other => panic ! (
234+ "simple_normalize_path must preserve VerbatimDisk; got {:?} for output {:?}" ,
235+ other, out
236+ ) ,
237+ }
238+ }
239+
240+ #[ test]
241+ fn verbatim_disk_with_dotdot_still_yields_verbatim_disk ( ) {
242+ // Simulates the anchored-symlink flow: a verbatim path with `..` segments
243+ // that collapse but still leave a non-trivial tail. The output prefix
244+ // must remain `VerbatimDisk` so callers using `Path::starts_with` against
245+ // a verbatim anchor floor get the match they expect.
246+ let input = Path :: new ( r"\\?\C:\a\b\c\..\..\d\e" ) ;
247+ let out = simple_normalize_path ( input) ;
248+ match prefix_kind ( & out) {
249+ Some ( Prefix :: VerbatimDisk ( b'C' ) ) => { }
250+ other => panic ! (
251+ "simple_normalize_path must preserve VerbatimDisk under `..`; got {:?} for output {:?}" ,
252+ other, out
253+ ) ,
254+ }
255+ }
256+
257+ #[ test]
258+ fn disk_starts_with_verbatim_disk_is_false_in_stdlib ( ) {
259+ // Pin the stdlib behavior this fix depends on: `Path::starts_with` is
260+ // component-based and treats `Prefix::Disk('C')` and `Prefix::VerbatimDisk('C')`
261+ // as non-equal. If this ever changes in stdlib, the regression above
262+ // becomes moot — but we want to know.
263+ let disk = Path :: new ( r"C:\foo\bar" ) ;
264+ let verbatim = Path :: new ( r"\\?\C:\foo\bar" ) ;
265+ assert ! ( matches!( prefix_kind( disk) , Some ( Prefix :: Disk ( _) ) ) ) ;
266+ assert ! ( matches!(
267+ prefix_kind( verbatim) ,
268+ Some ( Prefix :: VerbatimDisk ( _) )
269+ ) ) ;
270+ assert ! (
271+ !disk. starts_with( verbatim) ,
272+ "stdlib changed: Disk now matches VerbatimDisk in starts_with — revisit clamp logic"
273+ ) ;
274+ }
275+ }
0 commit comments