Skip to content

Commit 850cbf9

Browse files
chenxinyancBoshen
andauthored
fix: properly handle DOS device paths in strip_windows_prefix (#455)
Fixes #454 This PR changed the name and return type of `strip_windows_prefix` to `try_strip_windows_prefix`, allowing it to return `None` to indicate unsupported [DOS device paths](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths). Most significantly, symlinks referring to a drive without drive letter, usually accessed via a mount point (Mounted Volume), should not be resolved at all, as nodejs `import`/`require` does not support such properly, as of Node 22. This PR also rectified the [UNC path](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#unc-paths) resolution to prepend the `"\\"` portion to the path. --------- Co-authored-by: Boshen <[email protected]>
1 parent b9f1c67 commit 850cbf9

File tree

5 files changed

+80
-21
lines changed

5 files changed

+80
-21
lines changed

src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ pub enum ResolveError {
4141
#[error("{0}")]
4242
IOError(IOError),
4343

44+
/// For example, Windows UNC path with Volume GUID is not supported.
45+
#[error("Path {0:?} contains unsupported construct.")]
46+
PathNotSupported(PathBuf),
47+
4448
/// Node.js builtin module when `Options::builtin_modules` is enabled.
4549
///
4650
/// `is_runtime_module` can be used to determine whether the request

src/file_system.rs

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -157,29 +157,17 @@ impl FileSystemOs {
157157
pub fn read_link(path: &Path) -> io::Result<PathBuf> {
158158
let path = fs::read_link(path)?;
159159
cfg_if! {
160-
if #[cfg(windows)] {
161-
Ok(Self::strip_windows_prefix(path))
160+
if #[cfg(target_os = "windows")] {
161+
match crate::windows::try_strip_windows_prefix(path) {
162+
// We won't follow the link if we cannot represent its target properly.
163+
Ok(p) | Err(crate::ResolveError::PathNotSupported(p)) => Ok(p),
164+
_ => unreachable!(),
165+
}
162166
} else {
163167
Ok(path)
164168
}
165169
}
166170
}
167-
168-
pub fn strip_windows_prefix<P: AsRef<Path>>(path: P) -> PathBuf {
169-
const UNC_PATH_PREFIX: &[u8] = b"\\\\?\\UNC\\";
170-
const LONG_PATH_PREFIX: &[u8] = b"\\\\?\\";
171-
let path_bytes = path.as_ref().as_os_str().as_encoded_bytes();
172-
path_bytes
173-
.strip_prefix(UNC_PATH_PREFIX)
174-
.or_else(|| path_bytes.strip_prefix(LONG_PATH_PREFIX))
175-
.map_or_else(
176-
|| path.as_ref().to_path_buf(),
177-
|p| {
178-
// SAFETY: `as_encoded_bytes` ensures `p` is valid path bytes
179-
unsafe { PathBuf::from(std::ffi::OsStr::from_encoded_bytes_unchecked(p)) }
180-
},
181-
)
182-
}
183171
}
184172

185173
impl FileSystem for FileSystemOs {

src/fs_cache.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,12 @@ impl<Fs: FileSystem> Cache for FsCache<Fs> {
7979
let cached_path = self.canonicalize_impl(path)?;
8080
let path = cached_path.to_path_buf();
8181
cfg_if! {
82-
if #[cfg(windows)] {
83-
let path = crate::FileSystemOs::strip_windows_prefix(path);
82+
if #[cfg(target_os = "windows")] {
83+
crate::windows::try_strip_windows_prefix(path)
84+
} else {
85+
Ok(path)
8486
}
8587
}
86-
Ok(path)
8788
}
8889

8990
fn is_file(&self, path: &Self::Cp, ctx: &mut Ctx) -> bool {

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ mod specifier;
6565
mod tsconfig;
6666
#[cfg(feature = "fs_cache")]
6767
mod tsconfig_serde;
68+
#[cfg(target_os = "windows")]
69+
mod windows;
6870

6971
#[cfg(test)]
7072
mod tests;

src/windows.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::path::PathBuf;
2+
3+
use crate::ResolveError;
4+
5+
/// When applicable, converts a [DOS device path](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths)
6+
/// to a normal path (usually, "Traditional DOS paths" or "UNC path") that can be consumed by the `import`/`require` syntax of Node.js.
7+
/// Returns `None` if the path cannot be represented as a normal path.
8+
pub fn try_strip_windows_prefix(path: PathBuf) -> Result<PathBuf, ResolveError> {
9+
// See https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
10+
let path_bytes = path.as_os_str().as_encoded_bytes();
11+
12+
let path = if let Some(p) =
13+
path_bytes.strip_prefix(br"\\?\UNC\").or_else(|| path_bytes.strip_prefix(br"\\.\UNC\"))
14+
{
15+
// UNC paths
16+
// SAFETY:
17+
unsafe {
18+
PathBuf::from(std::ffi::OsStr::from_encoded_bytes_unchecked(&[br"\\", p].concat()))
19+
}
20+
} else if let Some(p) =
21+
path_bytes.strip_prefix(br"\\?\").or_else(|| path_bytes.strip_prefix(br"\\.\"))
22+
{
23+
// Assuming traditional DOS path "\\?\C:\"
24+
if p[1] != b':' {
25+
// E.g.,
26+
// \\?\Volume{b75e2c83-0000-0000-0000-602f00000000}
27+
// \\?\BootPartition\
28+
// It seems nodejs does not support DOS device paths with Volume GUIDs.
29+
// This can happen if the path points to a Mounted Volume without a drive letter.
30+
return Err(ResolveError::PathNotSupported(path));
31+
}
32+
// SAFETY:
33+
unsafe { PathBuf::from(std::ffi::OsStr::from_encoded_bytes_unchecked(p)) }
34+
} else {
35+
path
36+
};
37+
38+
Ok(path)
39+
}
40+
41+
#[test]
42+
fn test_try_strip_windows_prefix() {
43+
let pass = [
44+
(r"\\?\C:\Users\user\Documents\file1.txt", r"C:\Users\user\Documents\file1.txt"),
45+
(r"\\.\C:\Users\user\Documents\file2.txt", r"C:\Users\user\Documents\file2.txt"),
46+
(r"\\?\UNC\server\share\file3.txt", r"\\server\share\file3.txt"),
47+
];
48+
49+
for (path, expected) in pass {
50+
assert_eq!(try_strip_windows_prefix(PathBuf::from(path)), Ok(PathBuf::from(expected)));
51+
}
52+
53+
let fail = [
54+
r"\\?\Volume{c8ec34d8-3ba6-45c3-9b9d-3e4148e12d00}\file4.txt",
55+
r"\\?\BootPartition\file4.txt",
56+
];
57+
58+
for path in fail {
59+
assert_eq!(
60+
try_strip_windows_prefix(PathBuf::from(path)),
61+
Err(crate::ResolveError::PathNotSupported(PathBuf::from(path)))
62+
);
63+
}
64+
}

0 commit comments

Comments
 (0)