Skip to content

Commit ae8408c

Browse files
wan9chicodex
andcommitted
refactor(path): normalize absolute strip_prefix
Co-authored-by: GPT-5 Codex <codex@openai.com>
1 parent 4aa62ec commit ae8408c

6 files changed

Lines changed: 138 additions & 48 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/fspy_shared/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ shared_memory = { workspace = true, features = ["logging"] }
1616
thiserror = { workspace = true }
1717
tracing = { workspace = true }
1818
uuid = { workspace = true, features = ["v4"] }
19+
vite_path = { workspace = true }
1920

2021
[target.'cfg(target_os = "windows")'.dependencies]
2122
bytemuck = { workspace = true }
22-
os_str_bytes = { workspace = true }
2323
winapi = { workspace = true, features = ["std"] }
2424

2525
[dev-dependencies]

crates/fspy_shared/src/ipc/native_path.rs

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
#[cfg(unix)]
2+
use std::ffi::OsStr;
3+
#[cfg(unix)]
24
use std::os::unix::ffi::OsStrExt as _;
35
use std::{
4-
ffi::OsStr,
56
fmt::Debug,
67
mem::MaybeUninit,
78
path::{Path, StripPrefixError},
@@ -70,41 +71,8 @@ impl NativePath {
7071
base: P,
7172
f: F,
7273
) -> R {
73-
/// Strip the `\\?\`, `\\.\`, `\??\` prefix from a Windows path, if present.
74-
/// Does nothing on non-Windows platforms.
75-
///
76-
/// \\?\ and \\.\ are used to enable long paths and access to device paths.
77-
/// \??\ is used in Nt* calls.
78-
/// The resulting path is not necessarily valid or points to the same location,
79-
/// but it's good enough for sanitizing paths in `NativePath::strip_path_prefix`.
80-
#[cfg_attr(
81-
not(windows),
82-
expect(
83-
clippy::missing_const_for_fn,
84-
reason = "uses non-const for loop and strip_prefix on Windows"
85-
)
86-
)]
87-
fn strip_windows_path_prefix(p: &OsStr) -> &OsStr {
88-
#[cfg(windows)]
89-
{
90-
use os_str_bytes::OsStrBytesExt as _;
91-
for prefix in [r"\\?\", r"\\.\", r"\??\"] {
92-
if let Some(stripped) = p.strip_prefix(prefix) {
93-
return stripped;
94-
}
95-
}
96-
p
97-
}
98-
#[cfg(not(windows))]
99-
{
100-
p
101-
}
102-
}
103-
10474
let me = self.inner.to_cow_os_str();
105-
let me = strip_windows_path_prefix(&me);
106-
let base = strip_windows_path_prefix(base.as_ref().as_os_str());
107-
f(Path::new(me).strip_prefix(base))
75+
f(vite_path::strip_path_prefix(&me, base.as_ref().as_os_str()))
10876
}
10977
}
11078

crates/vite_path/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ thiserror = { workspace = true }
1717
ts-rs = { workspace = true, optional = true }
1818
vite_str = { workspace = true }
1919

20+
[target.'cfg(target_os = "windows")'.dependencies]
21+
os_str_bytes = { workspace = true }
22+
2023
[features]
2124
absolute-redaction = []
2225
ts-rs = ["dep:ts-rs", "vite_str/ts-rs"]

crates/vite_path/src/absolute/mod.rs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ impl AbsolutePath {
148148

149149
/// Returns a path that, when joined onto base, yields self.
150150
///
151+
/// On Windows, path namespace prefixes (`\\?\`, `\\.\`, and `\??\`) are
152+
/// ignored before matching. In that case the returned path round-trips
153+
/// against the prefix-normalized paths rather than necessarily preserving
154+
/// the original namespace prefix.
155+
///
151156
/// If `base` is not a prefix of `self`, returns [`None`].
152157
///
153158
/// If the stripped path is not a valid `RelativePath`. Returns an error with the reason and the stripped path.
@@ -160,18 +165,12 @@ impl AbsolutePath {
160165
base: P,
161166
) -> Result<Option<RelativePathBuf>, StripPrefixError<'_>> {
162167
let base = base.as_ref();
163-
let Ok(stripped_path) = self.0.strip_prefix(&base.0) else {
168+
let Ok(stripped_path) =
169+
crate::strip_path_prefix(self.as_path().as_os_str(), base.as_path().as_os_str())
170+
else {
164171
return Ok(None);
165172
};
166-
match RelativePathBuf::new(stripped_path) {
167-
Ok(relative_path) => Ok(Some(relative_path)),
168-
Err(FromPathError::NonRelative) => {
169-
unreachable!("stripped path should always be relative")
170-
}
171-
Err(FromPathError::InvalidPathData(invalid_path_data_error)) => {
172-
Err(StripPrefixError { stripped_path, invalid_path_data_error })
173-
}
174-
}
173+
stripped_path_to_relative(stripped_path).map(Some)
175174
}
176175

177176
/// Creates an owned [`AbsolutePathBuf`] with `path` adjoined to `self`.
@@ -220,6 +219,20 @@ impl AbsolutePath {
220219
}
221220
}
222221

