use tracing::{debug, trace, warn};
use aube_lockfile::dep_path_filename::{
DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH, dep_path_to_filename,
};
use aube_lockfile::graph_hash::GraphHashes;
use aube_lockfile::{LocalSource, LockedPackage, LockfileGraph};
use aube_store::{PackageIndex, Store, StoredFile};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
mod hoisted;
pub mod sys;
pub use hoisted::HoistedPlacements;
pub fn sweep_stale_tmp_dirs(virtual_store: &Path) {
let Ok(entries) = std::fs::read_dir(virtual_store) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.starts_with(".tmp-") {
continue;
}
let rest = &name[".tmp-".len()..];
let Some((pid_str, _rest)) = rest.split_once('-') else {
continue;
};
if pid_str.chars().any(|c| !c.is_ascii_digit()) {
continue;
}
if pid_str == std::process::id().to_string() {
continue;
}
let _ = remove_dir_all_with_retry(&entry.path());
}
}
pub fn remove_dir_all_with_retry(path: &Path) -> std::io::Result<()> {
#[cfg(not(windows))]
{
std::fs::remove_dir_all(path)
}
#[cfg(windows)]
{
use std::io::ErrorKind;
let mut delay_ms = 50u64;
for attempt in 0..10 {
match std::fs::remove_dir_all(path) {
Ok(()) => return Ok(()),
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()),
Err(e) => {
let retriable =
matches!(e.kind(), ErrorKind::PermissionDenied | ErrorKind::Other)
|| e.raw_os_error() == Some(32);
if !retriable || attempt == 9 {
return Err(e);
}
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
delay_ms = (delay_ms * 2).min(2000);
}
}
}
Ok(())
}
}
pub fn is_physical_importer(importer_path: &str) -> bool {
importer_path == "." || !importer_path.contains("/node_modules/")
}
fn remove_hidden_hoist_tree(path: &Path) {
match std::fs::symlink_metadata(path) {
Ok(md) if md.file_type().is_symlink() => {
let _ = std::fs::remove_file(path);
}
Ok(_) => {
let _ = std::fs::remove_dir_all(path);
}
Err(_) => {}
}
}
pub(crate) fn try_remove_entry(path: &Path) {
let _ = std::fs::remove_dir_all(path);
let _ = std::fs::remove_file(path);
}
pub fn mkdirp(dir: &Path) -> Result<(), Error> {
xx::file::mkdirp(dir).map_err(|e| Error::Xx(e.to_string()))
}
#[derive(Copy, Clone)]
pub(crate) enum EntryState {
Fresh,
Missing,
Stale,
}
pub(crate) fn sweep_stale_top_level_entries(
nm: &Path,
preserve: &std::collections::HashSet<&str>,
aube_dir_leaf: Option<&std::ffi::OsStr>,
) {
let scope_prefixes: std::collections::HashSet<&str> = preserve
.iter()
.filter_map(|n| n.split_once('/').map(|(scope, _)| scope))
.collect();
let Ok(entries) = std::fs::read_dir(nm) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with('.') {
continue;
}
if aube_dir_leaf == Some(name.as_os_str()) {
continue;
}
if preserve.contains(name_str.as_ref()) {
continue;
}
if scope_prefixes.contains(name_str.as_ref()) {
let scope_dir = entry.path();
if let Ok(inner) = std::fs::read_dir(&scope_dir) {
for inner_entry in inner.flatten() {
let inner_name = inner_entry.file_name();
let full = format!("{}/{}", name_str, inner_name.to_string_lossy());
if !preserve.contains(full.as_str()) {
try_remove_entry(&inner_entry.path());
}
}
}
if std::fs::read_dir(&scope_dir)
.map(|mut d| d.next().is_none())
.unwrap_or(false)
{
let _ = std::fs::remove_dir(&scope_dir);
}
continue;
}
try_remove_entry(&entry.path());
}
}
pub(crate) fn sweep_dead_hidden_hoist_entries(hidden: &Path) {
let Ok(entries) = std::fs::read_dir(hidden) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with('.') {
continue;
}
let path = entry.path();
if name_str.starts_with('@') {
match std::fs::symlink_metadata(&path) {
Ok(md) if md.is_dir() && !md.file_type().is_symlink() => {
sweep_dead_hidden_hoist_scope(&path);
if std::fs::read_dir(&path)
.map(|mut d| d.next().is_none())
.unwrap_or(false)
{
let _ = std::fs::remove_dir(&path);
}
}
Ok(_) => sweep_dead_hidden_hoist_entry(&path),
Err(_) => {}
}
continue;
}
sweep_dead_hidden_hoist_entry(&path);
}
}
fn sweep_dead_hidden_hoist_scope(scope_dir: &Path) {
let Ok(entries) = std::fs::read_dir(scope_dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
if name.to_string_lossy().starts_with('.') {
continue;
}
sweep_dead_hidden_hoist_entry(&entry.path());
}
}
fn sweep_dead_hidden_hoist_entry(path: &Path) {
match std::fs::symlink_metadata(path) {
Ok(md) if md.file_type().is_symlink() && path.exists() => {}
Ok(md) if md.file_type().is_symlink() => {
try_remove_entry(path);
}
Ok(md) if md.is_dir() => {
try_remove_entry(path);
}
Ok(_) => {
try_remove_entry(path);
}
Err(_) => {}
}
}
#[inline]
pub(crate) fn classify_entry_state(link_path: &Path, expected: &Path) -> EntryState {
match std::fs::read_link(link_path) {
Ok(existing) if existing == expected => {
if link_path.exists() {
EntryState::Fresh
} else {
EntryState::Stale
}
}
Ok(_) => EntryState::Stale,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => EntryState::Missing,
Err(_) => EntryState::Stale,
}
}
pub use sys::{
BinShimOptions, create_bin_shim, create_dir_link, normalize_path, parse_posix_shim_target,
remove_bin_shim, validate_bin_name, validate_bin_target,
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, strum::EnumString)]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
pub enum NodeLinker {
#[default]
Isolated,
Hoisted,
}
pub struct Linker {
virtual_store: PathBuf,
pub(crate) store: Store,
use_global_virtual_store: bool,
strategy: LinkStrategy,
pub(crate) patches: Patches,
hashes: Option<GraphHashes>,
virtual_store_dir_max_length: usize,
shamefully_hoist: bool,
public_hoist_patterns: Vec<glob::Pattern>,
public_hoist_negations: Vec<glob::Pattern>,
hoist: bool,
hoist_patterns: Vec<glob::Pattern>,
hoist_negations: Vec<glob::Pattern>,
hoist_workspace_packages: bool,
dedupe_direct_deps: bool,
pub(crate) node_linker: NodeLinker,
pub(crate) modules_dir_name: String,
pub(crate) aube_dir_override: Option<std::path::PathBuf>,
link_concurrency: Option<usize>,
virtual_store_only: bool,
}
pub type Patches = std::collections::BTreeMap<String, String>;
pub fn default_linker_parallelism() -> usize {
let default_limit = if cfg!(target_os = "macos") { 4 } else { 16 };
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
.min(default_limit)
}
type LinkPoolCache = std::sync::Mutex<Vec<(usize, std::sync::Arc<rayon::ThreadPool>)>>;
static LINK_POOL_CACHE: std::sync::OnceLock<LinkPoolCache> = std::sync::OnceLock::new();
fn link_pool(threads: usize) -> Option<std::sync::Arc<rayon::ThreadPool>> {
let cache = LINK_POOL_CACHE.get_or_init(|| std::sync::Mutex::new(Vec::new()));
let mut guard = cache.lock().ok()?;
if let Some((_, pool)) = guard.iter().find(|(t, _)| *t == threads) {
return Some(pool.clone());
}
match rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.thread_name(|i| format!("aube-linker-{i}"))
.build()
{
Ok(pool) => {
let pool = std::sync::Arc::new(pool);
guard.push((threads, pool.clone()));
Some(pool)
}
Err(err) => {
warn!("failed to build aube linker thread pool: {err}; falling back to caller thread");
None
}
}
}
fn with_link_pool<R: Send>(threads: usize, f: impl FnOnce() -> R + Send) -> R {
match link_pool(threads) {
Some(pool) => pool.install(f),
None => f(),
}
}
#[derive(Debug, Clone, Copy)]
pub enum LinkStrategy {
Reflink,
Hardlink,
Copy,
}
impl Linker {
pub fn new(store: &Store, strategy: LinkStrategy) -> Self {
let use_global_virtual_store = !aube_util::env::is_ci();
Self {
virtual_store: store.virtual_store_dir(),
store: store.clone(),
use_global_virtual_store,
strategy,
patches: Patches::new(),
hashes: None,
virtual_store_dir_max_length: DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH,
shamefully_hoist: false,
public_hoist_patterns: Vec::new(),
public_hoist_negations: Vec::new(),
hoist: true,
hoist_patterns: vec![glob::Pattern::new("*").expect("'*' is a valid glob pattern")],
hoist_negations: Vec::new(),
hoist_workspace_packages: true,
dedupe_direct_deps: false,
node_linker: NodeLinker::Isolated,
link_concurrency: None,
virtual_store_only: false,
modules_dir_name: "node_modules".to_string(),
aube_dir_override: None,
}
}
pub fn with_node_linker(mut self, node_linker: NodeLinker) -> Self {
self.node_linker = node_linker;
self
}
pub fn node_linker(&self) -> NodeLinker {
self.node_linker
}
pub fn with_modules_dir_name(mut self, name: impl Into<String>) -> Self {
let s = name.into();
self.modules_dir_name = if s.trim().is_empty() {
"node_modules".to_string()
} else {
s
};
self
}
pub fn modules_dir_name(&self) -> &str {
&self.modules_dir_name
}
pub fn with_aube_dir_override(mut self, path: std::path::PathBuf) -> Self {
self.aube_dir_override = Some(path);
self
}
pub fn aube_dir_for(&self, project_dir: &Path) -> std::path::PathBuf {
self.aube_dir_override
.clone()
.unwrap_or_else(|| project_dir.join(&self.modules_dir_name).join(".aube"))
}
pub fn with_link_concurrency(mut self, concurrency: Option<usize>) -> Self {
self.link_concurrency = concurrency;
self
}
pub fn with_use_global_virtual_store(mut self, enabled: bool) -> Self {
self.use_global_virtual_store = enabled;
self
}
fn link_parallelism(&self) -> usize {
self.link_concurrency
.unwrap_or_else(default_linker_parallelism)
.max(1)
}
pub fn with_shamefully_hoist(mut self, shamefully_hoist: bool) -> Self {
self.shamefully_hoist = shamefully_hoist;
self
}
pub fn with_public_hoist_pattern(mut self, patterns: &[String]) -> Self {
for raw in patterns {
let (neg, body) = match raw.strip_prefix('!') {
Some(rest) => (true, rest),
None => (false, raw.as_str()),
};
let Ok(pat) = glob::Pattern::new(body) else {
continue;
};
if neg {
self.public_hoist_negations.push(pat);
} else {
self.public_hoist_patterns.push(pat);
}
}
self
}
pub fn with_hoist(mut self, hoist: bool) -> Self {
self.hoist = hoist;
self
}
pub fn with_hoist_pattern(mut self, patterns: &[String]) -> Self {
self.hoist_patterns.clear();
self.hoist_negations.clear();
for raw in patterns {
let (neg, body) = match raw.strip_prefix('!') {
Some(rest) => (true, rest),
None => (false, raw.as_str()),
};
let Ok(pat) = glob::Pattern::new(body) else {
continue;
};
if neg {
self.hoist_negations.push(pat);
} else {
self.hoist_patterns.push(pat);
}
}
self
}
pub fn with_hoist_workspace_packages(mut self, on: bool) -> Self {
self.hoist_workspace_packages = on;
self
}
pub fn with_dedupe_direct_deps(mut self, on: bool) -> Self {
self.dedupe_direct_deps = on;
self
}
fn hoist_matches(&self, pkg_name: &str) -> bool {
if !self.hoist {
return false;
}
let opts = glob::MatchOptions {
case_sensitive: false,
require_literal_separator: false,
require_literal_leading_dot: false,
};
if !self
.hoist_patterns
.iter()
.any(|p| p.matches_with(pkg_name, opts))
{
return false;
}
!self
.hoist_negations
.iter()
.any(|p| p.matches_with(pkg_name, opts))
}
fn public_hoist_matches(&self, pkg_name: &str) -> bool {
if self.public_hoist_patterns.is_empty() {
return false;
}
let opts = glob::MatchOptions {
case_sensitive: false,
require_literal_separator: false,
require_literal_leading_dot: false,
};
if !self
.public_hoist_patterns
.iter()
.any(|p| p.matches_with(pkg_name, opts))
{
return false;
}
!self
.public_hoist_negations
.iter()
.any(|p| p.matches_with(pkg_name, opts))
}
pub fn with_virtual_store_dir_max_length(mut self, max_length: usize) -> Self {
self.virtual_store_dir_max_length = max_length;
self
}
pub fn with_virtual_store_only(mut self, only: bool) -> Self {
self.virtual_store_only = only;
self
}
pub fn virtual_store_only(&self) -> bool {
self.virtual_store_only
}
pub fn with_graph_hashes(mut self, hashes: GraphHashes) -> Self {
self.hashes = Some(hashes);
self
}
fn virtual_store_subdir(&self, dep_path: &str) -> String {
let hashed = match &self.hashes {
Some(h) => h.hashed_dep_path(dep_path),
None => dep_path.to_string(),
};
dep_path_to_filename(&hashed, self.virtual_store_dir_max_length)
}
fn aube_dir_entry_name(&self, dep_path: &str) -> String {
dep_path_to_filename(dep_path, self.virtual_store_dir_max_length)
}
pub fn uses_global_virtual_store(&self) -> bool {
self.use_global_virtual_store
}
#[cfg(test)]
fn new_with_gvs(store: &Store, strategy: LinkStrategy, use_global_virtual_store: bool) -> Self {
Self {
virtual_store: store.virtual_store_dir(),
store: store.clone(),
use_global_virtual_store,
strategy,
patches: Patches::new(),
hashes: None,
virtual_store_dir_max_length: DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH,
shamefully_hoist: false,
public_hoist_patterns: Vec::new(),
public_hoist_negations: Vec::new(),
hoist: true,
hoist_patterns: vec![glob::Pattern::new("*").expect("'*' is a valid glob pattern")],
hoist_negations: Vec::new(),
hoist_workspace_packages: true,
dedupe_direct_deps: false,
node_linker: NodeLinker::Isolated,
link_concurrency: None,
virtual_store_only: false,
modules_dir_name: "node_modules".to_string(),
aube_dir_override: None,
}
}
pub fn with_patches(mut self, patches: Patches) -> Self {
self.patches = patches;
self
}
pub fn detect_strategy(path: &Path) -> LinkStrategy {
Self::detect_strategy_cross(path, path)
}
pub fn detect_strategy_cross(src_dir: &Path, dst_dir: &Path) -> LinkStrategy {
type ProbeKey = (std::path::PathBuf, std::path::PathBuf);
static CACHE: std::sync::OnceLock<
std::sync::RwLock<std::collections::HashMap<ProbeKey, LinkStrategy>>,
> = std::sync::OnceLock::new();
let key = (src_dir.to_path_buf(), dst_dir.to_path_buf());
let cache = CACHE.get_or_init(Default::default);
if let Some(hit) = cache.read().expect("probe cache poisoned").get(&key) {
return *hit;
}
let test_src = src_dir.join(".aube-link-test-src");
let test_dst = dst_dir.join(".aube-link-test-dst");
let strategy = if std::fs::write(&test_src, b"test").is_ok() {
if reflink_copy::reflink(&test_src, &test_dst).is_ok() {
let _ = std::fs::remove_file(&test_src);
let _ = std::fs::remove_file(&test_dst);
LinkStrategy::Reflink
} else {
let _ = std::fs::remove_file(&test_dst);
let result = if std::fs::hard_link(&test_src, &test_dst).is_ok() {
LinkStrategy::Hardlink
} else {
LinkStrategy::Copy
};
let _ = std::fs::remove_file(&test_src);
let _ = std::fs::remove_file(&test_dst);
result
}
} else {
LinkStrategy::Copy
};
*cache
.write()
.expect("probe cache poisoned")
.entry(key)
.or_insert(strategy)
}
pub fn link_all(
&self,
project_dir: &Path,
graph: &LockfileGraph,
package_indices: &BTreeMap<String, PackageIndex>,
) -> Result<LinkStats, Error> {
if matches!(self.node_linker, NodeLinker::Hoisted) {
let mut stats = LinkStats::default();
let mut placements = HoistedPlacements::default();
hoisted::link_hoisted_importer(
self,
project_dir,
graph.root_deps(),
graph,
package_indices,
&mut stats,
&mut placements,
)?;
let _ = crate::remove_dir_all_with_retry(
&self.aube_dir_for(project_dir).join("node_modules"),
);
stats.hoisted_placements = Some(placements);
return Ok(stats);
}
let nm = project_dir.join(&self.modules_dir_name);
let aube_dir = self.aube_dir_for(project_dir);
mkdirp(&aube_dir)?;
sweep_stale_tmp_dirs(&aube_dir);
let mut root_dep_names: std::collections::HashSet<&str> =
graph.root_deps().iter().map(|d| d.name.as_str()).collect();
if self.shamefully_hoist {
for pkg in graph.packages.values() {
root_dep_names.insert(pkg.name.as_str());
}
} else if !self.public_hoist_patterns.is_empty() {
for pkg in graph.packages.values() {
if pkg.local_source.is_none() && self.public_hoist_matches(&pkg.name) {
root_dep_names.insert(pkg.name.as_str());
}
}
}
let aube_dir_leaf: Option<std::ffi::OsString> = if aube_dir.parent() == Some(nm.as_path()) {
aube_dir.file_name().map(|s| s.to_owned())
} else {
None
};
sweep_stale_top_level_entries(&nm, &root_dep_names, aube_dir_leaf.as_deref());
let mut stats = LinkStats::default();
let prev_applied = read_applied_patches(&nm);
let curr_applied = current_patch_hashes(&self.patches);
if !self.use_global_virtual_store {
wipe_changed_patched_entries(
&aube_dir,
graph,
&prev_applied,
&curr_applied,
self.virtual_store_dir_max_length,
);
}
let nested_link_targets = build_nested_link_targets(project_dir, graph);
for (dep_path, pkg) in &graph.packages {
let Some(ref local) = pkg.local_source else {
continue;
};
if matches!(local, LocalSource::Link(_)) {
continue;
}
let Some(index) = package_indices.get(dep_path) else {
continue;
};
let aube_entry = aube_dir.join(dep_path);
if !aube_entry.exists() {
self.materialize_into(
&aube_dir,
dep_path,
pkg,
index,
&mut stats,
false,
nested_link_targets.as_ref(),
)?;
} else {
stats.packages_cached += 1;
}
}
if self.use_global_virtual_store {
use rayon::prelude::*;
use rustc_hash::FxHashSet;
let mut step1_parents: FxHashSet<PathBuf> = FxHashSet::default();
for (dep_path, pkg) in &graph.packages {
if pkg.local_source.is_some() {
continue;
}
let entry = aube_dir.join(self.aube_dir_entry_name(dep_path));
if let Some(parent) = entry.parent() {
step1_parents.insert(parent.to_path_buf());
}
}
for parent in &step1_parents {
mkdirp(parent)?;
}
let link_parallelism = self.link_parallelism();
let step1_timer = std::time::Instant::now();
let step1_results: Vec<Result<LinkStats, Error>> =
with_link_pool(link_parallelism, || {
graph
.packages
.par_iter()
.filter_map(|(dep_path, pkg)| {
if pkg.local_source.is_some() {
return None;
}
Some((dep_path, pkg))
})
.map(|(dep_path, pkg)| {
let mut local_stats = LinkStats::default();
let local_aube_entry =
aube_dir.join(self.aube_dir_entry_name(dep_path));
let global_entry =
self.virtual_store.join(self.virtual_store_subdir(dep_path));
let state = classify_entry_state(&local_aube_entry, &global_entry);
if matches!(state, EntryState::Fresh) {
local_stats.packages_cached += 1;
return Ok(local_stats);
}
let owned_index;
let index = match package_indices.get(dep_path) {
Some(idx) => idx,
None => {
owned_index = self
.store
.load_index(
pkg.registry_name(),
&pkg.version,
pkg.integrity.as_deref(),
)
.ok_or_else(|| {
Error::MissingPackageIndex(dep_path.to_string())
})?;
&owned_index
}
};
self.ensure_in_virtual_store(
dep_path,
pkg,
index,
&mut local_stats,
nested_link_targets.as_ref(),
)?;
if matches!(state, EntryState::Stale) {
let _ = std::fs::remove_dir(&local_aube_entry)
.or_else(|_| std::fs::remove_file(&local_aube_entry));
}
sys::create_dir_link(&global_entry, &local_aube_entry)
.map_err(|e| Error::Io(local_aube_entry.clone(), e))?;
Ok(local_stats)
})
.collect()
});
for result in step1_results {
let local_stats = result?;
stats.packages_linked += local_stats.packages_linked;
stats.packages_cached += local_stats.packages_cached;
stats.files_linked += local_stats.files_linked;
}
tracing::debug!("link:step1 (gvs populate) {:.1?}", step1_timer.elapsed());
} else {
use rayon::prelude::*;
let link_parallelism = self.link_parallelism();
let step1_results: Vec<Result<LinkStats, Error>> =
with_link_pool(link_parallelism, || {
graph
.packages
.par_iter()
.filter_map(|(dep_path, pkg)| {
if pkg.local_source.is_some() {
return None;
}
Some((dep_path, pkg))
})
.map(|(dep_path, pkg)| {
let mut local_stats = LinkStats::default();
let aube_entry = aube_dir.join(self.aube_dir_entry_name(dep_path));
if aube_entry.exists() {
local_stats.packages_cached += 1;
return Ok(local_stats);
}
let owned_index;
let index = match package_indices.get(dep_path) {
Some(idx) => idx,
None => {
owned_index = self
.store
.load_index(
pkg.registry_name(),
&pkg.version,
pkg.integrity.as_deref(),
)
.ok_or_else(|| {
Error::MissingPackageIndex(dep_path.to_string())
})?;
&owned_index
}
};
self.materialize_into(
&aube_dir,
dep_path,
pkg,
index,
&mut local_stats,
false,
nested_link_targets.as_ref(),
)?;
Ok(local_stats)
})
.collect()
});
for result in step1_results {
let local_stats = result?;
stats.packages_linked += local_stats.packages_linked;
stats.packages_cached += local_stats.packages_cached;
stats.files_linked += local_stats.files_linked;
}
}
if self.virtual_store_only {
self.link_hidden_hoist(&aube_dir, graph)?;
if let Err(e) = write_applied_patches(&nm, &curr_applied) {
tracing::error!(
code = aube_codes::errors::ERR_AUBE_PATCHES_TRACKING_WRITE,
"failed to write .aube-applied-patches.json: {e}. next install may miss stale patched entries"
);
}
return Ok(stats);
}
use rayon::prelude::*;
let root_deps: Vec<_> = graph.root_deps().to_vec();
let link_parallelism = self.link_parallelism();
let step2_timer = std::time::Instant::now();
let results: Vec<Result<bool, Error>> = with_link_pool(link_parallelism, || {
root_deps
.par_iter()
.map(|dep| {
let target_dir = nm.join(&dep.name);
if let Some(pkg) = graph.packages.get(&dep.dep_path)
&& let Some(LocalSource::Link(rel)) = pkg.local_source.as_ref()
{
let abs_target = project_dir.join(rel);
let link_parent = target_dir.parent().unwrap_or(&nm);
let rel_target =
pathdiff::diff_paths(&abs_target, link_parent).unwrap_or(abs_target);
if reconcile_top_level_link(&target_dir, &rel_target)? {
return Ok(false);
}
if let Some(parent) = target_dir.parent() {
mkdirp(parent)?;
}
sys::create_dir_link(&rel_target, &target_dir)
.map_err(|e| Error::Io(target_dir.clone(), e))?;
return Ok(true);
}
let source_dir = aube_dir
.join(self.aube_dir_entry_name(&dep.dep_path))
.join("node_modules")
.join(&dep.name);
if !source_dir.exists() {
return Ok(false);
}
let link_parent = target_dir.parent().unwrap_or(&nm);
let rel_target = pathdiff::diff_paths(&source_dir, link_parent)
.unwrap_or_else(|| source_dir.clone());
if reconcile_top_level_link(&target_dir, &rel_target)? {
return Ok(false);
}
if let Some(parent) = target_dir.parent() {
mkdirp(parent)?;
}
sys::create_dir_link(&rel_target, &target_dir)
.map_err(|e| Error::Io(target_dir.clone(), e))?;
trace!("top-level: {}", dep.name);
Ok(true)
})
.collect()
});
for result in results {
if result? {
stats.top_level_linked += 1;
}
}
tracing::debug!(
"link:step2 (top-level symlinks) {:.1?}",
step2_timer.elapsed()
);
if !self.public_hoist_patterns.is_empty() {
self.hoist_remaining_into(
&nm,
&aube_dir,
graph,
&mut stats,
"public-hoist",
&|name| self.public_hoist_matches(name),
)?;
}
if self.shamefully_hoist {
self.hoist_remaining_into(&nm, &aube_dir, graph, &mut stats, "hoist", &|_| true)?;
}
self.link_hidden_hoist(&aube_dir, graph)?;
if let Err(e) = write_applied_patches(&nm, &curr_applied) {
tracing::error!(
code = aube_codes::errors::ERR_AUBE_PATCHES_TRACKING_WRITE,
"failed to write .aube-applied-patches.json: {e}. next install may miss stale patched entries"
);
}
Ok(stats)
}
fn link_workspace_hoisted(
&self,
root_dir: &Path,
graph: &LockfileGraph,
package_indices: &BTreeMap<String, PackageIndex>,
workspace_dirs: &BTreeMap<String, PathBuf>,
) -> Result<LinkStats, Error> {
let mut stats = LinkStats::default();
let mut placements = HoistedPlacements::default();
for (importer_path, deps) in &graph.importers {
if !is_physical_importer(importer_path) {
continue;
}
let importer_dir = if importer_path == "." {
root_dir.to_path_buf()
} else {
root_dir.join(importer_path)
};
let planner_deps: Vec<aube_lockfile::DirectDep> = deps
.iter()
.filter(|d| !workspace_dirs.contains_key(&d.name))
.cloned()
.collect();
hoisted::link_hoisted_importer(
self,
&importer_dir,
&planner_deps,
graph,
package_indices,
&mut stats,
&mut placements,
)?;
let nm = importer_dir.join(&self.modules_dir_name);
if !self.hoist_workspace_packages {
continue;
}
for dep in deps {
let Some(ws_dir) = workspace_dirs.get(&dep.name) else {
continue;
};
let link_path = nm.join(&dep.name);
if let Some(parent) = link_path.parent() {
mkdirp(parent)?;
}
try_remove_entry(&link_path);
let link_parent = link_path.parent().unwrap_or(&nm);
let target = pathdiff::diff_paths(ws_dir, link_parent).unwrap_or(ws_dir.clone());
sys::create_dir_link(&target, &link_path)
.map_err(|e| Error::Io(link_path.clone(), e))?;
stats.top_level_linked += 1;
}
}
let _ = crate::remove_dir_all_with_retry(&self.aube_dir_for(root_dir).join("node_modules"));
stats.hoisted_placements = Some(placements);
Ok(stats)
}
pub fn link_workspace(
&self,
root_dir: &Path,
graph: &LockfileGraph,
package_indices: &BTreeMap<String, PackageIndex>,
workspace_dirs: &BTreeMap<String, PathBuf>,
) -> Result<LinkStats, Error> {
if matches!(self.node_linker, NodeLinker::Hoisted) {
return self.link_workspace_hoisted(root_dir, graph, package_indices, workspace_dirs);
}
let root_nm = root_dir.join(&self.modules_dir_name);
let aube_dir = self.aube_dir_for(root_dir);
mkdirp(&aube_dir)?;
mkdirp(&root_nm)?;
let mut stats = LinkStats::default();
let prev_applied = read_applied_patches(&root_nm);
let curr_applied = current_patch_hashes(&self.patches);
if !self.use_global_virtual_store {
wipe_changed_patched_entries(
&aube_dir,
graph,
&prev_applied,
&curr_applied,
self.virtual_store_dir_max_length,
);
}
let nested_link_targets = build_nested_link_targets(root_dir, graph);
for (dep_path, pkg) in &graph.packages {
let Some(ref local) = pkg.local_source else {
continue;
};
if matches!(local, LocalSource::Link(_)) {
continue;
}
let Some(index) = package_indices.get(dep_path) else {
continue;
};
let aube_entry = aube_dir.join(self.aube_dir_entry_name(dep_path));
if aube_entry.exists() {
stats.packages_cached += 1;
continue;
}
self.materialize_into(
&aube_dir,
dep_path,
pkg,
index,
&mut stats,
false,
nested_link_targets.as_ref(),
)?;
}
if self.use_global_virtual_store {
use rayon::prelude::*;
use rustc_hash::FxHashSet;
let mut step1_parents: FxHashSet<PathBuf> = FxHashSet::default();
for (dep_path, pkg) in &graph.packages {
if pkg.local_source.is_some() {
continue;
}
let entry = aube_dir.join(self.aube_dir_entry_name(dep_path));
if let Some(parent) = entry.parent() {
step1_parents.insert(parent.to_path_buf());
}
}
for parent in &step1_parents {
mkdirp(parent)?;
}
let link_parallelism = self.link_parallelism();
let step1_timer = std::time::Instant::now();
let step1_results: Vec<Result<LinkStats, Error>> =
with_link_pool(link_parallelism, || {
graph
.packages
.par_iter()
.filter_map(|(dep_path, pkg)| {
if pkg.local_source.is_some() {
return None;
}
Some((dep_path, pkg))
})
.map(|(dep_path, pkg)| {
let mut local_stats = LinkStats::default();
let local_aube_entry =
aube_dir.join(self.aube_dir_entry_name(dep_path));
let global_entry =
self.virtual_store.join(self.virtual_store_subdir(dep_path));
let state = classify_entry_state(&local_aube_entry, &global_entry);
if matches!(state, EntryState::Fresh) {
local_stats.packages_cached += 1;
return Ok(local_stats);
}
let owned_index;
let index = match package_indices.get(dep_path) {
Some(idx) => idx,
None => {
owned_index = self
.store
.load_index(
pkg.registry_name(),
&pkg.version,
pkg.integrity.as_deref(),
)
.ok_or_else(|| {
Error::MissingPackageIndex(dep_path.to_string())
})?;
&owned_index
}
};
self.ensure_in_virtual_store(
dep_path,
pkg,
index,
&mut local_stats,
nested_link_targets.as_ref(),
)?;
if matches!(state, EntryState::Stale) {
let _ = std::fs::remove_dir(&local_aube_entry)
.or_else(|_| std::fs::remove_file(&local_aube_entry));
}
sys::create_dir_link(&global_entry, &local_aube_entry)
.map_err(|e| Error::Io(local_aube_entry.clone(), e))?;
Ok(local_stats)
})
.collect()
});
for result in step1_results {
let local_stats = result?;
stats.packages_linked += local_stats.packages_linked;
stats.packages_cached += local_stats.packages_cached;
stats.files_linked += local_stats.files_linked;
}
tracing::debug!(
"link_workspace:step1 (gvs populate) {:.1?}",
step1_timer.elapsed()
);
} else {
use rayon::prelude::*;
let link_parallelism = self.link_parallelism();
let step1_results: Vec<Result<LinkStats, Error>> =
with_link_pool(link_parallelism, || {
graph
.packages
.par_iter()
.filter_map(|(dep_path, pkg)| {
if pkg.local_source.is_some() {
return None;
}
Some((dep_path, pkg))
})
.map(|(dep_path, pkg)| {
let mut local_stats = LinkStats::default();
let aube_entry = aube_dir.join(self.aube_dir_entry_name(dep_path));
if aube_entry.exists() {
local_stats.packages_cached += 1;
return Ok(local_stats);
}
let owned_index;
let index = match package_indices.get(dep_path) {
Some(idx) => idx,
None => {
owned_index = self
.store
.load_index(
pkg.registry_name(),
&pkg.version,
pkg.integrity.as_deref(),
)
.ok_or_else(|| {
Error::MissingPackageIndex(dep_path.to_string())
})?;
&owned_index
}
};
self.materialize_into(
&aube_dir,
dep_path,
pkg,
index,
&mut local_stats,
false,
nested_link_targets.as_ref(),
)?;
Ok(local_stats)
})
.collect()
});
for result in step1_results {
let local_stats = result?;
stats.packages_linked += local_stats.packages_linked;
stats.packages_cached += local_stats.packages_cached;
stats.files_linked += local_stats.files_linked;
}
}
if self.virtual_store_only {
let aube_dir_leaf: Option<std::ffi::OsString> =
if aube_dir.parent() == Some(root_nm.as_path()) {
aube_dir.file_name().map(|s| s.to_owned())
} else {
None
};
if let Ok(entries) = std::fs::read_dir(&root_nm) {
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with('.') {
continue;
}
if aube_dir_leaf.as_deref() == Some(name.as_os_str()) {
continue;
}
try_remove_entry(&entry.path());
}
}
self.link_hidden_hoist(&aube_dir, graph)?;
if let Err(e) = write_applied_patches(&root_nm, &curr_applied) {
tracing::error!(
code = aube_codes::errors::ERR_AUBE_PATCHES_TRACKING_WRITE,
"failed to write .aube-applied-patches.json: {e}. next install may miss stale patched entries"
);
}
return Ok(stats);
}
let root_deps_by_name: std::collections::HashMap<&str, &aube_lockfile::DirectDep> =
if self.dedupe_direct_deps {
graph
.importers
.get(".")
.map(|deps| deps.iter().map(|d| (d.name.as_str(), d)).collect())
.unwrap_or_default()
} else {
std::collections::HashMap::new()
};
let aube_dir_leaf_root: Option<std::ffi::OsString> =
if aube_dir.parent() == Some(root_nm.as_path()) {
aube_dir.file_name().map(|s| s.to_owned())
} else {
None
};
for (importer_path, deps) in &graph.importers {
if !is_physical_importer(importer_path) {
continue;
}
let nm = if importer_path == "." {
root_nm.clone()
} else {
root_dir.join(importer_path).join(&self.modules_dir_name)
};
if importer_path != "." {
mkdirp(&nm)?;
}
let mut preserve: std::collections::HashSet<&str> =
deps.iter().map(|d| d.name.as_str()).collect();
if importer_path == "." {
if self.shamefully_hoist {
for pkg in graph.packages.values() {
preserve.insert(pkg.name.as_str());
}
} else if !self.public_hoist_patterns.is_empty() {
for pkg in graph.packages.values() {
if pkg.local_source.is_none() && self.public_hoist_matches(&pkg.name) {
preserve.insert(pkg.name.as_str());
}
}
}
}
let aube_leaf_here = if importer_path == "." {
aube_dir_leaf_root.as_deref()
} else {
None
};
sweep_stale_top_level_entries(&nm, &preserve, aube_leaf_here);
}
use rayon::prelude::*;
#[derive(Clone)]
struct Step2Task<'a> {
importer_path: &'a str,
nm: PathBuf,
dep: &'a aube_lockfile::DirectDep,
}
let tasks: Vec<Step2Task<'_>> = graph
.importers
.iter()
.filter(|(importer_path, _)| is_physical_importer(importer_path))
.flat_map(|(importer_path, deps)| {
let nm = if importer_path == "." {
root_nm.clone()
} else {
root_dir.join(importer_path).join(&self.modules_dir_name)
};
deps.iter().map(move |dep| Step2Task {
importer_path: importer_path.as_str(),
nm: nm.clone(),
dep,
})
})
.collect();
let link_parallelism = self.link_parallelism();
let step2_timer = std::time::Instant::now();
let step2_results: Vec<Result<bool, Error>> = with_link_pool(link_parallelism, || {
tasks
.par_iter()
.map(|task| {
let Step2Task {
importer_path,
nm,
dep,
} = task;
if self.dedupe_direct_deps
&& *importer_path != "."
&& let Some(root_dep) = root_deps_by_name.get(dep.name.as_str())
&& root_dep.dep_path == dep.dep_path
{
return Ok(false);
}
let link_path = nm.join(&dep.name);
if let Some(ws_dir) = workspace_dirs.get(&dep.name) {
if !self.hoist_workspace_packages {
return Ok(false);
}
let link_parent = link_path.parent().unwrap_or(nm);
let rel_target =
pathdiff::diff_paths(ws_dir, link_parent).unwrap_or(ws_dir.clone());
if reconcile_top_level_link(&link_path, &rel_target)? {
return Ok(false);
}
if let Some(parent) = link_path.parent() {
mkdirp(parent)?;
}
sys::create_dir_link(&rel_target, &link_path)
.map_err(|e| Error::Io(link_path.clone(), e))?;
return Ok(true);
}
if let Some(locked) = graph.packages.get(&dep.dep_path)
&& let Some(LocalSource::Link(rel)) = locked.local_source.as_ref()
{
let abs_target = root_dir.join(rel);
let link_parent = link_path.parent().unwrap_or(nm);
let rel_target =
pathdiff::diff_paths(&abs_target, link_parent).unwrap_or(abs_target);
if reconcile_top_level_link(&link_path, &rel_target)? {
return Ok(false);
}
if let Some(parent) = link_path.parent() {
mkdirp(parent)?;
}
sys::create_dir_link(&rel_target, &link_path)
.map_err(|e| Error::Io(link_path.clone(), e))?;
return Ok(true);
}
let source_dir = aube_dir
.join(self.aube_dir_entry_name(&dep.dep_path))
.join("node_modules")
.join(&dep.name);
if !source_dir.exists() {
return Ok(false);
}
let link_parent = link_path.parent().unwrap_or(nm);
let rel_target = pathdiff::diff_paths(&source_dir, link_parent)
.unwrap_or_else(|| source_dir.clone());
if reconcile_top_level_link(&link_path, &rel_target)? {
return Ok(false);
}
if let Some(parent) = link_path.parent() {
mkdirp(parent)?;
}
sys::create_dir_link(&rel_target, &link_path)
.map_err(|e| Error::Io(link_path.clone(), e))?;
trace!("workspace top-level: {} -> {}", dep.name, importer_path);
Ok(true)
})
.collect()
});
for result in step2_results {
if result? {
stats.top_level_linked += 1;
}
}
tracing::debug!(
"link_workspace:step2 (top-level symlinks) {:.1?}",
step2_timer.elapsed()
);
if !self.public_hoist_patterns.is_empty() {
self.hoist_remaining_into(
&root_nm,
&aube_dir,
graph,
&mut stats,
"workspace public-hoist",
&|name| self.public_hoist_matches(name),
)?;
}
if self.shamefully_hoist {
self.hoist_remaining_into(
&root_nm,
&aube_dir,
graph,
&mut stats,
"workspace hoist",
&|_| true,
)?;
}
self.link_hidden_hoist(&aube_dir, graph)?;
if let Err(e) = write_applied_patches(&root_nm, &curr_applied) {
tracing::error!(
code = aube_codes::errors::ERR_AUBE_PATCHES_TRACKING_WRITE,
"failed to write .aube-applied-patches.json: {e}. next install may miss stale patched entries"
);
}
Ok(stats)
}
fn link_hidden_hoist(&self, aube_dir: &Path, graph: &LockfileGraph) -> Result<(), Error> {
self.link_hidden_hoist_at(aube_dir, aube_dir, graph, false, true)?;
if self.use_global_virtual_store {
self.link_hidden_hoist_at(
&self.virtual_store,
&self.virtual_store,
graph,
true,
false,
)?;
}
Ok(())
}
fn link_hidden_hoist_at(
&self,
hidden_root: &Path,
source_root: &Path,
graph: &LockfileGraph,
use_hashed_subdirs: bool,
sweep_stale_entries: bool,
) -> Result<(), Error> {
let hidden = hidden_root.join("node_modules");
let mut claimed: rustc_hash::FxHashSet<&str> = rustc_hash::FxHashSet::default();
let packages: Vec<_> = if self.hoist {
graph
.packages
.iter()
.filter_map(|(dep_path, pkg)| {
if pkg.local_source.is_some() || !self.hoist_matches(&pkg.name) {
return None;
}
claimed.insert(pkg.name.as_str()).then_some((dep_path, pkg))
})
.collect()
} else {
Vec::new()
};
if !self.hoist {
if sweep_stale_entries {
remove_hidden_hoist_tree(&hidden);
} else {
sweep_dead_hidden_hoist_entries(&hidden);
}
return Ok(());
}
if sweep_stale_entries {
remove_hidden_hoist_tree(&hidden);
} else {
sweep_dead_hidden_hoist_entries(&hidden);
}
for (dep_path, pkg) in packages {
let source_subdir = if use_hashed_subdirs {
self.virtual_store_subdir(dep_path)
} else {
self.aube_dir_entry_name(dep_path)
};
let source_dir = source_root
.join(source_subdir)
.join("node_modules")
.join(&pkg.name);
if !source_dir.exists() {
continue;
}
let target_dir = hidden.join(&pkg.name);
if let Some(parent) = target_dir.parent() {
mkdirp(parent)?;
}
let link_parent = target_dir.parent().unwrap_or(&hidden);
let rel_target = pathdiff::diff_paths(&source_dir, link_parent)
.unwrap_or_else(|| source_dir.clone());
if reconcile_top_level_link(&target_dir, &rel_target)? {
continue;
}
sys::create_dir_link(&rel_target, &target_dir)
.map_err(|e| Error::Io(target_dir.clone(), e))?;
trace!("hidden-hoist: {}", pkg.name);
}
Ok(())
}
fn hoist_remaining_into(
&self,
nm: &Path,
aube_dir: &Path,
graph: &LockfileGraph,
stats: &mut LinkStats,
trace_label: &str,
select: &dyn Fn(&str) -> bool,
) -> Result<(), Error> {
let direct_dep_names: std::collections::HashSet<&str> =
graph.root_deps().iter().map(|d| d.name.as_str()).collect();
let mut claimed: rustc_hash::FxHashSet<&str> = rustc_hash::FxHashSet::default();
for (dep_path, pkg) in &graph.packages {
if pkg.local_source.is_some() {
continue;
}
if !select(&pkg.name) {
continue;
}
if direct_dep_names.contains(pkg.name.as_str()) {
continue;
}
if !claimed.insert(pkg.name.as_str()) {
continue;
}
let source_dir = aube_dir
.join(self.aube_dir_entry_name(dep_path))
.join("node_modules")
.join(&pkg.name);
if !source_dir.exists() {
continue;
}
let target_dir = nm.join(&pkg.name);
let link_parent = target_dir.parent().unwrap_or(nm);
let rel_target = pathdiff::diff_paths(&source_dir, link_parent)
.unwrap_or_else(|| source_dir.clone());
if reconcile_top_level_link(&target_dir, &rel_target)? {
continue;
}
if let Some(parent) = target_dir.parent() {
mkdirp(parent)?;
}
sys::create_dir_link(&rel_target, &target_dir)
.map_err(|e| Error::Io(target_dir.clone(), e))?;
trace!("{trace_label}: {}", pkg.name);
stats.top_level_linked += 1;
}
Ok(())
}
pub fn ensure_in_virtual_store(
&self,
dep_path: &str,
pkg: &LockedPackage,
index: &PackageIndex,
stats: &mut LinkStats,
nested_link_targets: Option<&BTreeMap<String, PathBuf>>,
) -> Result<(), Error> {
let subdir = self.virtual_store_subdir(dep_path);
let pkg_nm_dir = self
.virtual_store
.join(&subdir)
.join("node_modules")
.join(&pkg.name);
if pkg_nm_dir.exists() {
trace!("virtual store hit: {dep_path}");
stats.packages_cached += 1;
return Ok(());
}
let tmp_name = format!(".tmp-{}-{subdir}", std::process::id());
let tmp_base = self.virtual_store.join(&tmp_name);
let result = self.materialize_into(
&tmp_base,
dep_path,
pkg,
index,
stats,
true,
nested_link_targets,
);
if result.is_err() {
let _ = std::fs::remove_dir_all(&tmp_base);
return result;
}
let tmp_entry = tmp_base.join(&subdir);
let final_entry = self.virtual_store.join(&subdir);
if let Some(parent) = final_entry.parent() {
mkdirp(parent)?;
}
match aube_util::fs_atomic::rename_with_retry(&tmp_entry, &final_entry) {
Ok(()) => {
trace!("atomically placed {subdir} in virtual store");
}
Err(e) if final_entry.exists() => {
trace!("lost rename race for {dep_path}, using existing: {e}");
stats.packages_linked = stats.packages_linked.saturating_sub(1);
stats.files_linked = stats.files_linked.saturating_sub(index.len());
stats.packages_cached += 1;
let _ = std::fs::remove_dir_all(&tmp_base);
return Ok(());
}
Err(e) => {
let _ = std::fs::remove_dir_all(&tmp_base);
return Err(Error::Io(final_entry, e));
}
}
if let Err(e) = std::fs::remove_dir(&tmp_base) {
debug!(
"remove_dir({}) failed, leaving tmp in place: {e}",
tmp_base.display()
);
}
Ok(())
}
pub fn ensure_in_aube_dir(
&self,
aube_dir: &Path,
dep_path: &str,
pkg: &LockedPackage,
index: &PackageIndex,
stats: &mut LinkStats,
nested_link_targets: Option<&BTreeMap<String, PathBuf>>,
) -> Result<(), Error> {
let entry = aube_dir.join(self.aube_dir_entry_name(dep_path));
if entry.exists() {
stats.packages_cached += 1;
return Ok(());
}
self.materialize_into(
aube_dir,
dep_path,
pkg,
index,
stats,
false,
nested_link_targets,
)
}
#[allow(clippy::too_many_arguments)]
fn materialize_into(
&self,
base_dir: &Path,
dep_path: &str,
pkg: &LockedPackage,
index: &PackageIndex,
stats: &mut LinkStats,
apply_hashes: bool,
nested_link_targets: Option<&BTreeMap<String, PathBuf>>,
) -> Result<(), Error> {
let subdir = if apply_hashes {
self.virtual_store_subdir(dep_path)
} else {
self.aube_dir_entry_name(dep_path)
};
let pkg_nm_dir = base_dir.join(&subdir).join("node_modules").join(&pkg.name);
let pkg_nm_parent = base_dir.join(&subdir).join("node_modules");
let mut parents: Vec<PathBuf> = Vec::with_capacity(index.len() / 4 + 4);
parents.push(pkg_nm_dir.clone());
for rel_path in index.keys() {
validate_index_key(rel_path)?;
let target = pkg_nm_dir.join(rel_path);
if let Some(parent) = target.parent() {
parents.push(parent.to_path_buf());
}
}
for dep_name in pkg.dependencies.keys() {
if let Some(slash) = dep_name.find('/')
&& dep_name.starts_with('@')
{
parents.push(pkg_nm_parent.join(&dep_name[..slash]));
}
}
parents.sort_unstable();
parents.dedup();
for parent in &parents {
std::fs::create_dir_all(parent).map_err(|e| Error::Io(parent.clone(), e))?;
}
for (rel_path, stored) in index {
let target = pkg_nm_dir.join(rel_path);
if let Err(e) = self.link_file_fresh(stored, rel_path, &target) {
if let Error::MissingStoreFile { .. } = &e {
invalidate_stale_index_for_package(&self.store, pkg);
}
return Err(e);
}
stats.files_linked += 1;
if stored.executable {
#[cfg(unix)]
xx::file::make_executable(&target).map_err(|e| Error::Xx(e.to_string()))?;
}
}
let patch_key = pkg.spec_key();
if let Some(patch_text) = self.patches.get(&patch_key) {
apply_multi_file_patch(&pkg_nm_dir, patch_text)
.map_err(|msg| Error::Patch(patch_key.clone(), msg))?;
}
for (dep_name, dep_version) in &pkg.dependencies {
let dep_dep_path = format!("{dep_name}@{dep_version}");
if dep_name == &pkg.name {
continue;
}
let symlink_path = pkg_nm_parent.join(dep_name);
if let Some(map) = nested_link_targets
&& let Some(abs_target) = map.get(&dep_dep_path)
{
sys::create_dir_link(abs_target, &symlink_path)
.map_err(|e| Error::Io(symlink_path.clone(), e))?;
continue;
}
let sibling_subdir = if apply_hashes {
self.virtual_store_subdir(&dep_dep_path)
} else {
self.aube_dir_entry_name(&dep_dep_path)
};
let virtual_root = pkg_nm_parent
.parent()
.and_then(Path::parent)
.unwrap_or(&pkg_nm_parent);
let sibling_abs = virtual_root
.join(&sibling_subdir)
.join("node_modules")
.join(dep_name);
let link_parent = symlink_path.parent().unwrap_or(&pkg_nm_parent);
let target = pathdiff::diff_paths(&sibling_abs, link_parent)
.unwrap_or_else(|| sibling_abs.clone());
#[cfg(windows)]
let target = if apply_hashes {
self.virtual_store
.join(&sibling_subdir)
.join("node_modules")
.join(dep_name)
} else {
target
};
sys::create_dir_link(&target, &symlink_path)
.map_err(|e| Error::Io(symlink_path.clone(), e))?;
}
stats.packages_linked += 1;
trace!("materialized {dep_path} ({} files)", index.len());
Ok(())
}
pub(crate) fn link_file_fresh(
&self,
stored: &StoredFile,
rel_path: &str,
dst: &Path,
) -> Result<(), Error> {
#[cfg(target_os = "macos")]
const SMALL_FILE_COPY_MAX: u64 = 16 * 1024;
let map_io = |e: std::io::Error| classify_link_error(stored, rel_path, dst, e);
let missing_source = || Error::MissingStoreFile {
store_path: stored.store_path.clone(),
rel_path: rel_path.to_string(),
};
match self.strategy {
LinkStrategy::Reflink => {
#[cfg(target_os = "macos")]
if matches!(stored.size, Some(size) if size <= SMALL_FILE_COPY_MAX) {
std::fs::copy(&stored.store_path, dst).map_err(map_io)?;
return Ok(());
}
if let Err(e) = reflink_copy::reflink(&stored.store_path, dst) {
if !stored.store_path.exists() {
return Err(missing_source());
}
trace!("reflink failed, falling back to copy: {e}");
std::fs::copy(&stored.store_path, dst).map_err(map_io)?;
}
}
LinkStrategy::Hardlink => {
if let Err(e) = std::fs::hard_link(&stored.store_path, dst) {
if !stored.store_path.exists() {
return Err(missing_source());
}
trace!("hardlink failed, falling back to copy: {e}");
std::fs::copy(&stored.store_path, dst).map_err(map_io)?;
}
}
LinkStrategy::Copy => {
std::fs::copy(&stored.store_path, dst).map_err(map_io)?;
}
}
Ok(())
}
}
fn classify_link_error(
stored: &StoredFile,
rel_path: &str,
dst: &Path,
err: std::io::Error,
) -> Error {
if err.kind() == std::io::ErrorKind::NotFound && !stored.store_path.exists() {
return Error::MissingStoreFile {
store_path: stored.store_path.clone(),
rel_path: rel_path.to_string(),
};
}
Error::Io(dst.to_path_buf(), err)
}
pub(crate) fn invalidate_stale_index_for_package(store: &aube_store::Store, pkg: &LockedPackage) {
match store.invalidate_cached_index(pkg.registry_name(), &pkg.version, pkg.integrity.as_deref())
{
Ok(true) => debug!("invalidated stale index for {}", pkg.spec_key()),
Ok(false) => {}
Err(e) => warn!(
"failed to invalidate stale index for {}: {e}; manual recovery: rm -rf ~/.cache/aube/index",
pkg.spec_key()
),
}
}
#[derive(Debug, Default)]
pub struct LinkStats {
pub packages_linked: usize,
pub packages_cached: usize,
pub files_linked: usize,
pub top_level_linked: usize,
pub hoisted_placements: Option<HoistedPlacements>,
}
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum Error {
#[error("I/O error at {0}: {1}")]
Io(PathBuf, std::io::Error),
#[error("file error: {0}")]
Xx(String),
#[error("failed to link {0} -> {1}: {2}")]
#[diagnostic(code(ERR_AUBE_LINK_FAILED))]
Link(PathBuf, PathBuf, String),
#[error("failed to apply patch for {0}: {1}")]
#[diagnostic(code(ERR_AUBE_PATCH_FAILED))]
Patch(String, String),
#[error(
"internal: missing package index for {0} — caller skipped `load_index` but the package wasn't already materialized"
)]
#[diagnostic(code(ERR_AUBE_MISSING_PACKAGE_INDEX))]
MissingPackageIndex(String),
#[error("refusing to materialize unsafe index key: {0:?}")]
#[diagnostic(code(ERR_AUBE_UNSAFE_INDEX_KEY))]
UnsafeIndexKey(String),
#[error(
"cached package index references a missing CAS shard at {store_path} (file: {rel_path:?}). The store and its index cache are out of sync — rerun the install to re-fetch the tarball."
)]
#[diagnostic(code(ERR_AUBE_MISSING_STORE_FILE))]
MissingStoreFile {
store_path: PathBuf,
rel_path: String,
},
}
fn validate_index_key(key: &str) -> Result<(), Error> {
if key.is_empty()
|| key.starts_with('/')
|| key.starts_with('\\')
|| key.contains('\0')
|| key.contains('\\')
{
return Err(Error::UnsafeIndexKey(key.to_string()));
}
for component in std::path::Path::new(key).components() {
match component {
std::path::Component::ParentDir
| std::path::Component::RootDir
| std::path::Component::Prefix(_) => {
return Err(Error::UnsafeIndexKey(key.to_string()));
}
std::path::Component::Normal(os) => {
#[cfg(windows)]
{
if let Some(s) = os.to_str()
&& s.contains(':')
{
return Err(Error::UnsafeIndexKey(key.to_string()));
}
}
#[cfg(not(windows))]
{
let _ = os;
}
}
std::path::Component::CurDir => {}
}
}
Ok(())
}
fn reconcile_top_level_link(link_path: &Path, expected_target: &Path) -> Result<bool, Error> {
#[cfg(windows)]
{
use std::sync::OnceLock;
static CANON_CACHE: OnceLock<
std::sync::RwLock<std::collections::HashMap<PathBuf, PathBuf>>,
> = OnceLock::new();
fn cached_canonicalize(p: &Path) -> std::io::Result<PathBuf> {
let map = CANON_CACHE.get_or_init(Default::default);
if let Some(hit) = map.read().expect("canon cache poisoned").get(p) {
return Ok(hit.clone());
}
let canon = p.canonicalize()?;
map.write()
.expect("canon cache poisoned")
.insert(p.to_path_buf(), canon.clone());
Ok(canon)
}
let expected_abs = if expected_target.is_absolute() {
expected_target.to_path_buf()
} else {
let parent = link_path.parent().unwrap_or_else(|| Path::new(""));
parent.join(expected_target)
};
if let Ok(link_canon) = cached_canonicalize(link_path)
&& let Ok(exp_canon) = cached_canonicalize(&expected_abs)
&& link_canon == exp_canon
{
return Ok(true);
}
if link_path.symlink_metadata().is_err() {
return Ok(false);
}
match std::fs::remove_dir(link_path).or_else(|_| std::fs::remove_file(link_path)) {
Ok(()) => Ok(false),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(Error::Io(link_path.to_path_buf(), e)),
}
}
#[cfg(not(windows))]
{
match std::fs::read_link(link_path) {
Ok(existing) if existing == expected_target => Ok(true),
Ok(_) => {
let _ = std::fs::remove_dir(link_path).or_else(|_| std::fs::remove_file(link_path));
Ok(false)
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(_) => {
let _ =
std::fs::remove_dir_all(link_path).or_else(|_| std::fs::remove_file(link_path));
Ok(false)
}
}
}
}
fn current_patch_hashes(patches: &Patches) -> std::collections::BTreeMap<String, String> {
use sha2::{Digest, Sha256};
patches
.iter()
.map(|(k, v)| {
let mut h = Sha256::new();
h.update(v.as_bytes());
(k.clone(), hex::encode(h.finalize()))
})
.collect()
}
pub fn build_nested_link_targets(
project_dir: &Path,
graph: &LockfileGraph,
) -> Option<BTreeMap<String, PathBuf>> {
let map: BTreeMap<String, PathBuf> = graph
.packages
.iter()
.filter_map(|(dp, pkg)| match pkg.local_source.as_ref() {
Some(LocalSource::Link(rel)) => Some((dp.clone(), project_dir.join(rel))),
_ => None,
})
.collect();
if map.is_empty() { None } else { Some(map) }
}
fn read_applied_patches(nm_dir: &Path) -> std::collections::BTreeMap<String, String> {
let path = nm_dir.join(".aube-applied-patches.json");
let Ok(raw) = std::fs::read_to_string(&path) else {
return Default::default();
};
serde_json_parse_map(&raw).unwrap_or_default()
}
fn serde_json_parse_map(s: &str) -> Option<std::collections::BTreeMap<String, String>> {
serde_json::from_str(s).ok()
}
fn write_applied_patches(
nm_dir: &Path,
map: &std::collections::BTreeMap<String, String>,
) -> std::io::Result<()> {
let path = nm_dir.join(".aube-applied-patches.json");
let out = serde_json::to_string(map)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
aube_util::fs_atomic::atomic_write(&path, out.as_bytes())
}
fn wipe_changed_patched_entries(
aube_dir: &Path,
graph: &LockfileGraph,
prev: &std::collections::BTreeMap<String, String>,
curr: &std::collections::BTreeMap<String, String>,
max_length: usize,
) {
let mut affected: std::collections::HashSet<String> = std::collections::HashSet::new();
for k in prev.keys().chain(curr.keys()) {
if prev.get(k) != curr.get(k) {
affected.insert(k.clone());
}
}
if affected.is_empty() {
return;
}
for (dep_path, pkg) in &graph.packages {
let key = pkg.spec_key();
if affected.contains(&key) {
let entry = aube_dir.join(dep_path_to_filename(dep_path, max_length));
let _ = std::fs::remove_dir_all(entry);
}
}
}
fn is_safe_rel_component(rel: &str) -> bool {
if rel.is_empty() || rel.contains('\0') || rel.contains('\\') {
return false;
}
let p = Path::new(rel);
if p.is_absolute()
|| p.has_root()
|| rel.starts_with('/')
|| rel.len() >= 2 && rel.as_bytes()[1] == b':'
{
return false;
}
p.components().all(|c| {
matches!(
c,
std::path::Component::Normal(_) | std::path::Component::CurDir
)
})
}
fn ensure_no_symlink_in_chain(pkg_dir: &Path, rel: &str) -> Result<(), String> {
let mut cursor = pkg_dir.to_path_buf();
for comp in Path::new(rel).components() {
cursor.push(comp);
match std::fs::symlink_metadata(&cursor) {
Ok(meta) => {
if meta.file_type().is_symlink() {
return Err(format!("{}", cursor.display()));
}
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0400;
if meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(format!("{}", cursor.display()));
}
}
}
Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => break,
Err(e) => return Err(format!("stat {}: {e}", cursor.display())),
}
}
Ok(())
}
fn apply_multi_file_patch(pkg_dir: &Path, patch_text: &str) -> Result<(), String> {
let sections = split_patch_sections(patch_text);
if sections.is_empty() {
return Err("patch contained no `diff --git` sections".to_string());
}
for section in sections {
let rel = section
.rel_path
.as_ref()
.ok_or_else(|| "patch section missing file path".to_string())?;
if !is_safe_rel_component(rel) {
return Err(format!("patch file path escapes package: {rel:?}"));
}
if let Err(e) = ensure_no_symlink_in_chain(pkg_dir, rel) {
return Err(format!("patch target contains symlink: {e}"));
}
let target = pkg_dir.join(rel);
let original = if target.exists() {
std::fs::read_to_string(&target)
.map_err(|e| format!("failed to read {}: {e}", target.display()))?
} else {
String::new()
};
if section.is_deletion {
if target.exists() {
std::fs::remove_file(&target)
.map_err(|e| format!("failed to remove {}: {e}", target.display()))?;
}
continue;
}
let was_crlf = original.contains("\r\n");
let normalized = if was_crlf {
original.replace("\r\n", "\n")
} else {
original
};
let parsed = diffy::Patch::from_str(§ion.body)
.map_err(|e| format!("failed to parse patch for {rel}: {e}"))?;
let patched_lf = diffy::apply(&normalized, &parsed)
.map_err(|e| format!("failed to apply patch for {rel}: {e}"))?;
let patched = if was_crlf {
patched_lf.replace('\n', "\r\n").replace("\r\r\n", "\r\n")
} else {
patched_lf
};
#[cfg(windows)]
{
if target.exists() {
std::fs::remove_file(&target)
.map_err(|e| format!("failed to unlink {}: {e}", target.display()))?;
}
}
aube_util::fs_atomic::atomic_write(&target, patched.as_bytes()).map_err(|e| {
format!(
"failed to write patched file into place {}: {e}",
target.display()
)
})?;
}
Ok(())
}
struct PatchSection {
rel_path: Option<String>,
body: String,
is_deletion: bool,
}
fn parse_diff_git_b_path(rest: &str) -> Option<String> {
if let Some(after) = rest.strip_prefix("\"a/") {
let end_a = after.find("\" \"b/")?;
let after_b = &after[end_a + 5..];
let close = after_b.rfind('"')?;
return unescape_git_quoted(&after_b[..close]);
}
let body = rest.strip_prefix("a/")?;
let mut search_from = 0;
while let Some(rel) = body[search_from..].find(" b/") {
let abs = search_from + rel;
let path_a = &body[..abs];
let path_b = &body[abs + 3..];
if path_a == path_b {
return Some(path_b.to_string());
}
search_from = abs + 1;
}
body.find(" b/").map(|i| body[i + 3..].to_string())
}
fn unescape_git_quoted(s: &str) -> Option<String> {
let bytes = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'\\' {
out.push(bytes[i]);
i += 1;
continue;
}
if i + 1 >= bytes.len() {
return None;
}
match bytes[i + 1] {
b'\\' => {
out.push(b'\\');
i += 2;
}
b'"' => {
out.push(b'"');
i += 2;
}
b'n' => {
out.push(b'\n');
i += 2;
}
b't' => {
out.push(b'\t');
i += 2;
}
b'r' => {
out.push(b'\r');
i += 2;
}
b'a' => {
out.push(0x07);
i += 2;
}
b'b' => {
out.push(0x08);
i += 2;
}
b'f' => {
out.push(0x0C);
i += 2;
}
b'v' => {
out.push(0x0B);
i += 2;
}
d0 @ b'0'..=b'3'
if i + 3 < bytes.len()
&& (b'0'..=b'7').contains(&bytes[i + 2])
&& (b'0'..=b'7').contains(&bytes[i + 3]) =>
{
let n = ((d0 - b'0') << 6) | ((bytes[i + 2] - b'0') << 3) | (bytes[i + 3] - b'0');
out.push(n);
i += 4;
}
_ => return None,
}
}
String::from_utf8(out).ok()
}
fn split_patch_sections(text: &str) -> Vec<PatchSection> {
let mut out: Vec<PatchSection> = Vec::new();
let mut current_path: Option<String> = None;
let mut body = String::new();
let mut in_body = false;
let mut is_deletion = false;
let flush = |out: &mut Vec<PatchSection>,
path: &mut Option<String>,
body: &mut String,
is_deletion: &mut bool| {
if !body.is_empty() || *is_deletion {
out.push(PatchSection {
rel_path: path.take(),
body: std::mem::take(body),
is_deletion: std::mem::replace(is_deletion, false),
});
} else {
*path = None;
}
};
for line in text.split_inclusive('\n') {
let stripped = line.trim_end_matches(['\n', '\r']);
if let Some(rest) = stripped.strip_prefix("diff --git ") {
flush(&mut out, &mut current_path, &mut body, &mut is_deletion);
in_body = false;
current_path = parse_diff_git_b_path(rest);
continue;
}
if !in_body {
if stripped.starts_with("--- ") {
in_body = true;
if stripped == "--- /dev/null"
&& let Some(rel) = current_path.as_deref()
{
body.push_str(&format!("--- a/{rel}\n"));
} else {
body.push_str(stripped);
body.push('\n');
}
}
continue;
}
if stripped == "+++ /dev/null" {
is_deletion = true;
continue;
}
body.push_str(stripped);
body.push('\n');
}
flush(&mut out, &mut current_path, &mut body, &mut is_deletion);
out
}
#[cfg(test)]
mod importer_classification_tests {
use super::is_physical_importer;
#[test]
fn root_is_physical() {
assert!(is_physical_importer("."));
}
#[test]
fn workspace_paths_are_physical() {
assert!(is_physical_importer("packages/dev/core"));
assert!(is_physical_importer("apps/web"));
assert!(is_physical_importer("libs/@scope/name"));
}
#[test]
fn nested_peer_context_paths_are_virtual() {
assert!(!is_physical_importer(
"packages/dev/addons/node_modules/@dev/core"
));
assert!(!is_physical_importer(
"packages/a/node_modules/@s/b/node_modules/@s/c"
));
}
}
#[cfg(test)]
mod public_hoist_tests {
use super::*;
fn linker_with(patterns: &[&str]) -> Linker {
let store = Store::at(std::env::temp_dir().join("aube-public-hoist-test"));
let strs: Vec<String> = patterns.iter().map(|s| s.to_string()).collect();
Linker::new(&store, LinkStrategy::Copy).with_public_hoist_pattern(&strs)
}
#[test]
fn empty_pattern_matches_nothing() {
let l = linker_with(&[]);
assert!(!l.public_hoist_matches("react"));
assert!(!l.public_hoist_matches("eslint"));
}
#[test]
fn wildcard_matches_substring() {
let l = linker_with(&["*eslint*", "*prettier*"]);
assert!(l.public_hoist_matches("eslint"));
assert!(l.public_hoist_matches("eslint-plugin-react"));
assert!(l.public_hoist_matches("@typescript-eslint/parser"));
assert!(l.public_hoist_matches("prettier"));
assert!(!l.public_hoist_matches("react"));
}
#[test]
fn exact_name_match() {
let l = linker_with(&["react"]);
assert!(l.public_hoist_matches("react"));
assert!(!l.public_hoist_matches("react-dom"));
}
#[test]
fn negation_excludes_positive_match() {
let l = linker_with(&["*eslint*", "!eslint-config-*"]);
assert!(l.public_hoist_matches("eslint"));
assert!(l.public_hoist_matches("eslint-plugin-react"));
assert!(!l.public_hoist_matches("eslint-config-next"));
}
#[test]
fn case_insensitive() {
let l = linker_with(&["*ESLINT*"]);
assert!(l.public_hoist_matches("eslint"));
assert!(l.public_hoist_matches("ESLint"));
}
#[test]
fn invalid_patterns_are_silently_dropped() {
let l = linker_with(&["[unterminated", "react"]);
assert!(l.public_hoist_matches("react"));
assert!(!l.public_hoist_matches("eslint"));
}
}
#[cfg(test)]
mod patch_tests {
use super::*;
#[cfg(windows)]
#[test]
fn apply_multi_file_patch_refuses_to_follow_junction_outside_pkg() {
let outside = tempfile::tempdir().unwrap();
let pkg_root = tempfile::tempdir().unwrap();
let pkg = pkg_root.path().join("pkg");
std::fs::create_dir_all(&pkg).unwrap();
let escape = pkg.join("escape");
junction::create(outside.path(), &escape).unwrap();
let target = outside.path().join("victim.txt");
std::fs::write(&target, "untouched\n").unwrap();
let patch = "diff --git a/escape/victim.txt b/escape/victim.txt\n\
--- a/escape/victim.txt\n\
+++ b/escape/victim.txt\n\
@@ -1 +1 @@\n\
-untouched\n\
+PWNED\n";
let result = apply_multi_file_patch(&pkg, patch);
assert!(result.is_err(), "patch must refuse junction-bearing rel");
let after = std::fs::read_to_string(&target).unwrap();
assert_eq!(after, "untouched\n");
}
#[cfg(unix)]
#[test]
fn apply_multi_file_patch_refuses_to_follow_symlink_outside_pkg() {
let outside = tempfile::tempdir().unwrap();
let pkg_root = tempfile::tempdir().unwrap();
let pkg = pkg_root.path().join("pkg");
std::fs::create_dir_all(&pkg).unwrap();
let escape = pkg.join("escape");
std::os::unix::fs::symlink(outside.path(), &escape).unwrap();
let target = outside.path().join("victim.txt");
std::fs::write(&target, "untouched\n").unwrap();
let patch = "diff --git a/escape/victim.txt b/escape/victim.txt\n\
--- a/escape/victim.txt\n\
+++ b/escape/victim.txt\n\
@@ -1 +1 @@\n\
-untouched\n\
+PWNED\n";
let result = apply_multi_file_patch(&pkg, patch);
assert!(result.is_err(), "patch must refuse symlink-bearing rel");
let after = std::fs::read_to_string(&target).unwrap();
assert_eq!(after, "untouched\n");
}
#[test]
fn round_trips_simple_patch() {
let dir = tempfile::tempdir().unwrap();
let pkg = dir.path().join("pkg");
std::fs::create_dir_all(&pkg).unwrap();
std::fs::write(pkg.join("index.js"), "module.exports = 'old';\n").unwrap();
let patch = "diff --git a/index.js b/index.js\n\
--- a/index.js\n\
+++ b/index.js\n\
@@ -1 +1 @@\n\
-module.exports = 'old';\n\
+module.exports = 'new';\n";
apply_multi_file_patch(&pkg, patch).unwrap();
assert_eq!(
std::fs::read_to_string(pkg.join("index.js")).unwrap(),
"module.exports = 'new';\n"
);
}
#[test]
fn crlf_patch_path_does_not_carry_carriage_return() {
let patch = "diff --git a/index.js b/index.js\r\n\
--- a/index.js\r\n\
+++ b/index.js\r\n\
@@ -1 +1 @@\r\n\
-module.exports = 'old';\r\n\
+module.exports = 'new';\r\n";
let sections = split_patch_sections(patch);
assert_eq!(sections.len(), 1);
assert_eq!(sections[0].rel_path.as_deref(), Some("index.js"));
}
#[test]
fn crlf_deletion_patch_recognized() {
let patch = "diff --git a/removed.js b/removed.js\r\n\
deleted file mode 100644\r\n\
--- a/removed.js\r\n\
+++ /dev/null\r\n\
@@ -1 +0,0 @@\r\n\
-gone\r\n";
let sections = split_patch_sections(patch);
assert_eq!(sections.len(), 1);
assert!(sections[0].is_deletion);
}
#[test]
fn diff_git_path_with_space_b_substring() {
let patch = "diff --git a/a b/c.js b/a b/c.js\n\
--- a/a b/c.js\n\
+++ b/a b/c.js\n\
@@ -1 +1 @@\n\
-x\n\
+y\n";
let sections = split_patch_sections(patch);
assert_eq!(sections.len(), 1);
assert_eq!(sections[0].rel_path.as_deref(), Some("a b/c.js"));
}
#[test]
fn diff_git_quoted_path_form() {
let patch = "diff --git \"a/path with spaces.js\" \"b/path with spaces.js\"\n\
--- a/path with spaces.js\n\
+++ b/path with spaces.js\n\
@@ -1 +1 @@\n\
-x\n\
+y\n";
let sections = split_patch_sections(patch);
assert_eq!(sections.len(), 1);
assert_eq!(sections[0].rel_path.as_deref(), Some("path with spaces.js"));
}
#[test]
fn applies_lf_patch_against_crlf_file() {
let dir = tempfile::tempdir().unwrap();
let pkg = dir.path().join("pkg");
std::fs::create_dir_all(&pkg).unwrap();
std::fs::write(pkg.join("a.txt"), b"one\r\ntwo\r\nthree\r\n").unwrap();
let patch = "diff --git a/a.txt b/a.txt\n\
--- a/a.txt\n\
+++ b/a.txt\n\
@@ -1,3 +1,3 @@\n\
\x20one\n\
-two\n\
+TWO\n\
\x20three\n";
apply_multi_file_patch(&pkg, patch).unwrap();
let bytes = std::fs::read(pkg.join("a.txt")).unwrap();
assert_eq!(bytes, b"one\r\nTWO\r\nthree\r\n");
}
#[test]
fn crlf_restore_preserves_embedded_cr_byte() {
let dir = tempfile::tempdir().unwrap();
let pkg = dir.path().join("pkg");
std::fs::create_dir_all(&pkg).unwrap();
std::fs::write(pkg.join("a.txt"), b"one\r\ntwo\r\n").unwrap();
let patch = "diff --git a/a.txt b/a.txt\n\
--- a/a.txt\n\
+++ b/a.txt\n\
@@ -1,2 +1,2 @@\n\
-one\n\
+has\rcr\n\
\x20two\n";
apply_multi_file_patch(&pkg, patch).unwrap();
let bytes = std::fs::read(pkg.join("a.txt")).unwrap();
assert_eq!(bytes, b"has\rcr\r\ntwo\r\n");
}
#[test]
fn diff_git_quoted_path_unescapes_git_escapes() {
let path = parse_diff_git_b_path(r#""a/foo\".js" "b/foo\".js""#).expect("quoted parse");
assert_eq!(path, "foo\".js");
let path = parse_diff_git_b_path(r#""a/back\\slash.js" "b/back\\slash.js""#)
.expect("backslash parse");
assert_eq!(path, "back\\slash.js");
let path = parse_diff_git_b_path("\"a/caf\\303\\251.js\" \"b/caf\\303\\251.js\"")
.expect("octal parse");
assert_eq!(path, "café.js");
}
}
#[cfg(test)]
mod tests {
use super::*;
use aube_lockfile::{DepType, DirectDep, LockedPackage, LockfileGraph};
use aube_store::Store;
fn setup_store_with_files(dir: &Path) -> (Store, BTreeMap<String, aube_store::PackageIndex>) {
let store = Store::at(dir.join("store/files"));
let mut indices = BTreeMap::new();
let foo_stored = store
.import_bytes(b"module.exports = 'foo';", false)
.unwrap();
let mut foo_index = BTreeMap::new();
foo_index.insert("index.js".to_string(), foo_stored);
let foo_pkg = store
.import_bytes(b"{\"name\":\"foo\",\"version\":\"1.0.0\"}", false)
.unwrap();
foo_index.insert("package.json".to_string(), foo_pkg);
indices.insert("foo@1.0.0".to_string(), foo_index);
let bar_stored = store
.import_bytes(b"module.exports = 'bar';", false)
.unwrap();
let mut bar_index = BTreeMap::new();
bar_index.insert("index.js".to_string(), bar_stored);
indices.insert("bar@2.0.0".to_string(), bar_index);
(store, indices)
}
fn make_graph() -> LockfileGraph {
let mut packages = BTreeMap::new();
let mut foo_deps = BTreeMap::new();
foo_deps.insert("bar".to_string(), "2.0.0".to_string());
packages.insert(
"foo@1.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "1.0.0".to_string(),
integrity: None,
dependencies: foo_deps,
dep_path: "foo@1.0.0".to_string(),
..Default::default()
},
);
packages.insert(
"bar@2.0.0".to_string(),
LockedPackage {
name: "bar".to_string(),
version: "2.0.0".to_string(),
integrity: None,
dependencies: BTreeMap::new(),
dep_path: "bar@2.0.0".to_string(),
..Default::default()
},
);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "foo".to_string(),
dep_path: "foo@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
LockfileGraph {
importers,
packages,
..Default::default()
}
}
#[test]
fn test_detect_strategy() {
let dir = tempfile::tempdir().unwrap();
let strategy = Linker::detect_strategy(dir.path());
match strategy {
LinkStrategy::Reflink | LinkStrategy::Hardlink | LinkStrategy::Copy => {}
}
}
#[test]
fn test_link_all_handles_self_referential_dep_at_different_version() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let store = Store::at(dir.path().join("store/files"));
let mut indices = BTreeMap::new();
let host_index_js = store.import_bytes(b"/* react_ujs 3.3.0 */", false).unwrap();
let host_pkg_json = store
.import_bytes(b"{\"name\":\"react_ujs\",\"version\":\"3.3.0\"}", false)
.unwrap();
let mut host_index = BTreeMap::new();
host_index.insert("index.js".to_string(), host_index_js);
host_index.insert("package.json".to_string(), host_pkg_json);
indices.insert("react_ujs@3.3.0".to_string(), host_index);
let mut host_deps = BTreeMap::new();
host_deps.insert("react_ujs".to_string(), "^2.7.1".to_string());
let mut packages = BTreeMap::new();
packages.insert(
"react_ujs@3.3.0".to_string(),
LockedPackage {
name: "react_ujs".to_string(),
version: "3.3.0".to_string(),
integrity: None,
dependencies: host_deps,
dep_path: "react_ujs@3.3.0".to_string(),
..Default::default()
},
);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "react_ujs".to_string(),
dep_path: "react_ujs@3.3.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let stats = linker
.link_all(&project_dir, &graph, &indices)
.expect("install must succeed despite self-named dep");
assert_eq!(stats.packages_linked, 1);
let host_index =
project_dir.join("node_modules/.aube/react_ujs@3.3.0/node_modules/react_ujs/index.js");
assert!(host_index.exists(), "host package files must be present");
}
#[test]
fn test_link_all_creates_pnpm_virtual_store() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let graph = make_graph();
let stats = linker.link_all(&project_dir, &graph, &indices).unwrap();
assert!(project_dir.join("node_modules/.aube").exists());
let aube_foo = project_dir.join("node_modules/.aube/foo@1.0.0");
assert!(aube_foo.symlink_metadata().unwrap().is_symlink());
let foo_in_pnpm =
project_dir.join("node_modules/.aube/foo@1.0.0/node_modules/foo/index.js");
assert!(foo_in_pnpm.exists());
assert_eq!(
std::fs::read_to_string(&foo_in_pnpm).unwrap(),
"module.exports = 'foo';"
);
let bar_in_pnpm =
project_dir.join("node_modules/.aube/bar@2.0.0/node_modules/bar/index.js");
assert!(bar_in_pnpm.exists());
assert_eq!(stats.packages_linked, 2);
assert!(stats.files_linked >= 3); }
#[test]
fn test_link_file_fresh_reports_missing_cas_shard_and_invalidates_cache() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let foo_index = indices.get("foo@1.0.0").unwrap();
store.save_index("foo", "1.0.0", None, foo_index).unwrap();
let cached_path = store.index_dir().join("foo@1.0.0.json");
assert!(
cached_path.exists(),
"test setup: index cache must be written"
);
let pkgjson_store_path = foo_index.get("package.json").unwrap().store_path.clone();
std::fs::remove_file(&pkgjson_store_path).unwrap();
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let graph = make_graph();
let err = linker
.link_all(&project_dir, &graph, &indices)
.expect_err("link must fail when a referenced CAS shard is gone");
assert!(
matches!(&err, Error::MissingStoreFile { rel_path, .. } if rel_path == "package.json"),
"expected MissingStoreFile {{ rel_path: \"package.json\" }}, got {err:?}"
);
assert!(
!cached_path.exists(),
"stale index cache must be invalidated on MissingStoreFile"
);
}
#[test]
#[cfg(unix)]
fn test_link_file_fresh_hardlink_short_circuits_when_source_missing() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("store/files"));
let stored = store.import_bytes(b"hello", false).unwrap();
let store_path = stored.store_path.clone();
std::fs::remove_file(&store_path).unwrap();
let dst_dir = dir.path().join("dst");
std::fs::create_dir_all(&dst_dir).unwrap();
let dst = dst_dir.join("hello.txt");
let linker = Linker::new_with_gvs(&store, LinkStrategy::Hardlink, true);
let err = linker
.link_file_fresh(&stored, "hello.txt", &dst)
.expect_err("source missing must fail");
assert!(
matches!(
&err,
Error::MissingStoreFile { store_path: p, rel_path } if p == &store_path && rel_path == "hello.txt"
),
"expected MissingStoreFile from Hardlink branch, got {err:?}"
);
}
#[test]
fn test_link_all_creates_top_level_entries() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let linker = Linker::new(&store, LinkStrategy::Copy);
let graph = make_graph();
let stats = linker.link_all(&project_dir, &graph, &indices).unwrap();
let foo_top = project_dir.join("node_modules/foo/index.js");
assert!(foo_top.exists());
assert_eq!(
std::fs::read_to_string(&foo_top).unwrap(),
"module.exports = 'foo';"
);
let bar_top = project_dir.join("node_modules/bar/index.js");
assert!(!bar_top.exists());
assert_eq!(stats.top_level_linked, 1);
}
#[test]
fn test_link_all_transitive_symlinks() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let linker = Linker::new(&store, LinkStrategy::Copy);
let graph = make_graph();
linker.link_all(&project_dir, &graph, &indices).unwrap();
let bar_symlink = project_dir.join("node_modules/.aube/foo@1.0.0/node_modules/bar");
assert!(bar_symlink.symlink_metadata().unwrap().is_symlink());
}
#[test]
fn test_link_all_cleans_existing_node_modules() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
let nm = project_dir.join("node_modules");
std::fs::create_dir_all(&nm).unwrap();
std::fs::write(nm.join("stale-file.txt"), "old").unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let linker = Linker::new(&store, LinkStrategy::Copy);
let graph = make_graph();
linker.link_all(&project_dir, &graph, &indices).unwrap();
assert!(!nm.join("stale-file.txt").exists());
assert!(nm.join(".aube").exists());
}
#[test]
fn test_link_all_nested_node_modules_for_direct_deps() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let linker = Linker::new(&store, LinkStrategy::Copy);
let graph = make_graph();
linker.link_all(&project_dir, &graph, &indices).unwrap();
let foo_link = project_dir.join("node_modules/foo");
assert!(foo_link.symlink_metadata().unwrap().is_symlink());
let bar_sibling = project_dir.join("node_modules/.aube/foo@1.0.0/node_modules/bar");
assert!(bar_sibling.symlink_metadata().unwrap().is_symlink());
}
#[test]
fn test_global_virtual_store_is_populated() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let virtual_store = store.virtual_store_dir();
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let graph = make_graph();
linker.link_all(&project_dir, &graph, &indices).unwrap();
let foo_global = virtual_store.join("foo@1.0.0/node_modules/foo/index.js");
assert!(foo_global.exists());
assert_eq!(
std::fs::read_to_string(&foo_global).unwrap(),
"module.exports = 'foo';"
);
let bar_global = virtual_store.join("bar@2.0.0/node_modules/bar/index.js");
assert!(bar_global.exists());
}
#[test]
fn test_global_virtual_store_gets_hidden_hoist() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let virtual_store = store.virtual_store_dir();
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let mut graph = make_graph();
graph
.packages
.get_mut("foo@1.0.0")
.unwrap()
.dependencies
.clear();
linker.link_all(&project_dir, &graph, &indices).unwrap();
let project_hidden = project_dir.join("node_modules/.aube/node_modules/bar");
assert!(project_hidden.symlink_metadata().unwrap().is_symlink());
let global_hidden = virtual_store.join("node_modules/bar");
assert!(global_hidden.symlink_metadata().unwrap().is_symlink());
let from_real_store = virtual_store.join("foo@1.0.0/node_modules/bar/index.js");
assert!(
!from_real_store.exists(),
"bar is not a declared sibling of foo in this fixture"
);
let fallback = virtual_store.join("node_modules/bar/index.js");
assert_eq!(
std::fs::read_to_string(fallback).unwrap(),
"module.exports = 'bar';"
);
}
#[test]
fn test_global_virtual_store_hidden_hoist_prunes_only_dead_entries() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let virtual_store = store.virtual_store_dir();
let hidden = virtual_store.join("node_modules");
std::fs::create_dir_all(&hidden).unwrap();
let dotfile = hidden.join(".sentinel");
std::fs::write(&dotfile, "shared").unwrap();
let stale = hidden.join("stale");
std::fs::write(&stale, "old").unwrap();
let stale_scope = hidden.join("@stale-scope");
std::fs::write(&stale_scope, "old").unwrap();
let external_target = virtual_store.join("external@1.0.0/node_modules/external");
std::fs::create_dir_all(&external_target).unwrap();
let external_link = hidden.join("external");
sys::create_dir_link(
&pathdiff::diff_paths(&external_target, &hidden).unwrap(),
&external_link,
)
.unwrap();
let dead_link = hidden.join("dead");
sys::create_dir_link(
Path::new("../missing@1.0.0/node_modules/missing"),
&dead_link,
)
.unwrap();
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
linker
.link_all(&project_dir, &make_graph(), &indices)
.unwrap();
assert_eq!(std::fs::read_to_string(dotfile).unwrap(), "shared");
assert!(!stale.exists());
assert!(stale_scope.symlink_metadata().is_err());
assert!(external_link.symlink_metadata().unwrap().is_symlink());
assert!(dead_link.symlink_metadata().is_err());
assert!(hidden.join("bar").symlink_metadata().unwrap().is_symlink());
}
#[test]
fn test_global_virtual_store_hidden_hoist_disabled_keeps_live_shared_links() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let virtual_store = store.virtual_store_dir();
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
linker
.link_all(&project_dir, &make_graph(), &indices)
.unwrap();
let global_hidden = virtual_store.join("node_modules/bar");
assert!(global_hidden.symlink_metadata().unwrap().is_symlink());
Linker::new_with_gvs(&store, LinkStrategy::Copy, true)
.with_hoist(false)
.link_all(&project_dir, &make_graph(), &indices)
.unwrap();
assert!(global_hidden.symlink_metadata().unwrap().is_symlink());
}
#[test]
fn test_second_install_reuses_global_store() {
let dir = tempfile::tempdir().unwrap();
let (store, indices) = setup_store_with_files(dir.path());
let linker = Linker::new_with_gvs(&store, LinkStrategy::Copy, true);
let graph = make_graph();
let project1 = dir.path().join("project1");
std::fs::create_dir_all(&project1).unwrap();
let stats1 = linker.link_all(&project1, &graph, &indices).unwrap();
assert_eq!(stats1.packages_linked, 2);
assert_eq!(stats1.packages_cached, 0);
let project2 = dir.path().join("project2");
std::fs::create_dir_all(&project2).unwrap();
let stats2 = linker.link_all(&project2, &graph, &indices).unwrap();
assert_eq!(stats2.packages_linked, 0);
assert_eq!(stats2.packages_cached, 2);
assert_eq!(stats2.files_linked, 0);
let foo1 = project1.join("node_modules/foo/index.js");
let foo2 = project2.join("node_modules/foo/index.js");
assert!(foo1.exists());
assert!(foo2.exists());
assert_eq!(
std::fs::read_to_string(&foo1).unwrap(),
std::fs::read_to_string(&foo2).unwrap()
);
}
#[test]
fn test_link_all_repoints_symlink_after_version_bump() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let store = Store::at(dir.path().join("store/files"));
let mut indices_v1 = BTreeMap::new();
let foo_v1 = store
.import_bytes(b"module.exports = 'foo@1';", false)
.unwrap();
let mut foo_v1_index = BTreeMap::new();
foo_v1_index.insert("index.js".to_string(), foo_v1);
indices_v1.insert("foo@1.0.0".to_string(), foo_v1_index);
let mut graph_v1 = LockfileGraph::default();
graph_v1.packages.insert(
"foo@1.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "1.0.0".to_string(),
dep_path: "foo@1.0.0".to_string(),
..Default::default()
},
);
graph_v1.importers.insert(
".".to_string(),
vec![DirectDep {
name: "foo".to_string(),
dep_path: "foo@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
let linker = Linker::new(&store, LinkStrategy::Copy);
linker
.link_all(&project_dir, &graph_v1, &indices_v1)
.unwrap();
let foo_link = project_dir.join("node_modules/foo");
assert!(foo_link.symlink_metadata().unwrap().is_symlink());
assert_eq!(
std::fs::read_to_string(foo_link.join("index.js")).unwrap(),
"module.exports = 'foo@1';"
);
let mut indices_v2 = BTreeMap::new();
let foo_v2 = store
.import_bytes(b"module.exports = 'foo@2';", false)
.unwrap();
let mut foo_v2_index = BTreeMap::new();
foo_v2_index.insert("index.js".to_string(), foo_v2);
indices_v2.insert("foo@2.0.0".to_string(), foo_v2_index);
let mut graph_v2 = LockfileGraph::default();
graph_v2.packages.insert(
"foo@2.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "2.0.0".to_string(),
dep_path: "foo@2.0.0".to_string(),
..Default::default()
},
);
graph_v2.importers.insert(
".".to_string(),
vec![DirectDep {
name: "foo".to_string(),
dep_path: "foo@2.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
linker
.link_all(&project_dir, &graph_v2, &indices_v2)
.unwrap();
assert_eq!(
std::fs::read_to_string(project_dir.join("node_modules/foo/index.js")).unwrap(),
"module.exports = 'foo@2';"
);
}
#[test]
fn test_shamefully_hoist_repoints_after_transitive_version_bump() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
let store = Store::at(dir.path().join("store/files"));
let foo_v1 = store
.import_bytes(b"module.exports = 'foo@1';", false)
.unwrap();
let mut foo_v1_idx = BTreeMap::new();
foo_v1_idx.insert("index.js".to_string(), foo_v1);
let bar_v1 = store
.import_bytes(b"module.exports = 'bar@1';", false)
.unwrap();
let mut bar_v1_idx = BTreeMap::new();
bar_v1_idx.insert("index.js".to_string(), bar_v1);
let mut indices_v1 = BTreeMap::new();
indices_v1.insert("foo@1.0.0".to_string(), foo_v1_idx);
indices_v1.insert("bar@1.0.0".to_string(), bar_v1_idx);
let mut graph_v1 = LockfileGraph::default();
let mut bar_deps_v1 = BTreeMap::new();
bar_deps_v1.insert("foo".to_string(), "1.0.0".to_string());
graph_v1.packages.insert(
"bar@1.0.0".to_string(),
LockedPackage {
name: "bar".to_string(),
version: "1.0.0".to_string(),
dep_path: "bar@1.0.0".to_string(),
dependencies: bar_deps_v1,
..Default::default()
},
);
graph_v1.packages.insert(
"foo@1.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "1.0.0".to_string(),
dep_path: "foo@1.0.0".to_string(),
..Default::default()
},
);
graph_v1.importers.insert(
".".to_string(),
vec![DirectDep {
name: "bar".to_string(),
dep_path: "bar@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
let linker = Linker::new(&store, LinkStrategy::Copy).with_shamefully_hoist(true);
linker
.link_all(&project_dir, &graph_v1, &indices_v1)
.unwrap();
assert_eq!(
std::fs::read_to_string(project_dir.join("node_modules/foo/index.js")).unwrap(),
"module.exports = 'foo@1';",
"install 1 should hoist foo@1.0.0"
);
let foo_v2 = store
.import_bytes(b"module.exports = 'foo@2';", false)
.unwrap();
let mut foo_v2_idx = BTreeMap::new();
foo_v2_idx.insert("index.js".to_string(), foo_v2);
let mut indices_v2 = BTreeMap::new();
let bar_v1_for_v2 = store
.import_bytes(b"module.exports = 'bar@1';", false)
.unwrap();
let mut bar_v1_idx_v2 = BTreeMap::new();
bar_v1_idx_v2.insert("index.js".to_string(), bar_v1_for_v2);
indices_v2.insert("bar@1.0.0".to_string(), bar_v1_idx_v2);
indices_v2.insert("foo@2.0.0".to_string(), foo_v2_idx);
let mut graph_v2 = LockfileGraph::default();
let mut bar_deps_v2 = BTreeMap::new();
bar_deps_v2.insert("foo".to_string(), "2.0.0".to_string());
graph_v2.packages.insert(
"bar@1.0.0".to_string(),
LockedPackage {
name: "bar".to_string(),
version: "1.0.0".to_string(),
dep_path: "bar@1.0.0".to_string(),
dependencies: bar_deps_v2,
..Default::default()
},
);
graph_v2.packages.insert(
"foo@2.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "2.0.0".to_string(),
dep_path: "foo@2.0.0".to_string(),
..Default::default()
},
);
graph_v2.importers.insert(
".".to_string(),
vec![DirectDep {
name: "bar".to_string(),
dep_path: "bar@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
linker
.link_all(&project_dir, &graph_v2, &indices_v2)
.unwrap();
assert_eq!(
std::fs::read_to_string(project_dir.join("node_modules/foo/index.js")).unwrap(),
"module.exports = 'foo@2';",
"install 2 should repoint the hoisted symlink to foo@2.0.0"
);
}
#[test]
fn validate_index_key_accepts_normal_keys() {
validate_index_key("index.js").unwrap();
validate_index_key("lib/sub/a.js").unwrap();
validate_index_key("package.json").unwrap();
validate_index_key("a/b/c/d/e/f.js").unwrap();
}
#[cfg(not(windows))]
#[test]
fn validate_index_key_accepts_posix_colon_filename() {
validate_index_key("dist/__mocks__/package-json:version.d.ts").unwrap();
}
#[test]
fn validate_index_key_rejects_empty() {
assert!(matches!(
validate_index_key(""),
Err(Error::UnsafeIndexKey(_))
));
}
#[test]
fn validate_index_key_rejects_leading_slash() {
assert!(matches!(
validate_index_key("/etc/passwd"),
Err(Error::UnsafeIndexKey(_))
));
assert!(matches!(
validate_index_key("\\evil"),
Err(Error::UnsafeIndexKey(_))
));
}
#[test]
fn validate_index_key_rejects_parent_dir() {
assert!(matches!(
validate_index_key("../../etc/passwd"),
Err(Error::UnsafeIndexKey(_))
));
assert!(matches!(
validate_index_key("lib/../../../etc"),
Err(Error::UnsafeIndexKey(_))
));
}
#[test]
fn validate_index_key_rejects_nul_and_backslash() {
assert!(matches!(
validate_index_key("lib\0evil"),
Err(Error::UnsafeIndexKey(_))
));
assert!(matches!(
validate_index_key("lib\\..\\etc"),
Err(Error::UnsafeIndexKey(_))
));
}
#[cfg(windows)]
#[test]
fn validate_index_key_rejects_windows_drive() {
assert!(matches!(
validate_index_key("C:Windows"),
Err(Error::UnsafeIndexKey(_))
));
}
}