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
150 changes: 149 additions & 1 deletion src/paths/resolve_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,40 @@ pub fn normalize_buf_z<'a, P: PlatformT>(str: &[u8], buf: &'a mut [u8]) -> &'a m
unsafe { ZStr::from_raw_mut(buf.as_mut_ptr(), len) }
}

/// [`normalize_buf`] into `buf` when the result fits, otherwise into `spill` (grown as needed).
pub fn normalize_buf_spill<'a, P: PlatformT>(
buf: &'a mut [u8],
spill: &'a mut Vec<u8>,
str: &[u8],
) -> &'a [u8] {
normalize_buf::<P>(str, normalize_buf_or_spill(buf, spill, str))
}

/// [`normalize_buf_z`] into `buf` when the result fits, otherwise into `spill` (grown as needed).
pub fn normalize_buf_z_spill<'a, P: PlatformT>(
buf: &'a mut [u8],
spill: &'a mut Vec<u8>,
str: &[u8],
) -> &'a ZStr {
normalize_buf_z::<P>(str, normalize_buf_or_spill(buf, spill, str))
}

fn normalize_buf_or_spill<'a>(
buf: &'a mut [u8],
spill: &'a mut Vec<u8>,
str: &[u8],
) -> &'a mut [u8] {
// Normalizing grows a path by at most one byte (`""` -> `.`, `C:` -> `C:.`), plus the NUL.
let needed = str.len() + 2;
if needed <= buf.len() {
return buf;
}
if spill.len() < needed {
spill.resize(needed, 0);
}
&mut spill[..]
}

