diff --git a/src/install/TarballStream.rs b/src/install/TarballStream.rs index 75492719dbb..c49d7574aac 100644 --- a/src/install/TarballStream.rs +++ b/src/install/TarballStream.rs @@ -1376,6 +1376,7 @@ 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::(Dir::borrow(&dest_fd), &path[..]); @@ -1383,7 +1384,7 @@ fn make_directory(entry: &mut lib::Entry, dest_fd: Fd, path: OSPathZ, path_slice } #[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 => {} @@ -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); } }, } diff --git a/src/install/lockfile.rs b/src/install/lockfile.rs index 7bf934cf81e..0810cb1925f 100644 --- a/src/install/lockfile.rs +++ b/src/install/lockfile.rs @@ -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, @@ -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 diff --git a/src/libarchive/lib.rs b/src/libarchive/lib.rs index d557519121b..fdf40ef95ef 100644 --- a/src/libarchive/lib.rs +++ b/src/libarchive/lib.rs @@ -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"), + ); } } } diff --git a/test/js/bun/archive.test.ts b/test/js/bun/archive.test.ts index 57e76c09578..0a9fa9eab63 100644 --- a/test/js/bun/archive.test.ts +++ b/test/js/bun/archive.test.ts @@ -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; @@ -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)); } @@ -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",