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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/install/PackageInstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2248,6 +2248,11 @@ impl<'a> PackageInstall<'a> {
package_id: PackageID,
resolution_tag: resolution::Tag,
) -> bool {
// No entry can be named yet (a local tarball whose integrity the
// lockfile does not record); extracting it is what names one.
if self.cache_dir_subpath.is_empty() {
return true;
}
let state = manager.get_preinstall_state(package_id);
match state {
crate::PreinstallState::Done => false,
Expand Down
8 changes: 4 additions & 4 deletions src/install/PackageInstaller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,8 @@ impl<'a> PackageInstaller<'a> {

// If a newly computed integrity hash is available (e.g. for a GitHub
// tarball) and the lockfile doesn't already have one, persist it so
// the lockfile gets re-saved with the hash.
// the lockfile gets re-saved with the hash. Must happen before the
// callbacks below: a local tarball's cache entry is named after it.
if data.integrity.tag.is_supported() {
let pkg_metas = self.lockfile_mut().packages.items_meta_mut();
if !pkg_metas[package_id as usize].integrity.tag.is_supported() {
Expand Down Expand Up @@ -1523,9 +1524,8 @@ impl<'a> PackageInstaller<'a> {
}
}
resolution::Tag::LocalTarball => {
installer.cache_dir_subpath = package_manager::cached_tarball_folder_name(
self.manager_mut(),
*resolution.local_tarball(),
installer.cache_dir_subpath = package_manager::cached_local_tarball_folder_name(
&self.metas[package_id as usize].integrity,
patch_contents_hash,
);
installer.cache_dir = package_manager::get_cache_directory(self.manager_mut());
Expand Down
3 changes: 2 additions & 1 deletion src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ use directories::attempt_to_create_package_json_and_open;
pub use directories::{
attempt_to_create_package_json, cached_git_folder_name, cached_git_folder_name_print,
cached_git_folder_name_print_auto, cached_github_folder_name, cached_github_folder_name_print,
cached_github_folder_name_print_auto, cached_npm_package_folder_name,
cached_github_folder_name_print_auto, cached_local_tarball_folder_name,
cached_local_tarball_folder_name_print, cached_npm_package_folder_name,
cached_npm_package_folder_name_print, cached_npm_package_folder_print_basename,
cached_tarball_folder_name, cached_tarball_folder_name_print, compute_cache_dir_and_subpath,
fetch_cache_directory_path, get_cache_directory, get_cache_directory_and_abs_path,
Expand Down
66 changes: 63 additions & 3 deletions src/install/PackageManager/PackageManagerDirectories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use bun_core::{Global, Output, ZBox, env_var, fmt as bun_fmt};
use bun_dotenv::Loader as DotEnvLoader;
use bun_install::lockfile::{Format as LockfileFormat, LoadResult, Lockfile};
use bun_install::resolution::Tag as ResolutionTag;
use bun_install::{PackageID, Resolution};
use bun_install::{Integrity, PackageID, Resolution};
use bun_paths::{self as path, AbsPath, PathBuffer, SEP};
use bun_semver::{self as Semver, String as SemverString};
#[cfg(windows)]
Expand Down Expand Up @@ -487,6 +487,14 @@ impl<'a> ByteCursor<'a> {
self.put(bun_fmt::u64_hex_var_lower(&mut tmp, n));
}

/// Two lower-hex digits per byte.
#[inline(always)]
fn put_hex_bytes(&mut self, bytes: &[u8]) {
let end = self.at + bytes.len() * 2;
bun_fmt::bytes_to_hex_lower(bytes, &mut self.buf[self.at..end]);
self.at = end;
}

/// `@@@{d}` when set.
#[inline(always)]
fn put_cache_version(&mut self, v: Option<usize>) {
Expand Down Expand Up @@ -725,6 +733,8 @@ pub fn cached_npm_package_folder_print_basename<'a>(
w.finish_z()
}

/// `@T@<hash of the URL>@@@1`, for URL tarballs. `file:` tarballs use
/// `cached_local_tarball_folder_name_print`.
pub fn cached_tarball_folder_name_print<'a>(
buf: &'a mut [u8],
url: &[u8],
Expand All @@ -750,6 +760,43 @@ pub fn cached_tarball_folder_name(
)
}

/// `@T@sha512-<first 16 digest bytes as hex>@@@1`.
///
/// A `file:` tarball's resolution is the path as written in package.json
/// (`pkg.tgz`), which names a different tarball in every project sharing the
/// cache, so unlike a URL tarball it is cached under the integrity bun.lock
/// pins for it: the entry is only reused for the bytes it was extracted from.
///
/// Empty when the integrity is not known yet (lockfile written before tarball
/// integrity was recorded). Callers treat that like a cache miss: extracting the
/// tarball computes the integrity, and the install callbacks record it in the
/// lockfile before installing from the entry named after it.
pub fn cached_local_tarball_folder_name_print<'a>(
buf: &'a mut [u8],
integrity: &Integrity,
patch_hash: Option<u64>,
) -> &'a ZStr {
let Some(algorithm) = integrity.tag.name() else {
return ZStr::EMPTY;
};
let digest = integrity.slice();
let mut w = ByteCursor::new(buf);
w.put(b"@T@");
w.put(algorithm.as_bytes());
w.put_byte(b'-');
w.put_hex_bytes(&digest[..digest.len().min(16)]);
w.put_cache_version(Some(CacheVersion::CURRENT));
w.put_patch_hash(patch_hash);
w.finish_z()
}

pub fn cached_local_tarball_folder_name(
integrity: &Integrity,
patch_hash: Option<u64>,
) -> &'static ZStr {
cached_local_tarball_folder_name_print(cached_package_folder_name_buf(), integrity, patch_hash)
}

pub fn is_folder_in_cache(this: &mut PackageManager, folder_path: &ZStr) -> bool {
sys::directory_exists_at(get_cache_directory(this), folder_path).unwrap_or(false)
}
Expand Down Expand Up @@ -947,6 +994,7 @@ pub fn compute_cache_dir_and_subpath<'a>(
manager: &mut PackageManager,
pkg_name: &[u8],
resolution: &Resolution,
integrity: &Integrity,
folder_path_buf: &'a mut PathBuffer,
patch_hash: Option<u64>,
) -> CacheDirAndSubpath<'a> {
Expand Down Expand Up @@ -986,8 +1034,20 @@ pub fn compute_cache_dir_and_subpath<'a>(
cache_dir = Fd::cwd();
}
ResolutionTag::LocalTarball => {
let tarball = *resolution.local_tarball();
cache_dir_subpath = cached_tarball_folder_name(manager, tarball, patch_hash);
cache_dir_subpath = cached_local_tarball_folder_name(integrity, patch_hash);
if cache_dir_subpath.is_empty() {
Output::err_generic(
"the lockfile does not record an integrity for <b>{}@{}<r>, run <cyan>bun install<r> first",
(
bun_fmt::s(name),
resolution.fmt(
manager.lockfile.buffers.string_bytes.as_slice(),
bun_fmt::PathSep::Posix,
),
),
);
Global::exit(1);
}
cache_dir = get_cache_directory(manager);
}
ResolutionTag::RemoteTarball => {
Expand Down
5 changes: 2 additions & 3 deletions src/install/PackageManager/PackageManagerLifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,8 @@ impl PackageManager {
patch_hash,
)
}
ResolutionTag::LocalTarball => directories::cached_tarball_folder_name(
self,
*pkg.resolution.local_tarball(),
ResolutionTag::LocalTarball => directories::cached_local_tarball_folder_name(
&pkg.meta.integrity,
patch_hash,
),
ResolutionTag::RemoteTarball => directories::cached_tarball_folder_name(
Expand Down
13 changes: 11 additions & 2 deletions src/install/PackageManager/patchPackage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,14 @@ pub fn do_patch_commit(
// `compute_cache_dir_and_subpath` resolves `pkg.resolution`'s strings against `manager.lockfile`.
manager.lockfile = lockfile;
let name = manager.lockfile.str(&pkg.name).to_vec();
let cache_result =
compute_cache_dir_and_subpath(manager, &name, &pkg.resolution, &mut folder_path_buf, None);
let cache_result = compute_cache_dir_and_subpath(
manager,
&name,
&pkg.resolution,
&pkg.meta.integrity,
&mut folder_path_buf,
None,
);
let cache_dir: Fd = cache_result.cache_dir;
let cache_dir_subpath: &ZStr = cache_result.cache_dir_subpath;
let changes_dir: &[u8] = &changes_dir;
Expand Down Expand Up @@ -858,6 +864,7 @@ pub fn prepare_patch(manager: &mut PackageManager) -> Result<(), crate::Error> {
manager,
&name,
&actual_package.resolution,
&actual_package.meta.integrity,
&mut folder_path_buf,
existing_patchfile_hash,
);
Expand Down Expand Up @@ -914,10 +921,12 @@ pub fn prepare_patch(manager: &mut PackageManager) -> Result<(), crate::Error> {
};

let pkg_resolution = pkg.resolution;
let pkg_integrity = pkg.meta.integrity;
let cache_result = compute_cache_dir_and_subpath(
manager,
&pkg_name,
&pkg_resolution,
&pkg_integrity,
&mut folder_path_buf,
existing_patchfile_hash,
);
Expand Down
14 changes: 11 additions & 3 deletions src/install/PackageManager/runTasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ pub trait RunTasksCallbacks {
) {
unreachable!()
}
fn on_extract_store_installer(_ctx: &mut Self::Ctx, _task_id: Task::Id) {
fn on_extract_store_installer(
_ctx: &mut Self::Ctx,
_task_id: Task::Id,
_data: &bun_install::ExtractData,
) {
unreachable!()
}

Expand Down Expand Up @@ -1146,7 +1150,7 @@ pub fn run_tasks<C: RunTasksCallbacks>(
log_level,
);
} else if C::IS_STORE_INSTALLER {
C::on_extract_store_installer(extract_ctx, task.id);
C::on_extract_store_installer(extract_ctx, task.id, task.data_extract());
} else {
unreachable!("unexpected context type");
}
Expand Down Expand Up @@ -1475,7 +1479,11 @@ pub fn run_tasks<C: RunTasksCallbacks>(
log_level,
);
} else if C::IS_STORE_INSTALLER {
C::on_extract_store_installer(extract_ctx, task.id);
C::on_extract_store_installer(
extract_ctx,
task.id,
task.data_git_checkout(),
);
} else {
unreachable!("unexpected context type");
}
Expand Down
16 changes: 3 additions & 13 deletions src/install/TarballStream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1153,12 +1153,14 @@ impl TarballStream {

let (name, basename) = tarball.name_and_basename();

let integrity = tarball.lockfile_integrity(|| self.hasher.final_());
let mut result = match tarball.move_to_cache_directory(
&mut (*task).log,
self.tmpname.as_zstr(),
name,
basename,
self.resolved_github_dirname,
&integrity,
) {
Ok(r) => r,
Err(err) => {
Expand All @@ -1167,19 +1169,7 @@ impl TarballStream {
return;
}
};

match tarball.resolution.tag {
ResolutionTag::Github
| ResolutionTag::RemoteTarball
| ResolutionTag::LocalTarball => {
if tarball.integrity.tag.is_supported() {
result.integrity = tarball.integrity;
} else {
result.integrity = self.hasher.final_();
}
}
_ => {}
}
result.integrity = integrity;

if PackageManager::verbose_install() {
bun_core::pretty_errorln!(
Expand Down
64 changes: 40 additions & 24 deletions src/install/extract_tarball.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,30 @@ impl ExtractTarball {
return Err(crate::Error::IntegrityCheckFailed);
}
}
let mut result = self.extract(log, bytes)?;
let integrity = self.lockfile_integrity(|| Integrity::for_bytes(bytes));
let mut result = self.extract(log, bytes, &integrity)?;
result.integrity = integrity;
Ok(result)
}

// Compute and store SHA-512 integrity hash for GitHub / URL / local tarballs
// so the lockfile can pin the exact tarball content. On subsequent installs
// the hash stored in the lockfile is forwarded via this.integrity and verified
// above, preventing a compromised server from silently swapping the tarball.
/// The integrity the lockfile records for a GitHub / URL / local tarball, so
/// later installs verify the same bytes (see `run`). That is the value the
/// lockfile already pins (verified before extraction), or else `compute` from
/// the bytes on the first install. A local tarball's cache entry is named
/// after it (`move_to_cache_directory`), which is why it is settled before
/// extracting. Unknown for npm packages, whose integrity comes from the
/// registry manifest.
pub(crate) fn lockfile_integrity(&self, compute: impl FnOnce() -> Integrity) -> Integrity {
match self.resolution.tag {
ResolutionTag::Github | ResolutionTag::RemoteTarball | ResolutionTag::LocalTarball => {
if self.integrity.tag.is_supported() {
// Re-installing with an existing lockfile: integrity was already
// verified above, propagate the known value to ExtractData so that
// the lockfile keeps it on re-serialisation.
result.integrity = self.integrity;
self.integrity
} else {
// First install (no integrity in the lockfile yet): compute it.
result.integrity = Integrity::for_bytes(bytes);
compute()
}
}
_ => {}
_ => Integrity::default(),
}

Ok(result)
}
}

Expand Down Expand Up @@ -225,7 +227,12 @@ impl ExtractTarball {
(name, basename)
}

fn extract(&self, log: &mut bun_ast::Log, tgz_bytes: &[u8]) -> Result<ExtractData, Error> {
fn extract(
&self,
log: &mut bun_ast::Log,
tgz_bytes: &[u8],
integrity: &Integrity,
) -> Result<ExtractData, Error> {
let _tracer = bun_core::perf::trace("ExtractTarball.extract");

let tmpdir = Dir::borrow(&self.temp_dir);
Expand Down Expand Up @@ -439,19 +446,24 @@ impl ExtractTarball {
}
}

self.move_to_cache_directory(log, tmpname, name, basename, resolved)
self.move_to_cache_directory(log, tmpname, name, basename, resolved, integrity)
}

/// Rename the freshly-extracted temp directory into the cache, read
/// `package.json` if required, and build the `ExtractData` result. Shared
/// between the buffered and streaming extraction paths.
///
/// `resolved` (GitHub) and `integrity` (local tarballs, see
/// `lockfile_integrity`) name the cache entry for the resolutions whose
/// entries are keyed by content rather than by the resolution string.
pub(crate) fn move_to_cache_directory(
&self,
log: &mut bun_ast::Log,
tmpname: &ZStr,
name: &[u8],
basename: &[u8],
resolved: &[u8],
integrity: &Integrity,
) -> Result<ExtractData, Error> {
let package_manager = self.package_manager.get();

Expand Down Expand Up @@ -500,14 +512,18 @@ impl ExtractTarball {
)
.as_bytes()
}
ResolutionTag::LocalTarball | ResolutionTag::RemoteTarball => {
directories::cached_tarball_folder_name_print(
&mut bufs.folder_name_buf,
self.url.slice(),
None,
)
.as_bytes()
}
ResolutionTag::LocalTarball => directories::cached_local_tarball_folder_name_print(
&mut bufs.folder_name_buf,
integrity,
None,
)
.as_bytes(),
ResolutionTag::RemoteTarball => directories::cached_tarball_folder_name_print(
&mut bufs.folder_name_buf,
self.url.slice(),
None,
)
.as_bytes(),
_ => unreachable!(),
};
if folder_name.is_empty() || (folder_name.len() == 1 && folder_name[0] == b'/') {
Expand Down
Loading
Loading