Skip to content

Commit ff862e2

Browse files
authored
perf: reuse owned parent realpath buffer in realpath_uncached (#278)
1 parent 3fbe03a commit ff862e2

2 files changed

Lines changed: 37 additions & 6 deletions

File tree

src/cache.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::{
99
sync::Arc,
1010
};
1111

12-
use camino::{Utf8Path, Utf8PathBuf};
12+
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
1313
use dashmap::{DashMap, DashSet};
1414
use futures::future::BoxFuture;
1515
use rustc_hash::FxHasher;
@@ -18,7 +18,6 @@ use tokio::sync::OnceCell as OnceLock;
1818
use crate::{
1919
context::ResolveContext as Ctx,
2020
package_json::{off_to_location, PackageJson},
21-
path::PathUtil,
2221
resolver_path::{hash_path, ResolverPath},
2322
FileMetadata, FileSystem, JSONError, ResolveError, ResolveOptions, TsConfig,
2423
};
@@ -272,10 +271,18 @@ impl CachedPathImpl {
272271
.map(|path| Some(Utf8PathBuf::from_path_buf(path).expect("path should be UTF-8")));
273272
}
274273
if let Some(parent) = self.parent() {
275-
let parent_path = parent.realpath(fs).await?;
276-
return Ok(Some(
277-
parent_path.normalize_with(self.path.strip_prefix(parent.path()).unwrap()),
278-
));
274+
let mut real_path = parent.realpath(fs).await?;
275+
// Unnormalized paths (e.g. from alias values or absolute specifiers) can
276+
// end in `..`, where `file_name()` returns `None` and would silently drop
277+
// the component; POSIX semantics pop it after the parent is resolved.
278+
match self.path.components().next_back() {
279+
Some(Utf8Component::Normal(segment)) => real_path.push(segment),
280+
Some(Utf8Component::ParentDir) => {
281+
real_path.pop();
282+
}
283+
_ => {}
284+
}
285+
return Ok(Some(real_path));
279286
}
280287
Ok(None)
281288
})

src/tests/symlink.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,27 @@ async fn test() -> io::Result<()> {
157157

158158
Ok(())
159159
}
160+
161+
// With `symlinks` enabled, realpath must apply `..` components after resolving
162+
// the parent, not drop them. Unnormalized paths reach the resolver from user
163+
// input: absolute specifiers (kept verbatim on non-Windows), exact-match alias
164+
// values, and the `directory` argument of `resolve`.
165+
#[tokio::test]
166+
async fn dotdot_in_unnormalized_input() {
167+
let root = super::fixture_root().join("enhanced_resolve");
168+
let expected = Ok(root.join("lib/index.js"));
169+
let resolver = Resolver::default();
170+
171+
let specifier = root.join("test/../lib/index.js");
172+
let resolution = resolver
173+
.resolve(root.join("test"), specifier.to_str().unwrap())
174+
.await
175+
.map(|r| r.full_path());
176+
assert_eq!(resolution, expected, "absolute specifier containing `..`");
177+
178+
let resolution = resolver
179+
.resolve(root.join("test/.."), "./lib/index.js")
180+
.await
181+
.map(|r| r.full_path());
182+
assert_eq!(resolution, expected, "directory ending in `..`");
183+
}

0 commit comments

Comments
 (0)