Skip to content

Commit 7b746d4

Browse files
fix: use same UNC path normalization logic with libuv (#306)
--------- Co-authored-by: LongYinan <[email protected]>
1 parent ed9208d commit 7b746d4

File tree

8 files changed

+113
-56
lines changed

8 files changed

+113
-56
lines changed

Cargo.lock

Lines changed: 0 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ keywords = ["node", "resolve", "cjs", "esm", "enhanced-resolve"]
1414
license = "MIT"
1515
readme = "README.md"
1616
repository = "https://github.com/oxc-project/oxc-resolver"
17-
rust-version = "1.70"
17+
rust-version = "1.74"
1818
include = ["/src", "/examples", "/benches"]
1919

2020
[lib]
@@ -70,7 +70,6 @@ serde_json = { version = "1", features = [
7070
"preserve_order",
7171
] } # preserve_order: package_json.exports requires order such as `["require", "import", "default"]`
7272
rustc-hash = { version = "2" }
73-
dunce = "1" # Normalize Windows paths to the most compatible format, avoiding UNC where possible
7473
once_cell = "1" # Use `std::sync::OnceLock::get_or_try_init` when it is stable.
7574
thiserror = "1"
7675
json-strip-comments = "1"

fixtures/pnpm/longfilename/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const test = 'hello world'
2+
3+
export default test
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "@oxc-resolver/test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
3+
"private": true,
4+
"version": "0.0.0",
5+
"main": "index.js",
6+
"type": "module"
7+
}

