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
4 changes: 2 additions & 2 deletions src/install/PackageManager/patchPackage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1231,8 +1231,8 @@ fn overwrite_package_in_node_modules_folder(

let mut copier: FileCopier = FileCopier::init(
cached_package_folder.fd,
src_path,
dest_subpath,
src_path.into_checked(),
dest_subpath.into_checked(),
ignore_directories,
)?;

Expand Down
56 changes: 19 additions & 37 deletions src/install/isolated_install/FileCopier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,19 @@
use core::ptr;

use bun_alloc::AllocError;
#[cfg(not(windows))]
use bun_core::{Global, fmt as bun_fmt};
use bun_paths::path_options::{CheckLength, Kind, PathSeparators};
use bun_paths::{self, OSPathChar, OSPathSlice};
use bun_sys::{self as sys, Dir, E, EntryKind, Fd, walker_skippable, walker_skippable::Walker};

// The path-builder types here use the OS path unit: u8 on POSIX,
// u16 on Windows — encoded via `OSPathChar` so `slice()`/`slice_z()` produce
// the platform-native width. The auto separator mode normalizes `/` → `\` on Windows
// during `from`/`append`, which is load-bearing for the Win32 calls below.
// Length-checked like the Hardlinker's paths: the walker entries appended on Windows are unbounded.
type AbsPathAutoOs =
bun_paths::AbsPath<OSPathChar, { bun_paths::path_options::PathSeparators::AUTO }>;
type PathAutoOs = bun_paths::Path<
OSPathChar,
{ bun_paths::path_options::Kind::ANY },
{ bun_paths::path_options::PathSeparators::AUTO },
>;
bun_paths::AbsPath<OSPathChar, { PathSeparators::AUTO }, { CheckLength::CHECK }>;
type PathAutoOs =
bun_paths::Path<OSPathChar, { Kind::ANY }, { PathSeparators::AUTO }, { CheckLength::CHECK }>;

pub(crate) struct FileCopier {
pub(crate) src_path: AbsPathAutoOs,
Expand Down Expand Up @@ -110,15 +107,17 @@ impl FileCopier {

// A `path.save()` ResetScope would hold `&mut Path` and keep
// `self.src_path` / `self.dest_subpath` exclusively borrowed
// for the rest of the iteration. Capture the saved length and
// for the rest of the iteration. Capture the saved lengths and
// restore via `set_length` after the body instead.
let src_saved_len = self.src_path.len();
let _ = self.src_path.append(entry.path.as_slice());

let dest_saved_len = self.dest_subpath.len();
let _ = self.dest_subpath.append(entry.path.as_slice());
let appended = self.src_path.append(entry.path.as_slice()).is_ok()
&& self.dest_subpath.append(entry.path.as_slice()).is_ok();

let result: sys::Result<()> = match entry.kind {
_ if !appended => {
sys::Result::Err(sys::Error::from_code(E::ENAMETOOLONG, sys::Tag::copyfile))
}
EntryKind::Directory => {
// SAFETY: FFI — both `slice_z()` are NUL-terminated WStrs.
if unsafe {
Expand All @@ -129,12 +128,11 @@ impl FileCopier {
)
} == 0
{
let _ = bun_sys::make_path::make_path::<u16>(
&dest_dir,
entry.path.as_slice(),
);
// Also taken when the directory exists; make_path treats that as success.
bun_sys::make_path::make_path::<u16>(&dest_dir, entry.path.as_slice())
} else {
sys::Result::Ok(())
}
sys::Result::Ok(())
}
EntryKind::File => {
match bun_sys::copy_file::copy_file(
Expand Down Expand Up @@ -175,9 +173,7 @@ impl FileCopier {
self.src_path.set_length(src_saved_len);
self.dest_subpath.set_length(dest_saved_len);

if let sys::Result::Err(err) = result {
return sys::Result::Err(err);
}
result?;
}
#[cfg(not(windows))]
{
Expand All @@ -194,7 +190,7 @@ impl FileCopier {

let dest = match dest_dir.create_file_z(entry.path, Default::default()) {
Ok(f) => f,
Err(_) => 'dest: {
Err(_) => {
if let Some(entry_dirname) =
bun_paths::Dirname::dirname::<OSPathChar>(entry.path)
{
Expand All @@ -203,27 +199,13 @@ impl FileCopier {
entry_dirname,
);
}

match dest_dir.create_file_z(entry.path, Default::default()) {
Ok(f) => break 'dest f,
Err(err) => {
bun_core::pretty_errorln!(
"<r><red>{}<r>: copy file {}",
bstr::BStr::new(err.name()),
bun_fmt::fmt_os_path(entry.path, Default::default()),
);
Global::exit(1);
}
}
dest_dir.create_file_z(entry.path, Default::default())?
}
};

#[cfg(unix)]
{
let stat = match bun_sys::fstat(src.handle()) {
sys::Result::Ok(s) => s,
sys::Result::Err(_) => continue,
};
let stat = bun_sys::fstat(src.handle())?;
// SAFETY: fchmod is safe to call with any fd + mode; errors are ignored (`_ =`).
unsafe {
let _ = bun_sys::c::fchmod(dest.handle().native(), stat.st_mode);
Expand Down
54 changes: 35 additions & 19 deletions src/install/isolated_install/Hardlinker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@ use bun_sys::{self as sys, EntryKind, Fd, FdExt};
// on Windows — encoded here via the `OSPathChar` type alias so the struct's
// `slice()`/`slice_z()` produce the platform-native width without per-field
// `#[cfg]` divergence.
#[cfg(windows)]
use bun_paths::path_options::AssumeOk as _;
use bun_paths::path_options::{CheckLength, Kind, PathSeparators};
use bun_paths::{AbsPath, OSPathChar, OSPathSlice, Path};

type OsAbsPath = AbsPath<OSPathChar, { bun_paths::path_options::PathSeparators::AUTO }>;
type OsPath = Path<
OSPathChar,
{ bun_paths::path_options::Kind::ANY },
{ bun_paths::path_options::PathSeparators::AUTO },
>;
// Length-checked: the walker opens each directory relative to its parent, so entry paths are unbounded.
type OsAbsPath = AbsPath<OSPathChar, { PathSeparators::AUTO }, { CheckLength::CHECK }>;
type OsPath = Path<OSPathChar, { Kind::ANY }, { PathSeparators::AUTO }, { CheckLength::CHECK }>;

pub(crate) struct Hardlinker {
pub(crate) src: OsAbsPath,
pub(crate) dest: OsPath,
pub(crate) walker: Walker,
}

fn name_too_long() -> sys::Error {
sys::Error::from_code(sys::E::ENAMETOOLONG, sys::Tag::link)
}

impl Hardlinker {
pub(crate) fn init(
folder_dir: Fd,
Expand Down Expand Up @@ -97,25 +97,27 @@ impl Hardlinker {

// A `path.save()` ResetScope would hold `&mut Path` and keep
// `self.src`/`self.dest` exclusively borrowed for the rest of
// the iteration. Capture the saved length directly and restore
// the iteration. Capture the saved lengths directly and restore
// via `set_length` after the body (and before any error return)
// so the truncation happens on every exit.
let src_saved_len = self.src.len();
// `OsAbsPath`/`OsPath` use `CheckLength::ASSUME`, so `append`'s
// `Err(MaxPathExceeded)` arm is statically unreachable -- see
// `path_options::AssumeOk`.
self.src.append(entry.path.as_slice()).assume_ok();

let dest_saved_len = self.dest.len();
self.dest.append(entry.path.as_slice()).assume_ok();

let err: Option<sys::Error> = 'body: {
if self.src.append(entry.path.as_slice()).is_err()
|| self.dest.append(entry.path.as_slice()).is_err()
{
break 'body Some(name_too_long());
}

match entry.kind {
EntryKind::Directory => {
let _ = sys::make_path::make_path::<u16>(
if let sys::Result::Err(mkdir_err) = sys::make_path::make_path::<u16>(
&sys::Dir::cwd(),
self.dest.slice(),
);
) {
break 'body Some(mkdir_err);
}
}
EntryKind::File => {
let mut destfile_path_buf = bun_paths::w_path_buffer_pool::get();
Expand All @@ -136,6 +138,13 @@ impl Hardlinker {
dest_slice,
]
};
// The join and the NT prefix below write into the pooled buffers unchecked.
let needed_len =
dest_parts.iter().map(|part| part.len() + 1).sum::<usize>()
+ bun_paths::windows::NT_OBJECT_PREFIX.len();
if needed_len > destfile_path_buf2.len() {
break 'body Some(name_too_long());
}
let joined = bun_paths::resolve_path::join_string_buf_w_same::<
bun_paths::platform::Windows,
>(
Expand Down Expand Up @@ -246,12 +255,19 @@ impl Hardlinker {
// `len()` and restore via `set_length()` after the body so the
// truncation runs on every exit.
let dest_saved_len = self.dest.len();
let _ = self.dest.append(entry.path.as_bytes()); // OOM/capacity: fire-and-forget

let err: Option<sys::Error> = 'body: {
if self.dest.append(entry.path.as_bytes()).is_err() {
break 'body Some(name_too_long());
}

match entry.kind {
EntryKind::Directory => {
let _ = Fd::cwd().make_path(self.dest.slice());
if let sys::Result::Err(mkdir_err) =
Fd::cwd().make_path(self.dest.slice())
{
break 'body Some(mkdir_err);
}
}
EntryKind::File => {
match sys::linkat(
Expand Down
16 changes: 8 additions & 8 deletions src/install/isolated_install/Installer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -917,8 +917,8 @@ impl Task {

let mut hardlinker = Hardlinker::init(
folder_dir,
src,
dest,
src.into_checked(),
dest.into_checked(),
&[bun_paths::os_path_literal!("node_modules")],
)?;

Expand Down Expand Up @@ -1029,8 +1029,8 @@ impl Task {

let mut file_copier = FileCopier::init(
folder_dir,
src_path.into_sep::<{ PathSeparators::AUTO }>(),
dest.into_sep::<{ PathSeparators::AUTO }>(),
src_path.into_checked(),
dest.into_checked(),
&[bun_paths::os_path_literal!("node_modules")],
)?;

Expand Down Expand Up @@ -1338,8 +1338,8 @@ impl Task {

let mut hardlinker = Hardlinker::init(
cached_package_dir.unwrap(),
src,
dest_subpath,
src.into_checked(),
dest_subpath.into_checked(),
&[],
)?;

Expand Down Expand Up @@ -1420,8 +1420,8 @@ impl Task {

let mut file_copier = FileCopier::init(
cached_package_dir.unwrap(),
src_path.into_sep::<{ PathSeparators::AUTO }>(),
dest_subpath.into_sep::<{ PathSeparators::AUTO }>(),
src_path.into_checked(),
dest_subpath.into_checked(),
&[],
)?;

Expand Down
18 changes: 16 additions & 2 deletions src/paths/Path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub mod options {
}

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub(crate) enum CheckLength {
pub enum CheckLength {
AssumeAlwaysLessThanMaxPath,
CheckForGreaterThanMaxPath,
}
Expand Down Expand Up @@ -80,7 +80,8 @@ pub mod options {
}
impl CheckLength {
pub(crate) const ASSUME: u8 = 0;
pub(crate) const CHECK: u8 = 1;
/// For paths that get unbounded input appended; see also [`Path::into_checked`].
pub const CHECK: u8 = 1;
#[inline(always)]
pub(crate) const fn from_u8(v: u8) -> Self {
if v == 0 {
Expand Down Expand Up @@ -852,6 +853,19 @@ impl<U: PathUnit, const KIND: u8, const SEP_OPT: u8, const CHECK: u8>
/// nominally distinct, hence this explicit conversion.
#[inline]
pub fn into_sep<const NEW_SEP: u8>(self) -> Path<U, KIND, NEW_SEP, CHECK> {
self.reinterpret()
}

/// [`Self::into_sep`] for `CHECK`: from here on over-long input is `Err(MaxPathExceeded)`.
#[inline]
pub fn into_checked(self) -> Path<U, KIND, SEP_OPT, { CheckLength::CHECK }> {
self.reinterpret()
}

#[inline]
fn reinterpret<const NEW_SEP: u8, const NEW_CHECK: u8>(
self,
) -> Path<U, KIND, NEW_SEP, NEW_CHECK> {
// Explicit field move (not `transmute`): `Path`/`Buf` are `repr(Rust)`, so
// Rust gives no layout-compat guarantee between distinct const-generic
// instantiations. Rebuilding field-by-field is layout-agnostic and
Expand Down
3 changes: 1 addition & 2 deletions src/paths/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,8 +436,7 @@ pub use env_path::{EnvPath, EnvPathInput, PathComponentBuilder};
// ──────────────────────────────────────────────────────────────────────────
pub mod windows {
/// `\??\` — NT object-manager prefix (UTF-16).
pub(crate) const NT_OBJECT_PREFIX: [u16; 4] =
['\\' as u16, '?' as u16, '?' as u16, '\\' as u16];
pub const NT_OBJECT_PREFIX: [u16; 4] = ['\\' as u16, '?' as u16, '?' as u16, '\\' as u16];
/// `\??\UNC\` — NT object-manager UNC prefix (UTF-16).
pub(crate) const NT_UNC_OBJECT_PREFIX: [u16; 8] = [
'\\' as u16,
Expand Down
Loading
Loading