Skip to content

Commit e7cfc08

Browse files
chenxinyancBoshen
authored andcommitted
strip_windows_prefix: Handle DOS device paths properly.
1 parent b9f1c67 commit e7cfc08

File tree

3 files changed

+82
-18
lines changed

3 files changed

+82
-18
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: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -155,30 +155,52 @@ impl FileSystemOs {
155155
/// See [std::fs::read_link]
156156
#[inline]
157157
pub fn read_link(path: &Path) -> io::Result<PathBuf> {
158-
let path = fs::read_link(path)?;
158+
let target = fs::read_link(path)?;
159159
cfg_if! {
160160
if #[cfg(windows)] {
161-
Ok(Self::strip_windows_prefix(path))
161+
Ok(match Self::try_strip_windows_prefix(&target) {
162+
Some(path) => path,
163+
// We won't follow the link if we cannot represent its target properly.
164+
None => target,
165+
})
162166
} else {
163-
Ok(path)
167+
Ok(target.to_path_buf())
164168
}
165169
}
166170
}
167171

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"\\\\?\\";
172+
/// When applicable, converts a [DOS device path](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths)
173+
/// to a normal path (usually, "Traditional DOS paths" or "UNC path") that can be consumed by the `import`/`require` syntax of Node.js.
174+
/// Returns `None` if the path cannot be represented as a normal path.
175+
pub fn try_strip_windows_prefix<P: AsRef<Path>>(path: P) -> Option<PathBuf> {
176+
// See https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
171177
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-
)
178+
179+
let path = if let Some(p) =
180+
path_bytes.strip_prefix(br"\\?\UNC\").or(path_bytes.strip_prefix(br"\\.\UNC\"))
181+
{
182+
// UNC paths
183+
unsafe {
184+
PathBuf::from(std::ffi::OsStr::from_encoded_bytes_unchecked(&[br"\\", p].concat()))
185+
}
186+
} else if let Some(p) =
187+
path_bytes.strip_prefix(br"\\?\").or(path_bytes.strip_prefix(br"\\.\"))
188+
{
189+
// Assuming traditional DOS path "\\?\C:\"
190+
if p[1] != b':' {
191+
// E.g.,
192+
// \\?\Volume{b75e2c83-0000-0000-0000-602f00000000}
193+
// \\?\BootPartition\
194+
// It seems nodejs does not support DOS device paths with Volume GUIDs.
195+
// This can happen if the path points to a Mounted Volume without a drive letter.
196+
return None;
197+
}
198+
unsafe { PathBuf::from(std::ffi::OsStr::from_encoded_bytes_unchecked(p)) }
199+
} else {
200+
path.as_ref().to_path_buf()
201+
};
202+
203+
Some(path)
182204
}
183205
}
184206

@@ -245,3 +267,37 @@ fn metadata() {
245267
);
246268
let _ = meta;
247269
}
270+
271+
#[test]
272+
fn test_strip_windows_prefix() {
273+
assert_eq!(
274+
FileSystemOs::try_strip_windows_prefix(PathBuf::from(
275+
r"\\?\C:\Users\user\Documents\file.txt"
276+
)),
277+
Some(PathBuf::from(r"C:\Users\user\Documents\file.txt"))
278+
);
279+
280+
assert_eq!(
281+
FileSystemOs::try_strip_windows_prefix(PathBuf::from(
282+
r"\\.\C:\Users\user\Documents\file.txt"
283+
)),
284+
Some(PathBuf::from(r"C:\Users\user\Documents\file.txt"))
285+
);
286+
287+
assert_eq!(
288+
FileSystemOs::try_strip_windows_prefix(PathBuf::from(r"\\?\UNC\server\share\file.txt")),
289+
Some(PathBuf::from(r"\\server\share\file.txt"))
290+
);
291+
292+
assert_eq!(
293+
FileSystemOs::try_strip_windows_prefix(PathBuf::from(
294+
r"\\?\Volume{c8ec34d8-3ba6-45c3-9b9d-3e4148e12d00}\file.txt"
295+
)),
296+
None
297+
);
298+
299+
assert_eq!(
300+
FileSystemOs::try_strip_windows_prefix(PathBuf::from(r"\\?\BootPartition\file.txt")),
301+
None
302+
);
303+
}

src/fs_cache.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,14 @@ impl<Fs: FileSystem> Cache for FsCache<Fs> {
8080
let path = cached_path.to_path_buf();
8181
cfg_if! {
8282
if #[cfg(windows)] {
83-
let path = crate::FileSystemOs::strip_windows_prefix(path);
83+
match crate::FileSystemOs::try_strip_windows_prefix(&path) {
84+
Some(path) => Ok(path),
85+
None => Err(ResolveError::PathNotSupported(path)),
86+
}
87+
} else {
88+
Ok(path)
8489
}
8590
}
86-
Ok(path)
8791
}
8892

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

0 commit comments

Comments
 (0)