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
8 changes: 0 additions & 8 deletions src/bun_core/string/MutableString.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,14 +260,6 @@ impl MutableString {
unsafe { self.list.set_len(index) };
}

pub fn inflate(&mut self, amount: usize) -> Result<(), AllocError> {
// Callers always overwrite the inflated region, so the
// zero-fill here is technically redundant — but it lowers to a single
// memset and avoids `clippy::uninit_vec` / a `set_len` over uninit bytes.
self.list.resize(amount, 0);
Ok(())
}

#[inline]
pub fn append_char(&mut self, char: u8) -> Result<(), AllocError> {
self.list.push(char);
Expand Down
176 changes: 47 additions & 129 deletions src/install/TarballStream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ type OSPathZMut<'a> = &'a mut OSPathSliceZ;
enum Phase {
/// Call `archive_read_next_header` next.
WantHeader,
/// Currently writing the body of `out_fd`; call
/// Currently writing the body of `out`; call
/// `archive_read_data_block` next.
WantData,
/// `archive_read_next_header` returned EOF; we are done.
Expand Down Expand Up @@ -114,17 +114,11 @@ pub struct TarballStream {
phase: Phase,

/// Output file for the entry currently being written. `None` while
/// between entries or when the current entry is being skipped.
out_fd: Option<Fd>,
#[cfg(unix)]
use_pwrite: bool,
use_lseek: bool,
/// Per-entry write cursors, carried across `write_data_block` calls so
/// the sparse-file handling in `close_output_file` matches
/// `Archive.readDataIntoFd` exactly (which tracks these across its own
/// block loop). Reset in `begin_entry` when a new output file is opened.
entry_actual_offset: i64,
entry_final_offset: i64,
/// between entries or when the current entry is being skipped. The
/// writer is the one the buffered extractor uses, kept here so a block
/// can be written on each side of an ARCHIVE_RETRY yield.
out: Option<lib::EntryWriter>,
write_strategy: lib::WriteStrategy,

/// Temp directory files are written into before being renamed into the
/// cache. Lazily opened on the first drain so the HTTP thread never
Expand Down Expand Up @@ -264,12 +258,8 @@ impl TarballStream {
archive_holds_reading: false,
archive: None,
phase: Phase::WantHeader,
out_fd: None,
#[cfg(unix)]
use_pwrite: true,
use_lseek: true,
entry_actual_offset: 0,
entry_final_offset: 0,
out: None,
write_strategy: lib::WriteStrategy::default(),
dest: None,
tmpname: ZBox::from_bytes(b""),
hasher,
Expand Down Expand Up @@ -540,7 +530,7 @@ impl TarballStream {
// `&mut TarballStream` is held across any libarchive call (which may
// re-enter `archive_read_callback` and access `*this` via the same
// provenance). Transient `&mut *this` for `open_destination` /
// `begin_entry` / `write_data_block` / `close_output_file` is sound:
// `begin_entry` / `write_block` / `finish_output_file` is sound:
// those do not call into libarchive.
unsafe {
if (*this).archive.is_none() {
Expand Down Expand Up @@ -591,17 +581,15 @@ impl TarballStream {
Phase::WantData => {
let mut offset: i64 = 0;
let Some(block) = archive.next(&mut offset) else {
Comment thread
robobun marked this conversation as resolved.
// End of this entry's data.
(*this).close_output_file();
// End of this entry's data; `offset` is its length.
(*this).finish_output_file(offset)?;
(*this).phase = Phase::WantHeader;
continue;
};
match block.result {
lib::Result::Retry if !(*this).archive_holds_reading => return Ok(()),
lib::Result::Ok | lib::Result::Warn => {
if let Some(fd) = (*this).out_fd {
(*this).write_data_block(fd, &block)?;
}
(*this).write_block(&block)?;
}
_ => {
(*this).fail_detail = archive.error_string().to_vec();
Expand Down Expand Up @@ -724,20 +712,33 @@ impl TarballStream {
Ok(())
}

/// The current entry's data ended at `end`. Gives the output file that
/// length, then closes it.
fn finish_output_file(&mut self, end: i64) -> crate::Result<()> {
let Some(mut out) = self.out.take() else {
return Ok(());
};
let result = out.finish(end);
out.fd().close();
result.map_err(|e| e.to_zig_err().into())
}

/// Closes the output file of an entry that did not reach its end. The
/// file keeps the bytes that were written and nothing more.
fn close_output_file(&mut self) {
if let Some(fd) = self.out_fd {
// Same trailing-hole handling as `Archive.readDataIntoFd`:
// extend the file to cover the furthest block we were asked
// to write even if the pwrite/lseek fallback path left
// `actual_offset` behind.
if self.entry_final_offset > self.entry_actual_offset {
let _ = bun_sys::ftruncate(fd, self.entry_final_offset);
}
fd.close();
self.out_fd = None;
if let Some(out) = self.out.take() {
out.fd().close();
}
}

fn write_block(&mut self, block: &lib::Block<'_>) -> crate::Result<()> {
let Some(out) = self.out.as_mut() else {
return Ok(());
};
out.write(&mut self.write_strategy, block.offset, block.bytes)
.map_err(|e| e.to_zig_err().into())
}

/// Process one entry header returned by `read_next_header`. Opens the
/// output file (or creates the directory/symlink) and transitions to
/// `WantData` so the next `step()` iteration starts pulling its body.
Expand Down Expand Up @@ -782,7 +783,7 @@ impl TarballStream {
// npm tarballs only contain files; matching the libarchive path
// in Archiver.extractToDir we skip everything else.
self.phase = Phase::WantData;
self.out_fd = None;
self.out = None;
return Ok(());
}

Expand All @@ -794,7 +795,7 @@ impl TarballStream {
.filter(|s| !s.is_empty());
if tokenizer.next().is_none() {
self.phase = Phase::WantData;
self.out_fd = None;
self.out = None;
return Ok(());
}
// tokenizeScalar.rest() — need byte offset of remainder, not just
Expand All @@ -812,7 +813,7 @@ impl TarballStream {
bun_core::fmt::fmt_os_path(rest, Default::default()),
);
self.phase = Phase::WantData;
self.out_fd = None;
self.out = None;
return Ok(());
}
let normalized =
Expand All @@ -824,7 +825,7 @@ impl TarballStream {
unsafe { OSPathSliceZ::from_raw_mut(norm_buf.as_mut_ptr(), norm_len) };
if path.is_empty() || (path.len() == 1 && path[0] == ('.' as OSPathChar)) {
self.phase = Phase::WantData;
self.out_fd = None;
self.out = None;
return Ok(());
}
// `normalize_buf_t` collapses interior `..` but leaves a leading `..`
Expand All @@ -838,14 +839,14 @@ impl TarballStream {
&& (path.len() == 2 || path[2] == bun_paths::SEP as OSPathChar)
{
self.phase = Phase::WantData;
self.out_fd = None;
self.out = None;
return Ok(());
}
#[cfg(windows)]
{
if bun_paths::is_absolute_windows_wtf16(&path[..]) {
self.phase = Phase::WantData;
self.out_fd = None;
self.out = None;
return Ok(());
}
if self.npm_mode {
Expand All @@ -863,7 +864,7 @@ impl TarballStream {
FileKind::Directory => {
make_directory(entry, dest, path, path_slice);
self.phase = Phase::WantData;
self.out_fd = None;
self.out = None;
}
FileKind::SymLink => {
#[cfg(unix)]
Expand All @@ -875,7 +876,7 @@ impl TarballStream {
));
}
self.phase = Phase::WantData;
self.out_fd = None;
self.out = None;
}
FileKind::File => {
#[cfg(windows)]
Expand All @@ -886,100 +887,17 @@ impl TarballStream {
let mode: Mode = Mode::try_from((entry.perm() & 0o777) | 0o666).expect("int cast");
let fd = open_output_file(dest, path, path_slice, mode)?;
self.entry_count += 1;

#[cfg(any(target_os = "linux", target_os = "android"))]
{
let size: usize = usize::try_from(entry.size().max(0)).expect("int cast");
if size > 1_000_000 {
let _ = bun_sys::preallocate_file(
fd.native(),
0,
i64::try_from(size).expect("int cast"),
);
}
}

self.out_fd = Some(fd);
self.entry_actual_offset = 0;
self.entry_final_offset = 0;
self.out = Some(lib::EntryWriter::new(fd));
self.phase = Phase::WantData;
}
_ => {
self.phase = Phase::WantData;
self.out_fd = None;
self.out = None;
}
}
Ok(())
}

/// Write one data block from `archive_read_data_block`. Mirrors the
/// sparse/pwrite handling in `Archive.readDataIntoFd` but operates on a
/// single block so it can be interleaved with ARCHIVE_RETRY yields.
/// `entry_actual_offset` / `entry_final_offset` persist across calls so
/// `close_output_file` can perform the same trailing `ftruncate` the
/// buffered path does after its block loop.
fn write_data_block(&mut self, fd: Fd, block: &lib::Block) -> crate::Result<()> {
let file = bun_sys::File::borrow(&fd);
let data = block.bytes;
if data.is_empty() {
return Ok(());
}

self.entry_final_offset = self
.entry_final_offset
.max(block.offset + i64::try_from(data.len()).expect("int cast"));

#[cfg(unix)]
{
if self.use_pwrite {
match file.pwrite_all(data, block.offset) {
Ok(_) => {
self.entry_actual_offset = self
.entry_actual_offset
.max(block.offset + i64::try_from(data.len()).expect("int cast"));
return Ok(());
}
Err(_) => self.use_pwrite = false,
}
}
}

'seek: {
if block.offset == self.entry_actual_offset {
break 'seek;
}
if self.use_lseek {
match file.seek_to(u64::try_from(block.offset).expect("int cast")) {
Ok(_) => {
self.entry_actual_offset = block.offset;
break 'seek;
}
Err(_) => self.use_lseek = false,
}
}
if block.offset > self.entry_actual_offset {
let zero_count: usize =
usize::try_from(block.offset - self.entry_actual_offset).expect("int cast");
match lib::Archive::write_zeros_to_file(file, zero_count) {
lib::Result::Ok => {
self.entry_actual_offset = block.offset;
}
_ => return Err(crate::Error::Fail),
}
} else {
return Err(crate::Error::Fail);
}
}

match file.write_all(data) {
Ok(_) => {
self.entry_actual_offset += i64::try_from(data.len()).expect("int cast");
Ok(())
}
Err(e) => Err(e.to_zig_err().into()),
}
}

/// # Safety
/// `this` must be the live pointer returned by `init()`. Frees `*this`
/// — caller must not touch it after return. Takes a raw pointer (not
Expand Down Expand Up @@ -1264,8 +1182,8 @@ impl TarballStream {

impl Drop for TarballStream {
fn drop(&mut self) {
if let Some(fd) = self.out_fd {
fd.close();
if let Some(out) = self.out.take() {
out.fd().close();
}
if let Some(d) = self.dest {
d.close();
Expand Down
39 changes: 38 additions & 1 deletion src/install/extract_tarball.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,40 @@ pub(crate) fn uses_streaming_extraction() -> bool {
.unwrap_or(false)
}

/// RAII owner of a temporary extraction directory under `parent`. Removes
/// `parent/name` on drop unless [`commit`](Self::commit) is called after the
/// directory has been renamed into the cache. This keeps failed extractions
/// (decompression errors, truncated tarballs, rename failures) from leaking an
/// extraction directory per attempt in `$TMPDIR`.
struct TempExtractionDir<'a> {
parent: Fd,
name: Option<&'a ZStr>,
}

impl<'a> TempExtractionDir<'a> {
#[inline]
fn new(parent: Fd, name: &'a ZStr) -> Self {
Self {
parent,
name: Some(name),
}
}

/// Disarm the drop guard after the directory has been renamed away.
#[inline]
fn commit(mut self) {
self.name = None;
}
}

impl Drop for TempExtractionDir<'_> {
fn drop(&mut self) {
if let Some(name) = self.name {
let _ = Dir::borrow(&self.parent).delete_tree(name.as_bytes());
Comment thread
robobun marked this conversation as resolved.
}
}
}

impl ExtractTarball {
/// Derive the display name and a filesystem-safe basename for this
/// package. Shared by the buffered `extract()` path below and the
Expand Down Expand Up @@ -259,6 +293,7 @@ impl ExtractTarball {
let mut resolved: &'static [u8] = b"";
let tmpname =
FileSystem::tmpname(tmpname_suffix, &mut tmpname_buf.0, bun_core::fast_random())?;
let tmpdir_guard = TempExtractionDir::new(self.temp_dir, tmpname);
{
let extract_destination = match bun_sys::make_path::make_open_path(
tmpdir,
Expand Down Expand Up @@ -450,7 +485,9 @@ impl ExtractTarball {
}
}

self.move_to_cache_directory(log, tmpname, name, basename, resolved)
let result = self.move_to_cache_directory(log, tmpname, name, basename, resolved)?;
tmpdir_guard.commit();
Comment thread
robobun marked this conversation as resolved.
Ok(result)
}

/// Rename the freshly-extracted temp directory into the cache, read
Expand Down
Loading
Loading