Skip to content

Commit 0a4c11e

Browse files
wan9chicodex
andcommitted
refactor(path): add shared strip_path_prefix helper
Co-authored-by: GPT-5 Codex <codex@openai.com>
1 parent 52b9514 commit 0a4c11e

6 files changed

Lines changed: 192 additions & 47 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: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,32 @@ impl AbsolutePath {
163163
let Ok(stripped_path) = self.0.strip_prefix(&base.0) else {
164164
return Ok(None);
165165
};
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-
}
166+
stripped_path_to_relative(stripped_path).map(Some)
167+
}
168+
169+
/// Returns a path that, when joined onto base, yields self.
170+
///
171+
/// Unlike [`AbsolutePath::strip_prefix`], this normalizes Windows path
172+
/// namespace prefixes (`\\?\`, `\\.\`, and `\??\`) before matching.
173+
///
174+
/// If `base` is not a prefix of `self`, returns [`None`].
175+
///
176+
/// If the stripped path is not a valid `RelativePath`. Returns an error with the reason and the stripped path.
177+
///
178+
/// # Errors
179+
///
180+
/// Returns an error if the stripped path contains invalid UTF-8 or other path data issues.
181+
pub fn strip_path_prefix<P: AsRef<Self>>(
182+
&self,
183+
base: P,
184+
) -> Result<Option<RelativePathBuf>, StripPrefixError<'_>> {
185+
let base = base.as_ref();
186+
let Ok(stripped_path) =
187+
crate::strip_path_prefix(self.as_path().as_os_str(), base.as_path().as_os_str())
188+
else {
189+
return Ok(None);
190+
};
191+
stripped_path_to_relative(stripped_path).map(Some)
175192
}
176193

177194
/// Creates an owned [`AbsolutePathBuf`] with `path` adjoined to `self`.
@@ -220,6 +237,20 @@ impl AbsolutePath {
220237
}
221238
}
222239

240+
fn stripped_path_to_relative(
241+
stripped_path: &Path,
242+
) -> Result<RelativePathBuf, StripPrefixError<'_>> {
243+
match RelativePathBuf::new(stripped_path) {
244+
Ok(relative_path) => Ok(relative_path),
245+
Err(FromPathError::NonRelative) => {
246+
unreachable!("stripped path should always be relative")
247+
}
248+
Err(FromPathError::InvalidPathData(invalid_path_data_error)) => {
249+
Err(StripPrefixError { stripped_path, invalid_path_data_error })
250+
}
251+
}
252+
}
253+
223254
/// An Error returned from [`AbsolutePath::strip_prefix`] if the stripped path is not a valid `RelativePath`
224255
#[derive(thiserror::Error, Debug)]
225256
pub struct StripPrefixError<'a> {
@@ -397,6 +428,54 @@ mod tests {
397428
assert!(rel_path.is_none());
398429
}
399430

431+
#[test]
432+
fn strip_path_prefix() {
433+
let abs_path = AbsolutePath::new(Path::new(if cfg!(windows) {
434+
"C:\\Users\\foo\\bar"
435+
} else {
436+
"/home/foo/bar"
437+
}))
438+
.unwrap();
439+
440+
let prefix =
441+
AbsolutePath::new(Path::new(if cfg!(windows) { "C:\\Users" } else { "/home" }))
442+
.unwrap();
443+
444+
let rel_path = abs_path.strip_path_prefix(prefix).unwrap().unwrap();
445+
assert_eq!(rel_path.as_str(), "foo/bar");
446+
}
447+
448+
#[test]
449+
fn strip_path_prefix_not_found() {
450+
let abs_path = AbsolutePath::new(Path::new(if cfg!(windows) {
451+
"C:\\Users\\foo\\bar"
452+
} else {
453+
"/home/foo/bar"
454+
}))
455+
.unwrap();
456+
457+
let prefix = AbsolutePath::new(Path::new(if cfg!(windows) {
458+
"C:\\Users\\barz"
459+
} else {
460+
"/home/baz"
461+
}))
462+
.unwrap();
463+
464+
let rel_path = abs_path.strip_path_prefix(prefix).unwrap();
465+
assert!(rel_path.is_none());
466+
}
467+
468+
#[cfg(windows)]
469+
#[test]
470+
fn strip_path_prefix_ignores_windows_namespace_prefixes() {
471+
let abs_path = AbsolutePath::new(Path::new(r"\\?\C:\Users\foo\bar")).unwrap();
472+
let prefix = AbsolutePath::new(Path::new(r"C:\Users")).unwrap();
473+
474+
let rel_path = abs_path.strip_path_prefix(prefix).unwrap().unwrap();
475+
476+
assert_eq!(rel_path.as_str(), "foo/bar");
477+
}
478+
400479
#[cfg(unix)]
401480
#[test]
402481
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)