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
5 changes: 3 additions & 2 deletions src/install/TarballStream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1376,14 +1376,15 @@ fn make_directory(entry: &mut lib::Entry, dest_fd: Fd, path: OSPathZ, path_slice
if (mode & 0o4) != 0 {
mode |= 0o1;
}
let mode = Mode::try_from(mode).expect("int cast");
#[cfg(windows)]
{
let _ = bun_sys::make_path::make_path::<u16>(Dir::borrow(&dest_fd), &path[..]);
let _ = (path_slice, mode);
}
#[cfg(not(windows))]
{
match bun_sys::mkdirat_z(dest_fd, path, Mode::try_from(mode).expect("int cast")) {
match bun_sys::mkdirat_z(dest_fd, path, mode) {
Ok(()) => {}
Err(e) => match e.get_errno() {
bun_sys::E::EEXIST | bun_sys::E::ENOTDIR => {}
Expand All @@ -1392,7 +1393,7 @@ fn make_directory(entry: &mut lib::Entry, dest_fd: Fd, path: OSPathZ, path_slice
return;
};
let _ = dest_fd.make_path(dir);
let _ = bun_sys::mkdirat_z(dest_fd, path, 0o777);
let _ = bun_sys::mkdirat_z(dest_fd, path, mode);
}
},
}
Expand Down
18 changes: 13 additions & 5 deletions src/install/lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1998,7 +1998,19 @@ impl Lockfile {
ZStr::from_buf(&tmpname_buf, written - 1)
};

let file = match File::openat(Fd::cwd(), tmpname, sys::O::CREAT | sys::O::WRONLY, 0o777) {
// Create the temp lockfile with its final permissions; fchmod below
// keeps the existing post-write permission normalization.
let filemode: sys::Mode = if save_format == LockfileFormat::Text {
0o644
} else {
0o755
};
let file = match File::openat(
Fd::cwd(),
tmpname,
sys::O::CREAT | sys::O::WRONLY,
filemode,
) {
sys::Result::Err(e) => {
Output::err(
e,
Expand All @@ -2023,10 +2035,6 @@ impl Lockfile {
#[cfg(unix)]
{
// chmod 755 for binary, 644 for plaintext
let mut filemode: sys::Mode = 0o755;
if save_format == LockfileFormat::Text {
filemode = 0o644;
}
match sys::fchmod(file.handle, filemode) {
sys::Result::Err(e) => {
let _ = file.close(); // close error is non-actionable
Expand Down
6 changes: 5 additions & 1 deletion src/libarchive/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1937,7 +1937,11 @@ impl Archiver {
return Err(err.into());
}
let _ = dir.make_path_u8(dirname);
let _ = bun_sys::mkdirat_z(dir_fd, path_z, 0o777);
let _ = bun_sys::mkdirat_z(
dir_fd,
path_z,
bun_sys::Mode::try_from(mode).expect("int cast"),
);
}
}
}
Expand Down
40 changes: 32 additions & 8 deletions test/js/bun/archive.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
import { existsSync, readdirSync, rmSync } from "node:fs";
import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
import { join } from "path";

// Minimal ustar tarball builder (pathnames must be <100 bytes).
function ustarHeader(name: string, size: number): Buffer {
function ustarHeader(name: string, size: number, mode = 0o644, type: "0" | "5" = "0"): Buffer {
if (Buffer.byteLength(name) > 99) throw new Error("ustar name too long: " + name);
const h = Buffer.alloc(512);
h.write(name, 0, 100, "utf8");
h.write("0000644\0", 100);
h.write(mode.toString(8).padStart(7, "0") + "\0", 100);
h.write("0000000\0", 108);
h.write("0000000\0", 116);
h.write(size.toString(8).padStart(11, "0") + "\0", 124);
h.write("00000000000\0", 136);
h.write(" ", 148);
h.write("0", 156);
h.write(type, 156);
h.write("ustar\0", 257);
h.write("00", 263);
let sum = 0;
Expand All @@ -23,13 +23,22 @@ function ustarHeader(name: string, size: number): Buffer {
return h;
}

function ustarEntry(name: string, data: Buffer): Buffer {
function ustarEntry(name: string, data: Buffer, mode?: number, type?: "0" | "5"): Buffer {
const pad = Buffer.alloc((512 - (data.length % 512)) % 512);
return Buffer.concat([ustarHeader(name, data.length), data, pad]);
return Buffer.concat([ustarHeader(name, data.length, mode, type), data, pad]);
}

function buildTarball(entries: Array<{ name: string; data: Buffer | string }>): Uint8Array {
const parts = entries.map(e => ustarEntry(e.name, typeof e.data === "string" ? Buffer.from(e.data) : e.data));
function buildTarball(
entries: Array<{ name: string; data?: Buffer | string; mode?: number; type?: "0" | "5" }>,
): Uint8Array {
const parts = entries.map(e =>
ustarEntry(
e.name,
typeof e.data === "string" ? Buffer.from(e.data) : (e.data ?? Buffer.alloc(0)),
e.mode,
e.type,
),
);
parts.push(Buffer.alloc(1024));
return new Uint8Array(Buffer.concat(parts));
}
Expand Down Expand Up @@ -498,6 +507,21 @@ describe("Bun.Archive", () => {
expect(content).toBe("Hello, World!");
});

test.skipIf(isWindows)("preserves directory mode after creating missing parents", async () => {
using dir = tempDir("archive-extract-dir-mode", {});
const tarball = buildTarball([{ name: "private/nested", mode: 0o700, type: "5" }]);
const archive = new Bun.Archive(tarball);

const previousUmask = process.umask(0);
try {
await archive.extract(String(dir));
} finally {
process.umask(previousUmask);
}

expect(statSync(join(String(dir), "private", "nested")).mode & 0o777).toBe(0o700);
});

test("throws when extracting to a file path instead of directory", async () => {
using dir = tempDir("archive-extract-to-file", {
"existing-file.txt": "I am a file",
Expand Down