222+
fn stripped_path_to_relative(
223+
stripped_path: &Path,
224+
) -> Result<RelativePathBuf, StripPrefixError<'_>> {
225+
match RelativePathBuf::new(stripped_path) {
226+
Ok(relative_path) => Ok(relative_path),
227+
Err(FromPathError::NonRelative) => {
228+
unreachable!("stripped path should always be relative")
229+
}
230+
Err(FromPathError::InvalidPathData(invalid_path_data_error)) => {
231+
Err(StripPrefixError { stripped_path, invalid_path_data_error })
232+
}
233+
}
234+
}
235+
223236
/// An Error returned from [`AbsolutePath::strip_prefix`] if the stripped path is not a valid `RelativePath`
224237
#[derive(thiserror::Error, Debug)]
225238
pub struct StripPrefixError<'a> {
@@ -397,6 +410,17 @@ mod tests {
397410
assert!(rel_path.is_none());
398411
}
399412

413+
#[cfg(windows)]
414+
#[test]
415+
fn strip_prefix_ignores_windows_namespace_prefixes() {
416+
let abs_path = AbsolutePath::new(Path::new(r"\\?\C:\Users\foo\bar")).unwrap();
417+
let prefix = AbsolutePath::new(Path::new(r"C:\Users")).unwrap();
418+
419+
let rel_path = abs_path.strip_prefix(prefix).unwrap().unwrap();
420+
421+
assert_eq!(rel_path.as_str(), "foo/bar");
422+
}
423+
400424
#[cfg(unix)]
401425
#[test]
402426
fn strip_prefix_invalid_relative() {

crates/vite_path/src/lib.rs

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
pub mod absolute;
44
pub mod relative;
55

6-
use std::io;
6+
use std::{
7+
ffi::OsStr,
8+
io,
9+
path::{Path, StripPrefixError},
10+
};
711

812
#[cfg(feature = "absolute-redaction")]
913
pub use absolute::redaction;
@@ -31,3 +35,93 @@ pub fn current_dir() -> io::Result<AbsolutePathBuf> {
3135
// Do a runtime check just in case.
3236
Ok(AbsolutePathBuf::new(cwd).unwrap())
3337
}
38+
39+
/// Strips `base` from `path`, after normalizing Windows path namespace prefixes.
40+
///
41+
/// On Windows, the `\\?\`, `\\.\`, and `\??\` prefixes are ignored before
42+
/// matching. On other platforms this is equivalent to [`Path::strip_prefix`].
43+
///
44+
/// This is purely lexical and does not access the filesystem.
45+
///
46+
/// # Errors
47+
///
48+
/// Returns an error if `base` is not a path prefix of `path` after applying the
49+
/// platform-specific prefix normalization above.
50+
pub fn strip_path_prefix<'a>(path: &'a OsStr, base: &OsStr) -> Result<&'a Path, StripPrefixError> {
51+
let path = strip_windows_path_prefix(path);
52+
let base = strip_windows_path_prefix(base);
53+
Path::new(path).strip_prefix(base)
54+
}
55+
56+
/// Strip the `\\?\`, `\\.\`, `\??\` prefix from a Windows path, if present.
57+
/// Does nothing on non-Windows platforms.
58+
///
59+
/// `\\?\` and `\\.\` are used to enable long paths and access to device paths.
60+
/// `\??\` is used in Nt* calls.
61+
/// The resulting path is not necessarily valid or points to the same location,
62+
/// but it is enough for lexical path-prefix comparisons.
63+
#[cfg_attr(
64+
not(windows),
65+
expect(
66+
clippy::missing_const_for_fn,
67+
reason = "uses non-const for loop and strip_prefix on Windows"
68+
)
69+
)]
70+
fn strip_windows_path_prefix(p: &OsStr) -> &OsStr {
71+
#[cfg(windows)]
72+
{
73+
use os_str_bytes::OsStrBytesExt as _;
74+
75+
for prefix in [r"\\?\", r"\\.\", r"\??\"] {
76+
if let Some(stripped) = p.strip_prefix(prefix) {
77+
return stripped;
78+
}
79+
}
80+
p
81+
}
82+
#[cfg(not(windows))]
83+
{
84+
p
85+
}
86+
}
87+
88+
#[cfg(test)]
89+
mod tests {
90+
use std::ffi::OsStr;
91+
92+
use super::*;
93+
94+
#[test]
95+
fn strip_path_prefix_strips_base() {
96+
let path =
97+
OsStr::new(if cfg!(windows) { r"C:\repo\pkg\file.txt" } else { "/repo/pkg/file.txt" });
98+
let base = OsStr::new(if cfg!(windows) { r"C:\repo" } else { "/repo" });
99+
100+
let stripped = strip_path_prefix(path, base).unwrap();
101+
102+
assert_eq!(
103+
stripped,
104+
Path::new(if cfg!(windows) { r"pkg\file.txt" } else { "pkg/file.txt" })
105+
);
106+
}
107+
108+
#[test]
109+
fn strip_path_prefix_reports_mismatch() {
110+
let path =
111+
OsStr::new(if cfg!(windows) { r"C:\repo\pkg\file.txt" } else { "/repo/pkg/file.txt" });
112+
let base = OsStr::new(if cfg!(windows) { r"C:\other" } else { "/other" });
113+
114+
assert!(strip_path_prefix(path, base).is_err());
115+
}
116+
117+
#[cfg(windows)]
118+
#[test]
119+
fn strip_path_prefix_ignores_windows_namespace_prefixes() {
120+
let path = OsStr::new(r"\??\C:\repo\pkg\file.txt");
121+
let base = OsStr::new(r"\\?\C:\repo");
122+
123+
let stripped = strip_path_prefix(path, base).unwrap();
124+
125+
assert_eq!(stripped, Path::new(r"pkg\file.txt"));
126+
}
127+
}

0 commit comments

Comments
 (0)