use crate::{Error, ResolveTask};
use aube_lockfile::{LocalSource, LockedPackage};
use aube_registry::client::RegistryClient;
use aube_util::path::normalize_lexical;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub(crate) fn rebase_local(
local: &LocalSource,
importer_root: &Path,
project_root: &Path,
) -> LocalSource {
if importer_root == project_root {
if let LocalSource::Exec(path) = local {
return LocalSource::Exec(normalize_lexical(path));
}
return local.clone();
}
let Some(local_path) = local.path() else {
return local.clone();
};
let abs = normalize_lexical(&importer_root.join(local_path));
let rebased = pathdiff::diff_paths(&abs, project_root).map_or(abs, |p| normalize_lexical(&p));
match local {
LocalSource::Directory(_) => LocalSource::Directory(rebased),
LocalSource::Tarball(_) => LocalSource::Tarball(rebased),
LocalSource::Link(_) => LocalSource::Link(rebased),
LocalSource::Portal(_) => LocalSource::Portal(rebased),
LocalSource::Exec(_) => LocalSource::Exec(rebased),
LocalSource::Git(_) | LocalSource::RemoteTarball(_) => local.clone(),
}
}
pub fn resolve_exec_script_path(
local: &LocalSource,
project_root: &Path,
) -> Result<PathBuf, String> {
let LocalSource::Exec(rel) = local else {
return Err("resolve_exec_script_path called on non-exec source".to_string());
};
let script = project_root.join(rel);
if !script.is_file() {
return Err(format!("{} is not a file", script.display()));
}
let canonical_root = project_root
.canonicalize()
.map_err(|e| format!("canonicalize project root {}: {e}", project_root.display()))?;
let canonical_script = script
.canonicalize()
.map_err(|e| format!("canonicalize exec script {}: {e}", script.display()))?;
if !canonical_script.starts_with(&canonical_root) {
return Err(format!(
"{} resolves outside project root {}",
script.display(),
canonical_root.display()
));
}
Ok(canonical_script)
}
const MAX_RESOLVE_TARBALL_DECOMPRESSED_BYTES: u64 = 64 * 1024 * 1024;
const MAX_RESOLVE_PACKAGE_JSON_BYTES: u64 = 8 * 1024 * 1024;
fn read_tarball_package_json(bytes: &[u8]) -> Result<Vec<u8>, String> {
use std::io::Read;
let gz = flate2::read::GzDecoder::new(bytes);
let capped = gz.take(MAX_RESOLVE_TARBALL_DECOMPRESSED_BYTES);
let mut archive = tar::Archive::new(capped);
for entry in archive.entries().map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let entry_path = entry.path().map_err(|e| e.to_string())?.to_path_buf();
if entry_path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n == "package.json")
&& entry_path.components().count() == 2
{
let mut buf = Vec::new();
entry
.take(MAX_RESOLVE_PACKAGE_JSON_BYTES + 1)
.read_to_end(&mut buf)
.map_err(|e| e.to_string())?;
if buf.len() as u64 > MAX_RESOLVE_PACKAGE_JSON_BYTES {
return Err("package.json exceeds 8 MiB cap".to_string());
}
return Ok(buf);
}
}
Err("tarball has no top-level package.json".to_string())
}
pub(crate) fn read_local_manifest(
local: &LocalSource,
importer_root: &Path,
) -> Result<(String, String, BTreeMap<String, String>), Error> {
let Some(local_path) = local.path() else {
return Err(Error::Registry(
local.specifier(),
"read_local_manifest called on non-path source".to_string(),
));
};
let path = importer_root.join(local_path);
let content = match local {
LocalSource::Directory(_) | LocalSource::Link(_) | LocalSource::Portal(_) => {
std::fs::read(path.join("package.json"))
.map_err(|e| Error::Registry(local.specifier(), e.to_string()))?
}
LocalSource::Tarball(_) => {
let bytes = std::fs::read(&path)
.map_err(|e| Error::Registry(local.specifier(), e.to_string()))?;
read_tarball_package_json(&bytes).map_err(|e| Error::Registry(local.specifier(), e))?
}
LocalSource::Exec(_) | LocalSource::Git(_) | LocalSource::RemoteTarball(_) => {
return Err(Error::Registry(
local.specifier(),
"read_local_manifest: generated or remote source handled separately".to_string(),
));
}
};
let pj: aube_manifest::PackageJson = sonic_rs::from_slice(&content)
.or_else(|_| serde_json::from_slice(&content))
.map_err(|e| Error::Registry(local.specifier(), e.to_string()))?;
Ok((
pj.name.unwrap_or_default(),
pj.version.unwrap_or_else(|| "0.0.0".to_string()),
pj.dependencies,
))
}
pub(crate) async fn resolve_exec_manifest(
name: &str,
local: &LocalSource,
project_root: &Path,
) -> Result<(String, BTreeMap<String, String>), Error> {
let LocalSource::Exec(_) = local else {
return Err(Error::Registry(
name.to_string(),
"resolve_exec_manifest called on non-exec source".to_string(),
));
};
let script = resolve_exec_script_path(local, project_root).map_err(|e| {
Error::Registry(
name.to_string(),
format!("exec dependency {}: {e}", local.specifier()),
)
})?;
let temp = tempfile::Builder::new()
.prefix("aube-exec-resolve-")
.tempdir()
.map_err(|e| Error::Registry(name.to_string(), e.to_string()))?;
let build_dir = temp.path().join("build");
let temp_dir = temp.path().join("temp");
std::fs::create_dir_all(&build_dir)
.map_err(|e| Error::Registry(name.to_string(), e.to_string()))?;
std::fs::create_dir_all(&temp_dir)
.map_err(|e| Error::Registry(name.to_string(), e.to_string()))?;
let env = serde_json::json!({
"tempDir": temp_dir,
"buildDir": build_dir,
"locator": format!("{name}@{}", local.specifier()),
});
let status = tokio::process::Command::new("node")
.arg("-e")
.arg(crate::YARN_EXEC_WRAPPER)
.arg(&script)
.env("AUBE_YARN_EXEC_ENV", env.to_string())
.current_dir(project_root)
.status()
.await
.map_err(|e| {
Error::Registry(
name.to_string(),
format!("execute {} with Node.js from PATH: {e}", local.specifier()),
)
})?;
if !status.success() {
return Err(Error::Registry(
name.to_string(),
format!(
"exec dependency {} failed with status {status}",
local.specifier()
),
));
}
let content = std::fs::read(build_dir.join("package.json")).map_err(|e| {
Error::Registry(
name.to_string(),
format!("read generated package.json for {}: {e}", local.specifier()),
)
})?;
let pj: aube_manifest::PackageJson = sonic_rs::from_slice(&content)
.or_else(|_| serde_json::from_slice(&content))
.map_err(|e| Error::Registry(name.to_string(), e.to_string()))?;
Ok((
pj.version.unwrap_or_else(|| "0.0.0".to_string()),
pj.dependencies,
))
}
pub(crate) fn dep_path_for(name: &str, version: &str) -> String {
format!("{name}@{version}")
}
pub(crate) fn is_non_registry_specifier(s: &str) -> bool {
if s.starts_with("link:") {
return true;
}
if s.starts_with("portal:") {
return true;
}
if s.starts_with("exec:") {
return true;
}
if aube_lockfile::parse_git_spec(s).is_some() {
return true;
}
if aube_lockfile::LocalSource::looks_like_remote_tarball_url(s) {
return true;
}
s.starts_with("file:")
}
pub(crate) fn should_block_exotic_subdep(
task: &ResolveTask,
resolved: &BTreeMap<String, LockedPackage>,
block_exotic_subdeps: bool,
) -> bool {
block_exotic_subdeps
&& !task.is_root
&& !task
.parent
.as_ref()
.and_then(|parent| resolved.get(parent))
.is_some_and(|pkg| {
matches!(
pkg.local_source,
Some(LocalSource::Directory(_))
| Some(LocalSource::Link(_))
| Some(LocalSource::Portal(_))
| Some(LocalSource::Exec(_))
)
})
}
pub(crate) async fn resolve_git_source(
name: &str,
git: &aube_lockfile::GitSource,
shallow: bool,
client: Option<&RegistryClient>,
) -> Result<
(
LocalSource,
String,
BTreeMap<String, String>,
Option<String>,
),
Error,
> {
let original_url = git.url.clone();
let committish = git.committish.clone();
let subpath = git.subpath.clone();
let hosted = aube_lockfile::parse_hosted_git(&original_url);
let runtime_url = hosted
.as_ref()
.map(|h| h.https_url())
.unwrap_or_else(|| original_url.clone());
let runtime_url_for_ref = runtime_url.clone();
let committish_for_ref = committish.clone();
let name_for_ref = name.to_string();
let resolved_sha = tokio::task::spawn_blocking(move || -> Result<String, Error> {
let seed = aube_store::git_resolve_ref(&runtime_url_for_ref, committish_for_ref.as_deref())
.map_err(|e| Error::Registry(name_for_ref.clone(), e.to_string()))?;
Ok(seed)
})
.await
.map_err(|e| {
Error::Registry(
name.to_string(),
format!("git ls-remote task panicked: {e}"),
)
})??;
let codeload_url = hosted.as_ref().and_then(|h| h.tarball_url(&resolved_sha));
if codeload_url.is_some()
&& git.integrity.is_some()
&& let Some((clone_dir, _head_sha)) = aube_store::codeload_cache_lookup(
&original_url,
&resolved_sha,
git.integrity.as_deref(),
)
{
let integrity = aube_store::codeload_cache_integrity(
&original_url,
&resolved_sha,
git.integrity.as_deref(),
);
let pkg_root = match &subpath {
Some(sub) => clone_dir.join(sub),
None => clone_dir.clone(),
};
let manifest_bytes = std::fs::read(pkg_root.join("package.json")).map_err(|e| {
let where_ = subpath
.as_deref()
.map(|s| format!(" at /{s}"))
.unwrap_or_default();
Error::Registry(
name.to_string(),
format!("read package.json in cached codeload extract{where_}: {e}"),
)
})?;
let pj: aube_manifest::PackageJson = serde_json::from_slice(&manifest_bytes)
.map_err(|e| Error::Registry(name.to_string(), e.to_string()))?;
let version = pj.version.unwrap_or_else(|| "0.0.0".to_string());
return Ok((
LocalSource::Git(aube_lockfile::GitSource {
url: original_url,
committish,
resolved: resolved_sha,
integrity: git.integrity.clone(),
subpath,
}),
version,
pj.dependencies,
integrity,
));
}
if let (Some(c), Some(url_to_fetch)) = (client, codeload_url.as_deref()) {
match c.fetch_tarball_bytes(url_to_fetch).await {
Ok(bytes) => {
let bytes_vec = bytes.to_vec();
if let Some(pinned) = &git.integrity {
aube_store::verify_integrity(&bytes_vec, pinned)
.map_err(|e| Error::Registry(name.to_string(), e.to_string()))?;
}
let integrity = git
.integrity
.clone()
.unwrap_or_else(|| aube_store::sha512_integrity(&bytes_vec));
let url_for_extract = original_url.clone();
let sha_for_extract = resolved_sha.clone();
let integrity_for_extract = integrity.clone();
let subpath_for_extract = subpath.clone();
let name_for_extract = name.to_string();
let extracted = tokio::task::spawn_blocking(move || -> Result<_, Error> {
let (clone_dir, resolved) = aube_store::extract_codeload_tarball(
&bytes_vec,
&url_for_extract,
&sha_for_extract,
Some(&integrity_for_extract),
)
.map_err(|e| Error::Registry(name_for_extract.clone(), e.to_string()))?;
let pkg_root = match &subpath_for_extract {
Some(sub) => clone_dir.join(sub),
None => clone_dir.clone(),
};
let manifest_bytes =
std::fs::read(pkg_root.join("package.json")).map_err(|e| {
let where_ = subpath_for_extract
.as_deref()
.map(|s| format!(" at /{s}"))
.unwrap_or_default();
Error::Registry(
name_for_extract.clone(),
format!("read package.json in codeload extract{where_}: {e}"),
)
})?;
let pj: aube_manifest::PackageJson = serde_json::from_slice(&manifest_bytes)
.map_err(|e| Error::Registry(name_for_extract.clone(), e.to_string()))?;
let version = pj.version.unwrap_or_else(|| "0.0.0".to_string());
Ok((resolved, version, pj.dependencies))
})
.await
.map_err(|e| {
Error::Registry(name.to_string(), format!("codeload extract panicked: {e}"))
})?;
let integrity = aube_store::sha512_integrity(&bytes);
match extracted {
Ok((resolved, version, deps)) => {
return Ok((
LocalSource::Git(aube_lockfile::GitSource {
url: original_url,
committish,
resolved,
integrity: Some(integrity.clone()),
subpath,
}),
version,
deps,
Some(integrity),
));
}
Err(e) => {
tracing::debug!(
name,
"codeload extract failed, falling back to git clone: {e}",
);
}
}
}
Err(e) => {
tracing::debug!(
name,
url = %aube_util::url::redact_url(url_to_fetch),
"codeload fetch failed, falling back to git clone: {e}",
);
}
}
}
let runtime_url_for_clone = runtime_url;
let original_url_for_lockfile = original_url.clone();
let resolved_sha_for_clone = resolved_sha.clone();
let subpath_for_clone = subpath.clone();
let name_for_clone = name.to_string();
let (local, version, deps) = tokio::task::spawn_blocking(move || -> Result<_, Error> {
let (clone_dir, resolved) =
aube_store::git_shallow_clone(&runtime_url_for_clone, &resolved_sha_for_clone, shallow)
.map_err(|e| Error::Registry(name_for_clone.clone(), e.to_string()))?;
let pkg_root = match &subpath_for_clone {
Some(sub) => clone_dir.join(sub),
None => clone_dir.clone(),
};
let manifest_bytes = std::fs::read(pkg_root.join("package.json")).map_err(|e| {
let where_ = subpath_for_clone
.as_deref()
.map(|s| format!(" at /{s}"))
.unwrap_or_default();
Error::Registry(
name_for_clone.clone(),
format!("read package.json in clone{where_}: {e}"),
)
})?;
let pj: aube_manifest::PackageJson = serde_json::from_slice(&manifest_bytes)
.map_err(|e| Error::Registry(name_for_clone.clone(), e.to_string()))?;
let version = pj.version.unwrap_or_else(|| "0.0.0".to_string());
Ok((
LocalSource::Git(aube_lockfile::GitSource {
url: original_url_for_lockfile,
committish,
resolved,
integrity: None,
subpath: subpath_for_clone,
}),
version,
pj.dependencies,
))
})
.await
.map_err(|e| Error::Registry(name.to_string(), format!("git task panicked: {e}")))??;
Ok((local, version, deps, None))
}
pub(crate) async fn resolve_remote_tarball(
name: &str,
tarball: &aube_lockfile::RemoteTarballSource,
client: &RegistryClient,
) -> Result<(LocalSource, String, BTreeMap<String, String>), Error> {
let bytes = client
.fetch_tarball_bytes(&tarball.url)
.await
.map_err(|e| {
Error::Registry(
name.to_string(),
format!("fetch {}: {e}", aube_util::url::redact_url(&tarball.url)),
)
})?;
let name_owned = name.to_string();
let url = aube_util::url::redact_url(&tarball.url);
let (integrity, version, deps) = tokio::task::spawn_blocking(move || -> Result<_, Error> {
let integrity = aube_store::sha512_integrity(&bytes);
let manifest_bytes = read_tarball_package_json(&bytes)
.map_err(|e| Error::Registry(name_owned.clone(), format!("tarball {url}: {e}")))?;
let pj: aube_manifest::PackageJson = serde_json::from_slice(&manifest_bytes)
.map_err(|e| Error::Registry(name_owned.clone(), e.to_string()))?;
let version = pj.version.unwrap_or_else(|| "0.0.0".to_string());
Ok((integrity, version, pj.dependencies))
})
.await
.map_err(|e| Error::Registry(name.to_string(), format!("tarball task panicked: {e}")))??;
Ok((
LocalSource::RemoteTarball(aube_lockfile::RemoteTarballSource {
url: tarball.url.clone(),
integrity,
git_hosted: tarball.git_hosted,
}),
version,
deps,
))
}
#[cfg(test)]
mod rebase_local_tests {
use super::*;
use std::path::{Path, PathBuf};
#[test]
fn workspace_file_climbs_out_of_importer_to_root_sibling() {
let local = LocalSource::Directory(PathBuf::from("../../vendor-dir"));
let rebased = rebase_local(&local, Path::new("packages/app"), Path::new(""));
match rebased {
LocalSource::Directory(p) => assert_eq!(p, PathBuf::from("vendor-dir")),
other => panic!("expected Directory, got {other:?}"),
}
}
#[test]
fn two_importers_referencing_same_target_collide_on_dep_path() {
let a = rebase_local(
&LocalSource::Directory(PathBuf::from("../../vendor-dir")),
Path::new("packages/app"),
Path::new(""),
);
let b = rebase_local(
&LocalSource::Directory(PathBuf::from("../vendor-dir")),
Path::new("packages"),
Path::new(""),
);
assert_eq!(a.dep_path("vendor-dir"), b.dep_path("vendor-dir"));
}
#[test]
fn root_and_transitive_exec_paths_collide_on_dep_path() {
let root = rebase_local(
&LocalSource::Exec(PathBuf::from("./scripts/generate-exec.js")),
Path::new(""),
Path::new(""),
);
let transitive = rebase_local(
&LocalSource::Exec(PathBuf::from("../../scripts/generate-exec.js")),
Path::new("packages/portal"),
Path::new(""),
);
assert_eq!(root.dep_path("exec-pkg"), transitive.dep_path("exec-pkg"));
}
#[test]
fn normalize_preserves_unresolvable_leading_parent() {
assert_eq!(
normalize_lexical(Path::new("../vendor")),
PathBuf::from("../vendor")
);
}
#[test]
fn dep_path_and_specifier_use_posix_separators() {
let win = LocalSource::Directory(PathBuf::from("vendor\\nested\\dir"));
let unix = LocalSource::Directory(PathBuf::from("vendor/nested/dir"));
assert_eq!(win.dep_path("foo"), unix.dep_path("foo"));
assert_eq!(win.specifier(), "file:vendor/nested/dir");
assert_eq!(unix.specifier(), "file:vendor/nested/dir");
}
#[test]
fn exec_script_must_stay_inside_project_root() {
let temp = tempfile::tempdir().unwrap();
let project_root = temp.path().join("project");
let outside = temp.path().join("outside.js");
std::fs::create_dir(&project_root).unwrap();
std::fs::write(&outside, "").unwrap();
let local = LocalSource::Exec(PathBuf::from("../outside.js"));
let err = resolve_exec_script_path(&local, &project_root).unwrap_err();
assert!(err.contains("resolves outside project root"), "{err}");
}
#[test]
fn exec_script_inside_project_root_is_allowed() {
let temp = tempfile::tempdir().unwrap();
let project_root = temp.path().join("project");
let script_dir = project_root.join("scripts");
let script = script_dir.join("generate.js");
std::fs::create_dir_all(&script_dir).unwrap();
std::fs::write(&script, "").unwrap();
let local = LocalSource::Exec(PathBuf::from("scripts/generate.js"));
let resolved = resolve_exec_script_path(&local, &project_root).unwrap();
assert_eq!(resolved, script.canonicalize().unwrap());
}
}
#[cfg(test)]
mod cve_audit_tarball_bomb {
use super::*;
use std::io::Write;
fn build_zero_tarball(uncompressed_size: usize) -> Vec<u8> {
let mut tar_buf: Vec<u8> = Vec::new();
{
let mut builder = tar::Builder::new(&mut tar_buf);
let payload = vec![0u8; uncompressed_size];
let mut header = tar::Header::new_gnu();
header.set_path("pkg/package.json").unwrap();
header.set_size(payload.len() as u64);
header.set_mode(0o644);
header.set_cksum();
builder.append(&header, &payload[..]).unwrap();
builder.finish().unwrap();
}
let mut gz = Vec::new();
{
let mut enc = flate2::write::GzEncoder::new(&mut gz, flate2::Compression::best());
enc.write_all(&tar_buf).unwrap();
enc.finish().unwrap();
}
gz
}
fn build_dummy_then_package_json(dummy_size: usize) -> Vec<u8> {
let mut tar_buf: Vec<u8> = Vec::new();
{
let mut builder = tar::Builder::new(&mut tar_buf);
let dummy = vec![0u8; dummy_size];
let mut h1 = tar::Header::new_gnu();
h1.set_path("pkg/dummy.bin").unwrap();
h1.set_size(dummy.len() as u64);
h1.set_mode(0o644);
h1.set_cksum();
builder.append(&h1, &dummy[..]).unwrap();
let manifest = b"{\"name\":\"x\",\"version\":\"0.0.1\"}";
let mut h2 = tar::Header::new_gnu();
h2.set_path("pkg/package.json").unwrap();
h2.set_size(manifest.len() as u64);
h2.set_mode(0o644);
h2.set_cksum();
builder.append(&h2, &manifest[..]).unwrap();
builder.finish().unwrap();
}
let mut gz = Vec::new();
{
let mut enc = flate2::write::GzEncoder::new(&mut gz, flate2::Compression::best());
enc.write_all(&tar_buf).unwrap();
enc.finish().unwrap();
}
gz
}
#[test]
fn read_tarball_package_json_rejects_decompression_bomb() {
let bomb = build_zero_tarball(200 * 1024 * 1024);
assert!(
bomb.len() < 400 * 1024,
"compressed bomb too large to call this an amplification: {}",
bomb.len()
);
let result = read_tarball_package_json(&bomb);
assert!(
result.is_err(),
"200 MiB decompressed payload must be rejected by the cap, got {:?}",
result.as_ref().map(|b| b.len())
);
}
#[test]
fn read_tarball_package_json_rejects_dummy_entry_amplification() {
let bomb = build_dummy_then_package_json(200 * 1024 * 1024);
assert!(
bomb.len() < 400 * 1024,
"compressed multi-entry bomb too large: {}",
bomb.len()
);
let result = read_tarball_package_json(&bomb);
assert!(
result.is_err(),
"decompressed dummy entry preceding package.json must hit the output cap"
);
}
}