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
56 changes: 31 additions & 25 deletions src/runtime/cli/pack_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1446,7 +1446,7 @@ fn get_bundled_deps(
// ───────────────────────────────────────────────────────────────────────────

#[derive(Clone, Copy, PartialEq, Eq)]
enum BinType {
pub(crate) enum BinType {
File,
Dir,
}
Expand All @@ -1461,15 +1461,11 @@ fn get_package_bins(json: &Expr) -> Result<Vec<BinInfo>, AllocError> {

let mut path_buf = PathBuffer::uninit();

if let Some(bin) = json.as_property(b"bin") {
if let Some(bin) = non_empty_bin(json) {
if let Some(bin_str) = bin.expr.as_string(pack_bump()) {
let normalized = resolve_path::normalize_buf::<resolve_path::platform::Posix>(
bin_str,
&mut path_buf,
);
if !bin_path_escapes_root(normalized) {
if let Some(subpath) = bin_subpath(bin_str, BinType::File, &mut path_buf) {
bins.push(BinInfo {
path: ZBox::from_bytes(normalized),
path: ZBox::from_bytes(subpath),
ty: BinType::File,
});
}
Expand All @@ -1484,13 +1480,9 @@ fn get_package_bins(json: &Expr) -> Result<Vec<BinInfo>, AllocError> {
for bin_prop in bin_obj.properties.slice() {
if let Some(bin_prop_value) = &bin_prop.value {
if let Some(bin_str) = bin_prop_value.as_string(pack_bump()) {
let normalized = resolve_path::normalize_buf::<resolve_path::platform::Posix>(
bin_str,
&mut path_buf,
);
if !bin_path_escapes_root(normalized) {
if let Some(subpath) = bin_subpath(bin_str, BinType::File, &mut path_buf) {
bins.push(BinInfo {
path: ZBox::from_bytes(normalized),
path: ZBox::from_bytes(subpath),
ty: BinType::File,
});
}
Comment thread
robobun marked this conversation as resolved.
Expand All @@ -1506,13 +1498,9 @@ fn get_package_bins(json: &Expr) -> Result<Vec<BinInfo>, AllocError> {
if let ExprData::EObject(directories_obj) = &directories.expr.data {
if let Some(bin) = directories_obj.as_property(b"bin") {
if let Some(bin_str) = bin.expr.as_string(pack_bump()) {
let normalized = resolve_path::normalize_buf::<resolve_path::platform::Posix>(
bin_str,
&mut path_buf,
);
if !bin_path_escapes_root(normalized) {
if let Some(subpath) = bin_subpath(bin_str, BinType::Dir, &mut path_buf) {
bins.push(BinInfo {
path: ZBox::from_bytes(normalized),
path: ZBox::from_bytes(subpath),
ty: BinType::Dir,
});
}
Expand All @@ -1524,8 +1512,27 @@ fn get_package_bins(json: &Expr) -> Result<Vec<BinInfo>, AllocError> {
Ok(bins)
}

fn bin_path_escapes_root(p: &[u8]) -> bool {
path::is_absolute_loose(p) || p == b".." || p.starts_with(b"../")
/// Like `bun install`, an empty `"bin"` string counts as absent.
pub(crate) fn non_empty_bin(json: &Expr) -> Option<bun_ast::expr::Query> {
json.as_property(b"bin")
.filter(|bin| !matches!(&bin.expr.data, ExprData::EString(bin_str) if bin_str.is_blank()))
}

pub(crate) fn bin_subpath<'a>(value: &[u8], ty: BinType, buf: &'a mut [u8]) -> Option<&'a [u8]> {
let normalized: &'a [u8] =
resolve_path::normalize_buf::<resolve_path::platform::Posix>(value, buf);
let subpath = match ty {
BinType::Dir => strings::without_trailing_slash(normalized),
BinType::File if normalized.ends_with(b"/") || normalized == b"package.json" => {
return None;
}
BinType::File => normalized,
};
(!is_package_root_or_outside(subpath)).then_some(subpath)
}

pub(crate) fn is_package_root_or_outside(p: &[u8]) -> bool {
p.is_empty() || p == b"." || path::is_absolute_loose(p) || p == b".." || p.starts_with(b"../")
}

fn is_package_bin(bins: &[BinInfo], maybe_bin_path: &[u8]) -> bool {
Expand All @@ -1537,9 +1544,8 @@ fn is_package_bin(bins: &[BinInfo], maybe_bin_path: &[u8]) -> bool {
}
}
BinType::Dir => {
let bin_without_trailing = strings::without_trailing_slash(bin.path.as_bytes());
if maybe_bin_path.starts_with(bin_without_trailing) {
let remain = &maybe_bin_path[bin_without_trailing.len()..];
if maybe_bin_path.starts_with(bin.path.as_bytes()) {
let remain = &maybe_bin_path[bin.path.as_bytes().len()..];
if remain.len() > 1
&& remain[0] == b'/'
&& strings::index_of_char(&remain[1..], b'/').is_none()
Expand Down
130 changes: 60 additions & 70 deletions src/runtime/cli/publish_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1589,6 +1589,11 @@ impl PublishCommand {
None
}

fn bin_target<'a>(value: &[u8], path_buf: &'a mut [u8]) -> Option<&'a ZStr> {
let target: &'a ZStr = normalize_buf_z::<path::platform::Posix>(value, path_buf);
(!pack::is_package_root_or_outside(target.as_bytes())).then_some(target)
}

fn normalize_bin(
json: &mut Expr,
bump: &bun_alloc::Arena,
Expand All @@ -1604,35 +1609,31 @@ impl PublishCommand {
};
}
let mut path_buf = PathBuffer::uninit();
let use_directories_bin = pack::non_empty_bin(json).is_none();
if let Some(bin_query) = json.as_property(b"bin") {
match &bin_query.expr.data {
ExprData::EString(bin_str) => {
let mut bin_props: Vec<G::Property> = Vec::new();
let normalized = strings::without_prefix_comptime_z(
normalize_buf_z::<path::platform::Posix>(
bin_str.string(bump)?,
&mut *path_buf,
),
b"./",
);
if !bun_sys::exists_at(workspace_root, normalized) {
bun_core::warn!(
"bin '{}' does not exist",
bstr::BStr::new(normalized.as_bytes()),
);
}
if let Some(value) = Self::bin_target(bin_str.string(bump)?, &mut *path_buf) {
if !bun_sys::exists_at(workspace_root, value) {
bun_core::warn!(
"bin '{}' does not exist",
bstr::BStr::new(value.as_bytes()),
);
}

bin_props.push(G::Property {
key: Some(Expr::init(
E::String::init(leak!(package_name)),
bun_ast::Loc::EMPTY,
)),
value: Some(Expr::init(
E::String::init(leak!(normalized.as_bytes())),
bun_ast::Loc::EMPTY,
)),
..Default::default()
});
bin_props.push(G::Property {
key: Some(Expr::init(
E::String::init(leak!(package_name)),
bun_ast::Loc::EMPTY,
)),
value: Some(Expr::init(
E::String::init(leak!(value.as_bytes())),
bun_ast::Loc::EMPTY,
)),
..Default::default()
});
}

json.data
.e_object_mut()
Expand Down Expand Up @@ -1674,32 +1675,17 @@ impl PublishCommand {
continue;
}

let value: Option<bun_core::ZBox> = 'value: {
if let Some(value) = &bin_prop.value {
if let Some(vs) = value.data.as_e_string() {
if vs.len() != 0 {
break 'value Some(bun_core::ZBox::from_bytes(
strings::without_prefix_comptime_z(
// replace separators
normalize_buf_z::<path::platform::Posix>(
vs.string(bump)?,
&mut *path_buf,
),
b"./",
)
.as_bytes(),
));
}
}
}
None
let Some(value) =
bin_prop.value.as_ref().and_then(|v| v.data.as_e_string())
else {
continue;
};
let Some(value) = value else { continue };
if value.is_empty() {
let Some(value) = Self::bin_target(value.string(bump)?, &mut *path_buf)
else {
continue;
}
};

if !bun_sys::exists_at(workspace_root, &value) {
if !bun_sys::exists_at(workspace_root, value) {
bun_core::warn!(
"bin '{}' does not exist",
bstr::BStr::new(value.as_bytes()),
Expand Down Expand Up @@ -1734,22 +1720,21 @@ impl PublishCommand {
}
_ => {}
}
} else if let Some(directories_query) = json.as_property(b"directories") {
}

// An empty "bin" string is now `{}`. The listing below replaces it.
if use_directories_bin && let Some(directories_query) = json.as_property(b"directories") {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if let Some(bin_query) = directories_query.expr.as_property(b"bin") {
let Some(bin_dir_str) = bin_query.expr.as_string(bump) else {
return Ok(());
};
let mut bin_props: Vec<G::Property> = Vec::new();
let normalized_bin_dir = bun_core::ZBox::from_bytes(
strings::without_trailing_slash(strings::without_prefix(
normalize_buf::<path::platform::Posix>(bin_dir_str, &mut *path_buf),
b"./",
)),
);

if normalized_bin_dir.is_empty() {
let Some(bin_dir_subpath) =
pack::bin_subpath(bin_dir_str, pack::BinType::Dir, &mut *path_buf)
else {
return Ok(());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
};
let normalized_bin_dir = bun_core::ZBox::from_bytes(bin_dir_subpath);
Comment thread
claude[bot] marked this conversation as resolved.

let bin_dir = match bun_sys::openat(
workspace_root,
Expand All @@ -1758,34 +1743,39 @@ impl PublishCommand {
0,
) {
Ok(fd) => fd,
Err(e) => {
if e.get_errno() == bun_sys::E::ENOENT {
Err(e) => match e.get_errno() {
bun_sys::E::ENOENT => {
bun_core::warn!(
"bin directory '{}' does not exist",
bstr::BStr::new(normalized_bin_dir.as_bytes()),
);
return Ok(());
} else {
}
bun_sys::E::ENOTDIR => {
bun_core::warn!(
"bin directory '{}' is not a directory",
bstr::BStr::new(normalized_bin_dir.as_bytes()),
);
return Ok(());
}
_ => {
Output::err(
e,
"failed to open bin directory: '{}'",
(bstr::BStr::new(normalized_bin_dir.as_bytes()),),
);
Global::crash();
}
}
},
};

let mut dirs: Vec<(Fd, Box<[u8]>, bool)> = Vec::new();
let mut dirs: Vec<(Fd, Box<[u8]>)> = Vec::new();

dirs.push((bin_dir, normalized_bin_dir.as_bytes().into(), false));
dirs.push((bin_dir, normalized_bin_dir.as_bytes().into()));

while let Some(dir_info) = dirs.pop() {
let (dir, dir_subpath, close_dir) = dir_info;
let _close = scopeguard::guard(dir, move |d| {
if close_dir {
let _ = d.close();
}
while let Some((dir, dir_subpath)) = dirs.pop() {
let _close = scopeguard::guard(dir, |d| {
let _ = d.close();
});

let mut iter = DirIterator::iterate(dir);
Expand Down Expand Up @@ -1845,7 +1835,7 @@ impl PublishCommand {
else {
continue;
};
dirs.push((subdir, subpath.as_bytes().into(), true));
dirs.push((subdir, subpath.as_bytes().into()));
}
}
}
Expand Down
Loading
Loading