fixtures/pnpm/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "1.0.0",
44
"private": true,
55
"devDependencies": {
6+
"@oxc-resolver/test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": "file:./longfilename",
67
"axios": "1.6.2",
78
"ipaddr.js": "2.2.0",
89
"postcss": "8.4.33",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/file_system.rs

Lines changed: 83 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ use cfg_if::cfg_if;
77
#[cfg(feature = "yarn_pnp")]
88
use pnp::fs::{LruZipCache, VPath, VPathInfo, ZipCache};
99

10+
#[cfg(windows)]
11+
const UNC_PATH_PREFIX: &[u8] = b"\\\\?\\UNC\\";
12+
#[cfg(windows)]
13+
const LONG_PATH_PREFIX: &[u8] = b"\\\\?\\";
14+
1015
/// File System abstraction used for `ResolverGeneric`
1116
pub trait FileSystem: Send + Sync {
1217
/// See [std::fs::read_to_string]
@@ -164,55 +169,12 @@ impl FileSystem for FileSystemOs {
164169
cfg_if! {
165170
if #[cfg(feature = "yarn_pnp")] {
166171
match VPath::from(path)? {
167-
VPath::Zip(info) => {
168-
dunce::canonicalize(info.physical_base_path().join(info.zip_path))
169-
}
170-
VPath::Virtual(info) => dunce::canonicalize(info.physical_base_path()),
171-
VPath::Native(path) => dunce::canonicalize(path),
172+
VPath::Zip(info) => fast_canonicalize(info.physical_base_path().join(info.zip_path)),
173+
VPath::Virtual(info) => fast_canonicalize(info.physical_base_path()),
174+
VPath::Native(path) => fast_canonicalize(path),
172175
}
173-
} else if #[cfg(windows)] {
174-
dunce::canonicalize(path)
175176
} else {
176-
use std::path::Component;
177-
let mut path_buf = path.to_path_buf();
178-
loop {
179-
let link = fs::read_link(&path_buf)?;
180-
path_buf.pop();
181-
if fs::symlink_metadata(&path_buf)?.is_symlink()
182-
{
183-
path_buf = self.canonicalize(path_buf.as_path())?;
184-
}
185-
for component in link.components() {
186-
match component {
187-
Component::ParentDir => {
188-
path_buf.pop();
189-
}
190-
Component::Normal(seg) => {
191-
#[cfg(target_family = "wasm")]
192-
{
193-
// Need to trim the extra \0 introduces by https://github.com/nodejs/uvwasi/issues/262
194-
path_buf.push(seg.to_string_lossy().trim_end_matches('\0'));
195-
}
196-
#[cfg(not(target_family = "wasm"))]
197-
{
198-
path_buf.push(seg);
199-
}
200-
}
201-
Component::RootDir => {
202-
path_buf = PathBuf::from("/");
203-
}
204-
Component::CurDir | Component::Prefix(_) => {}
205-
}
206-
if fs::symlink_metadata(&path_buf)?.is_symlink()
207-
{
208-
path_buf = self.canonicalize(path_buf.as_path())?;
209-
}
210-
}
211-
if !fs::symlink_metadata(&path_buf)?.is_symlink() {
212-
break;
213-
}
214-
}
215-
Ok(path_buf)
177+
fast_canonicalize(path)
216178
}
217179
}
218180
}
@@ -227,3 +189,77 @@ fn metadata() {
227189
);
228190
let _ = meta;
229191
}
192+
193+
#[inline]
194+
fn fast_canonicalize<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
195+
#[cfg(windows)]
196+
{
197+
// fs::canonicalize was faster on Windows (https://github.com/oxc-project/oxc-resolver/pull/306)
198+
Ok(node_compatible_raw_canonicalize(fs::canonicalize(path)?))
199+
}
200+
#[cfg(not(windows))]
201+
{
202+
fast_canonicalize_non_windows(path.as_ref().to_path_buf())
203+
}
204+
}
205+
206+
#[inline]
207+
#[cfg(not(windows))]
208+
// This is A faster fs::canonicalize implementation by reducing the number of syscalls
209+
fn fast_canonicalize_non_windows(path: PathBuf) -> io::Result<PathBuf> {
210+
use std::path::Component;
211+
let mut path_buf = path;
212+
213+
loop {
214+
let link = fs::read_link(&path_buf)?;
215+
path_buf.pop();
216+
if fs::symlink_metadata(&path_buf)?.is_symlink() {
217+
path_buf = fast_canonicalize(path_buf)?;
218+
}
219+
for component in link.components() {
220+
match component {
221+
Component::ParentDir => {
222+
path_buf.pop();
223+
}
224+
Component::Normal(seg) => {
225+
#[cfg(target_family = "wasm")]
226+
{
227+
// Need to trim the extra \0 introduces by https://github.com/nodejs/uvwasi/issues/262
228+
path_buf.push(seg.to_string_lossy().trim_end_matches('\0'));
229+
}
230+
#[cfg(not(target_family = "wasm"))]
231+
{
232+
path_buf.push(seg);
233+
}
234+
}
235+
Component::RootDir => {
236+
path_buf = PathBuf::from("/");
237+
}
238+
Component::CurDir | Component::Prefix(_) => {}
239+
}
240+
241+
if fs::symlink_metadata(&path_buf)?.is_symlink() {
242+
path_buf = fast_canonicalize(path_buf)?;
243+
}
244+
}
245+
if !fs::symlink_metadata(&path_buf)?.is_symlink() {
246+
break;
247+
}
248+
}
249+
Ok(path_buf)
250+
}
251+
252+
#[cfg(windows)]
253+
fn node_compatible_raw_canonicalize<P: AsRef<Path>>(path: P) -> PathBuf {
254+
let path_bytes = path.as_ref().as_os_str().as_encoded_bytes();
255+
path_bytes
256+
.strip_prefix(UNC_PATH_PREFIX)
257+
.or_else(|| path_bytes.strip_prefix(LONG_PATH_PREFIX))
258+
.map_or_else(
259+
|| path.as_ref().to_path_buf(),
260+
|p| {
261+
// SAFETY: `as_encoded_bytes` ensures `p` is valid path bytes
262+
unsafe { PathBuf::from(std::ffi::OsStr::from_encoded_bytes_unchecked(p)) }
263+
},
264+
)
265+
}

tests/resolve_test.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,13 @@ fn nested_symlinks() {
205205
Ok(dir.join("nm/index.js"))
206206
);
207207
}
208+
209+
#[test]
210+
fn windows_symlinked_longfilename() {
211+
let dir = dir();
212+
let path = dir.join("fixtures/pnpm");
213+
let module_path = dir.join("node_modules/.pnpm/@oxc-resolver+test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_m464apeldykmdsyzlfhtrggk24/node_modules/@oxc-resolver/test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/index.js");
214+
215+
let resolution = Resolver::new(ResolveOptions::default()).resolve(&path, "@oxc-resolver/test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").map(|r| r.full_path());
216+
assert_eq!(resolution, Ok(module_path));
217+
}

0 commit comments

Comments
 (0)