pub fn normalize_buf_t<'a, T: PathChar, P: PlatformT>(str: &[T], buf: &'a mut [T]) -> &'a mut [T] {
if str.is_empty() {
buf[0] = T::from_u8(b'.');
Expand Down Expand Up @@ -2455,7 +2489,7 @@ pub use crate::PathChar;

// Run with `cargo test -p bun_paths` (also the Miri lane, `bun run rust:miri -p bun_paths`).
// Normalizing reaches `bun_core::strings`' byte searches, whose highway kernels
// are only linked into the full binary, so the two they reference are satisfied
// are only linked into the full binary, so the ones they reference are satisfied
// below with scalar stubs (the simdutf counterparts live in string_paths.rs).
// Under Miri the wrappers take their own scalar path and never call these.
#[cfg(test)]
Expand Down Expand Up @@ -2497,6 +2531,45 @@ mod tests {
.unwrap_or(text_len)
}

/// Returns `haystack_len` when `needle` does not occur, like the kernel.
#[unsafe(no_mangle)]
unsafe extern "C" fn highway_last_index_of_char(
haystack: *const u8,
haystack_len: usize,
needle: u8,
) -> usize {
// SAFETY: test stub; callers pass a valid (ptr, len) pair.
let haystack = unsafe { core::slice::from_raw_parts(haystack, haystack_len) };
haystack
.iter()
.rposition(|&b| b == needle)
.unwrap_or(haystack_len)
}

/// Returns `usize::MAX` when `needle` does not occur, like the kernel.
#[unsafe(no_mangle)]
unsafe extern "C" fn highway_memrmem16(
haystack: *const u16,
haystack_len: usize,
needle: *const u16,
needle_len: usize,
) -> usize {
// SAFETY: test stub; callers pass valid (ptr, len) pairs.
let (haystack, needle) = unsafe {
(
core::slice::from_raw_parts(haystack, haystack_len),
core::slice::from_raw_parts(needle, needle_len),
)
};
if needle_len > haystack_len {
return usize::MAX;
}
(0..=haystack_len - needle_len)
.rev()
.find(|&i| haystack[i..i + needle_len] == *needle)
.unwrap_or(usize::MAX)
}

#[test]
fn normalize_string_spill_leaves_spill_untouched_when_the_input_fits() {
let mut spill = Vec::new();
Expand Down Expand Up @@ -2561,4 +2634,79 @@ mod tests {
b"C:."
);
}

#[test]
fn normalize_buf_spill_leaves_spill_untouched_when_the_input_fits() {
let mut buf = [0u8; 32];
let mut spill = Vec::new();
assert_eq!(
normalize_buf_spill::<platform::Posix>(&mut buf, &mut spill, b"./bins/../cli/./x.js"),
b"cli/x.js"
);
assert_eq!(
normalize_buf_z_spill::<platform::Posix>(&mut buf, &mut spill, b"./bins/").as_bytes(),
b"bins/"
);
assert!(spill.is_empty());
}

#[test]
fn normalize_buf_spill_spills_input_longer_than_buf() {
let mut buf = [0u8; 32];
let name = vec![b'b'; buf.len() * 3];
let mut input = b"./".to_vec();
input.extend_from_slice(&name);
input.extend_from_slice(b"/./x.js");
let mut expected = name;
expected.extend_from_slice(b"/x.js");

let mut spill = Vec::new();
assert_eq!(
normalize_buf_spill::<platform::Posix>(&mut buf, &mut spill, &input),
&expected[..]
);
expected.push(0);
assert_eq!(
normalize_buf_z_spill::<platform::Posix>(&mut buf, &mut spill, &input)
.as_bytes_with_nul(),
&expected[..]
);
assert!(!spill.is_empty());
}

#[test]
fn normalize_buf_z_spill_spills_input_exactly_as_long_as_buf() {
// The input normalizes to `buf.len()` bytes, leaving no room for the NUL.
let mut buf = [0u8; 32];
let input = vec![b'a'; buf.len()];
let mut expected = input.clone();
expected.push(0);

let mut spill = Vec::new();
assert_eq!(
normalize_buf_z_spill::<platform::Posix>(&mut buf, &mut spill, &input)
.as_bytes_with_nul(),
&expected[..]
);
assert!(!spill.is_empty());
}

#[test]
fn normalize_buf_spill_sizes_the_spill_for_the_empty_input_becoming_a_dot() {
let mut buf = [0u8; 1];

let mut spill = Vec::new();
assert_eq!(
normalize_buf_spill::<platform::Posix>(&mut buf, &mut spill, b""),
b"."
);
assert_eq!(spill.len(), 2);

let mut spill = Vec::new();
assert_eq!(
normalize_buf_z_spill::<platform::Posix>(&mut buf, &mut spill, b"").as_bytes_with_nul(),
b".\0"
);
assert_eq!(spill.len(), 2);
}
}
27 changes: 16 additions & 11 deletions src/runtime/cli/pack_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type CowString = CowSlice<u8>;
use crate::cli::run_command::RunCommand;
use bun_core::ZBox;
use bun_core::{ZStr, strings};
use bun_paths::resolve_path;
use bun_paths::resolve_path::{self, normalize_buf_spill};
use bun_semver as Semver;
use bun_sha_hmac::sha;
use bun_sys::{
Expand Down Expand Up @@ -1460,12 +1460,14 @@ fn get_package_bins(json: &Expr) -> Result<Vec<BinInfo>, AllocError> {
let mut bins: Vec<BinInfo> = Vec::new();

let mut path_buf = PathBuffer::uninit();
let mut path_spill: Vec<u8> = Vec::new();

if let Some(bin) = json.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,
let normalized = normalize_buf_spill::<path::platform::Posix>(
&mut path_buf,
&mut path_spill,
bin_str,
);
if !bin_path_escapes_root(normalized) {
bins.push(BinInfo {
Expand All @@ -1484,9 +1486,10 @@ 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,
let normalized = normalize_buf_spill::<path::platform::Posix>(
&mut path_buf,
&mut path_spill,
bin_str,
);
if !bin_path_escapes_root(normalized) {
bins.push(BinInfo {
Expand All @@ -1506,9 +1509,10 @@ 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,
let normalized = normalize_buf_spill::<path::platform::Posix>(
&mut path_buf,
&mut path_spill,
bin_str,
);
if !bin_path_escapes_root(normalized) {
bins.push(BinInfo {
Expand Down Expand Up @@ -2309,12 +2313,13 @@ pub(crate) fn pack<const FOR_PUBLISH: bool>(
let mut excludes: Vec<Pattern> = Vec::new();

let mut path_buf = PathBuffer::uninit();
let mut path_spill: Vec<u8> = Vec::new();
while let Some(files_entry) = files_array.next() {
if let Some(file_entry_str) = files_entry.as_string(bump) {
let normalized = resolve_path::normalize_buf::<
resolve_path::platform::Posix,
>(
file_entry_str, &mut path_buf
let normalized = normalize_buf_spill::<path::platform::Posix>(
&mut path_buf,
&mut path_spill,
file_entry_str,
);
let Some(parsed) = Pattern::from_utf8(normalized)? else {
continue;
Expand Down
26 changes: 17 additions & 9 deletions src/runtime/cli/publish_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use bun_install::lockfile::{LoadResult, LoadStep};
use bun_install::{self as install, Lockfile, Npm, PackageManager, Subcommand};
use bun_libarchive::lib::{Archive, ArchiveIterator, IteratorResult as ArchiveIterResult};
use bun_parsers::json as json_mod;
use bun_paths::resolve_path::{join_abs_string_buf_z, normalize_buf, normalize_buf_z};
use bun_paths::resolve_path::{join_abs_string_buf_z, normalize_buf_spill, normalize_buf_z_spill};
use bun_paths::{self as path, PathBuffer};
use bun_resolver::fs::FileSystem;
use bun_sha_hmac as sha;
Expand Down Expand Up @@ -1625,14 +1625,16 @@ impl PublishCommand {
};
}
let mut path_buf = PathBuffer::uninit();
let mut path_spill: Vec<u8> = Vec::new();
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)?,
normalize_buf_z_spill::<path::platform::Posix>(
&mut *path_buf,
&mut path_spill,
bin_str.string(bump)?,
),
b"./",
);
Expand Down Expand Up @@ -1677,9 +1679,10 @@ impl PublishCommand {
if ks.len() != 0 {
break 'key Some(Box::<[u8]>::from(
strings::without_prefix(
normalize_buf::<path::platform::Posix>(
ks.string(bump)?,
normalize_buf_spill::<path::platform::Posix>(
&mut *path_buf,
&mut path_spill,
ks.string(bump)?,
),
b"./",
),
Expand All @@ -1702,9 +1705,10 @@ impl PublishCommand {
break 'value Some(bun_core::ZBox::from_bytes(
strings::without_prefix_comptime_z(
// replace separators
normalize_buf_z::<path::platform::Posix>(
vs.string(bump)?,
normalize_buf_z_spill::<path::platform::Posix>(
&mut *path_buf,
&mut path_spill,
vs.string(bump)?,
),
b"./",
)
Expand Down Expand Up @@ -1763,7 +1767,11 @@ impl PublishCommand {
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),
normalize_buf_spill::<path::platform::Posix>(
&mut *path_buf,
&mut path_spill,
bin_dir_str,
),
b"./",
)),
);
Expand All @@ -1780,7 +1788,7 @@ impl PublishCommand {
) {
Ok(fd) => fd,
Err(e) => {
if e.get_errno() == bun_sys::E::ENOENT {
if matches!(e.get_errno(), bun_sys::E::ENOENT | bun_sys::E::ENAMETOOLONG) {
bun_core::warn!(
"bin directory '{}' does not exist",
bstr::BStr::new(normalized_bin_dir.as_bytes()),
Expand Down
Loading
Loading