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
122 changes: 78 additions & 44 deletions src/runtime/api/Archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,22 @@ pub enum ExtractError {
pub(crate) enum ExtractResult {
Success(u32),
Err(ExtractError),
/// The file system refused an entry. Boxed: the job allocation keeps its size class.
SysErr(Box<bun_sys::Error>),
}

impl ExtractResult {
/// `err` with its path below `root`. An error that carries no path gets `entry`.
fn refused(err: &bun_sys::Error, root: &[u8], entry: &[u8]) -> ExtractResult {
let below: &[u8] = if err.path.is_empty() {
entry
} else {
&err.path
};
let mut path = bun_paths::join_sep_maybe_z::<false>(&[root, below]).into_vec();
bun_paths::resolve_path::posix_to_platform_in_place(&mut path[root.len()..]);
ExtractResult::SysErr(Box::new(err.with_path(&path)))
}
}

pub(crate) struct ExtractContext {
Expand All @@ -697,6 +713,7 @@ impl TaskContext for ExtractContext {
ExtractResult::Err(e) => PromiseResult::Reject(
global.create_error_instance(format_args!("{}", <&'static str>::from(e))),
),
ExtractResult::SysErr(sys_err) => PromiseResult::Reject(sys_err.to_js(global)),
})
}
}
Expand All @@ -705,15 +722,11 @@ impl ExtractContext {
fn do_run(&mut self) -> ExtractResult {
// If we have glob patterns, use filtered extraction
if self.glob_patterns.is_some() {
let count = match extract_to_disk_filtered(
return extract_to_disk_filtered(
self.store.shared_view(),
&self.path,
self.glob_patterns.as_deref(),
) {
Ok(c) => c,
Err(_) => return ExtractResult::Err(ExtractError::ReadError),
};
return ExtractResult::Success(count);
);
}

// Otherwise use the fast path without filtering
Expand Down Expand Up @@ -1240,19 +1253,28 @@ pub(crate) fn match_glob_patterns(patterns: &[GlobPattern], pathname: &[u8]) ->
!has_positive_patterns || matches_positive
}

/// The error of a mkdir of `dir`. On Windows it names an open call and carries no path.
fn mkdir_error(mut err: bun_sys::Error, dir: &[u8]) -> bun_sys::Error {
err.syscall = bun_sys::Tag::mkdir;
if err.path.is_empty() {
err.path = dir.into();
}
err
}

/// Extract archive to disk with glob pattern filtering.
/// Supports negative patterns with "!" prefix (e.g., "!node_modules/**").
fn extract_to_disk_filtered(
file_buffer: &[u8],
root: &[u8],
glob_patterns: Option<&[GlobPattern]>,
) -> crate::Result<u32> {
) -> ExtractResult {
use libarchive::lib;
let archive = lib::ReadArchive::new();
configure_archive_reader(&archive);

if archive.read_open_memory(file_buffer) != lib::Result::Ok {
return Err(crate::Error::ReadError);
return ExtractResult::Err(ExtractError::ReadError);
}

// Open/create target directory using bun.sys
Expand All @@ -1262,7 +1284,7 @@ fn extract_to_disk_filtered(
if bun_paths::is_absolute(root) {
break 'brk match bun_sys::open_a(root, bun_sys::O::RDONLY | bun_sys::O::DIRECTORY, 0) {
Ok(fd) => fd,
Err(_) => return Err(crate::Error::OpenError),
Err(_) => return ExtractResult::Err(ExtractError::ReadError),
};
} else {
break 'brk match bun_sys::openat_a(
Expand All @@ -1272,7 +1294,7 @@ fn extract_to_disk_filtered(
0,
) {
Ok(fd) => fd,
Err(_) => return Err(crate::Error::OpenError),
Err(_) => return ExtractResult::Err(ExtractError::ReadError),
};
}
};
Expand All @@ -1286,18 +1308,20 @@ fn extract_to_disk_filtered(
match archive.read_next_header(&mut entry) {
lib::Result::Eof => break,
lib::Result::Retry => continue,
lib::Result::Failed | lib::Result::Fatal => return Err(crate::Error::ReadError),
lib::Result::Failed | lib::Result::Fatal => {
return ExtractResult::Err(ExtractError::ReadError);
}
lib::Result::Ok | lib::Result::Warn => {}
}
let entry_ref = lib::Entry::opaque_ref(entry);
// Same platform split as `FilesContext::do_run`; see `entry_pathname_utf8`.
#[cfg(not(windows))]
let raw_pathname_z = entry_ref.pathname();
#[cfg(windows)]
let raw_pathname_zbox = ZBox::from_vec_with_nul(
entry_pathname_utf8(entry_ref)
.map_err(|_| crate::Error::Alloc(bun_alloc::AllocError))?,
);
let raw_pathname_zbox = match entry_pathname_utf8(entry_ref) {
Ok(name) => ZBox::from_vec_with_nul(name),
Err(_) => return ExtractResult::Err(ExtractError::ReadError),
};
#[cfg(windows)]
let raw_pathname_z = raw_pathname_zbox.as_zstr();
let raw_pathname = raw_pathname_z.as_bytes();
Expand Down Expand Up @@ -1330,11 +1354,23 @@ fn extract_to_disk_filtered(

match kind {
bun_sys::FileKind::Directory => {
match dir_fd.make_path(pathname) {
// Directory already exists - don't count as extracted
Err(e) if e.get_errno() == bun_sys::E::EEXIST => continue,
Err(_) => continue,
Ok(()) => {}
// The entry `x/` is the directory `x`.
let name_len = strings::without_trailing_slash(pathname).len();
normalized_buf[name_len] = 0;
let name = bun_core::ZStr::from_buf(&normalized_buf[..], name_len);
let made = match bun_sys::mkdirat(dir_fd, name, 0o755) {
Err(err) if err.get_errno() == bun_sys::E::ENOENT => {
dir_fd.make_path(name.as_bytes())
}
// Any other refusal is fine when a directory holds the name.
Err(_) if matches!(bun_sys::directory_exists_at(dir_fd, name), Ok(true)) => {
Ok(())
}
made => made,
};
if let Err(err) = made {
let err = mkdir_error(err, name.as_bytes());
return ExtractResult::refused(&err, root, name.as_bytes());
}
count += 1;
}
Expand All @@ -1343,33 +1379,31 @@ fn extract_to_disk_filtered(
// Sanitize permissions: use entry perms masked to 0o777, or default 0o644
let entry_perm = entry_ref.perm();
let mode: Mode = if entry_perm != 0 {
Mode::try_from(entry_perm & 0o777).expect("int cast")
// Without owner-write, a second extract or a later entry of this name gets EACCES.
Mode::try_from(entry_perm & 0o777).expect("int cast") | 0o200
Comment thread
robobun marked this conversation as resolved.
Resolved
} else {
0o644
};

// Create parent directories if needed (ignore expected errors)
if let Some(parent_dir) = bun_core::dirname(pathname) {
match dir_fd.make_path(parent_dir) {
// Expected: directory already exists
Err(e) if e.get_errno() == bun_sys::E::EEXIST => {}
// Permission errors: skip this file, will fail at openat
Err(e) if e.get_errno() == bun_sys::E::EACCES => {}
// Other errors: skip, will fail at openat
Err(_) => {}
Ok(()) => {}
}
}

// Create and write the file using bun.sys
let file_fd: Fd = match bun_sys::openat(
dir_fd,
pathname_z,
bun_sys::O::WRONLY | bun_sys::O::CREAT | bun_sys::O::TRUNC,
mode,
) {
let open = || {
bun_sys::openat(
dir_fd,
pathname_z,
bun_sys::O::WRONLY | bun_sys::O::CREAT | bun_sys::O::TRUNC,
mode,
)
};
let opened = open().or_else(|err| match bun_core::dirname(pathname) {
// The parent directory is missing.
Some(parent) if err.get_errno() == bun_sys::E::ENOENT => dir_fd
.make_path(parent)
.map_err(|err| mkdir_error(err, parent))
.and_then(|()| open()),
_ => Err(err),
});
let file_fd: Fd = match opened {
Ok(fd) => fd,
Err(_) => continue,
Err(err) => return ExtractResult::refused(&err, root, pathname),
Comment thread
robobun marked this conversation as resolved.
Resolved
};

let write_success = size == 0
Expand All @@ -1379,7 +1413,7 @@ fn extract_to_disk_filtered(
if !write_success {
// Remove partial file on failure
let _ = bun_sys::unlinkat(dir_fd, pathname_z);
return Err(crate::Error::ReadError);
return ExtractResult::Err(ExtractError::ReadError);
}
count += 1;
}
Expand Down Expand Up @@ -1415,5 +1449,5 @@ fn extract_to_disk_filtered(
}
}

Ok(count)
ExtractResult::Success(count)
}
6 changes: 0 additions & 6 deletions src/runtime/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,6 @@ pub enum Error {
InvalidHeaderName,
#[error("FormatError")]
FormatError,
#[error("ReadError")]
ReadError,
#[error("OpenError")]
OpenError,
#[error("UnexpectedPendingResolution")]
UnexpectedPendingResolution,
#[error("AsyncModule")]
Expand Down Expand Up @@ -396,8 +392,6 @@ impl Error {
Self::UnableToEncode => "UnableToEncode",
Self::InvalidHeaderName => "InvalidHeaderName",
Self::FormatError => "FormatError",
Self::ReadError => "ReadError",
Self::OpenError => "OpenError",
Self::UnexpectedPendingResolution => "UnexpectedPendingResolution",
Self::AsyncModule => "AsyncModule",
Self::BlobNotFound => "BlobNotFound",
Expand Down
Loading
Loading