Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::{
sync::Arc,
};

use camino::{Utf8Path, Utf8PathBuf};
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
use dashmap::{DashMap, DashSet};
use futures::future::BoxFuture;
use rustc_hash::FxHasher;
Expand All @@ -18,7 +18,6 @@ use tokio::sync::OnceCell as OnceLock;
use crate::{
context::ResolveContext as Ctx,
package_json::{off_to_location, PackageJson},
path::PathUtil,
resolver_path::{hash_path, ResolverPath},
FileMetadata, FileSystem, JSONError, ResolveError, ResolveOptions, TsConfig,
};
Expand Down Expand Up @@ -272,10 +271,18 @@ impl CachedPathImpl {
.map(|path| Some(Utf8PathBuf::from_path_buf(path).expect("path should be UTF-8")));
}
if let Some(parent) = self.parent() {
let parent_path = parent.realpath(fs).await?;
return Ok(Some(
parent_path.normalize_with(self.path.strip_prefix(parent.path()).unwrap()),
));
let mut real_path = parent.realpath(fs).await?;
// Unnormalized paths (e.g. from alias values or absolute specifiers) can
// end in `..`, where `file_name()` returns `None` and would silently drop
// the component; POSIX semantics pop it after the parent is resolved.
match self.path.components().next_back() {
Some(Utf8Component::Normal(segment)) => real_path.push(segment),
Some(Utf8Component::ParentDir) => {
real_path.pop();
}
_ => {}
}
return Ok(Some(real_path));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Ok(None)
})
Expand Down
24 changes: 24 additions & 0 deletions src/tests/symlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,27 @@ async fn test() -> io::Result<()> {

Ok(())
}

// With `symlinks` enabled, realpath must apply `..` components after resolving
// the parent, not drop them. Unnormalized paths reach the resolver from user
// input: absolute specifiers (kept verbatim on non-Windows), exact-match alias
// values, and the `directory` argument of `resolve`.
#[tokio::test]
async fn dotdot_in_unnormalized_input() {
let root = super::fixture_root().join("enhanced_resolve");
let expected = Ok(root.join("lib/index.js"));
let resolver = Resolver::default();

let specifier = root.join("test/../lib/index.js");
let resolution = resolver
.resolve(root.join("test"), specifier.to_str().unwrap())
.await
.map(|r| r.full_path());
assert_eq!(resolution, expected, "absolute specifier containing `..`");

let resolution = resolver
.resolve(root.join("test/.."), "./lib/index.js")
.await
.map(|r| r.full_path());
assert_eq!(resolution, expected, "directory ending in `..`");
}
Loading