Skip to content

Commit cd2d0ee

Browse files
committed
fix: preserve VerbatimDisk prefix in simple_normalize_path
Building `\\?\` then pushing a drive letter silently drops the verbatim marker, producing a `Prefix::Disk` output. `Path::starts_with` treats `Disk` and `VerbatimDisk` as distinct, so anchored-symlink clamp checks against a verbatim floor would fail. Fix by constructing the verbatim path as a single `PathBuf::from(format!(r"\\?\{drive}\"))` string. Drive-relative paths (`C:foo`) skip the verbatim prefix to preserve per-drive CWD semantics. Adds regression tests pinning the new behavior and the stdlib `starts_with` invariant this fix depends on.
1 parent b4cda2c commit cd2d0ee

1 file changed

Lines changed: 80 additions & 3 deletions

File tree

src/normalize.rs

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)