mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(io): respect mode option when copying files with Bun.write() (#25906)
## Summary - Fixes #25903 - `Bun.write()` mode option ignored when copying from `Bun.file()` - The destination file now correctly uses the specified `mode` option instead of default permissions - Works on Linux (via open flags), macOS (chmod after clonefile), and Windows (chmod after copyfile) ## Test plan - [x] Added regression test in `test/regression/issue/25903.test.ts` - [x] Test passes with `bun bd test test/regression/issue/25903.test.ts` - [x] Test fails with `USE_SYSTEM_BUN=1 bun test test/regression/issue/25903.test.ts` (verifies the bug exists) ## Changes - `src/bun.js/webcore/Blob.zig`: Add `mode` field to `WriteFileOptions` and parse from options - `src/bun.js/webcore/blob/copy_file.zig`: Use `destination_mode` in `CopyFile` struct and `doOpenFile` - `packages/bun-types/bun.d.ts`: Add `mode` option to BunFile copy overloads 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
147
test/regression/issue/25903.test.ts
Normal file
147
test/regression/issue/25903.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { isWindows, tempDir } from "harness";
|
||||
import { stat } from "node:fs/promises";
|
||||
|
||||
// Tests for issue #25903: Bun.write() mode option when copying files using Bun.file()
|
||||
// The mode option is respected when copying files via Bun.file() as the source.
|
||||
// These tests are skipped on Windows where Unix-style file permissions don't apply.
|
||||
|
||||
describe.skipIf(isWindows)("Bun.write() mode option", () => {
|
||||
test("Bun.write() respects mode option when copying files via Bun.file()", async () => {
|
||||
using dir = tempDir("issue-25903", {});
|
||||
const sourcePath = `${dir}/source.txt`;
|
||||
const destPath = `${dir}/dest.txt`;
|
||||
|
||||
// Create source file (mode is determined by the write path's default behavior)
|
||||
await Bun.write(sourcePath, "hello world");
|
||||
|
||||
// Get source file's actual permissions to verify they differ from what we'll set
|
||||
const sourceStat = await stat(sourcePath);
|
||||
const sourceMode = sourceStat.mode & 0o777;
|
||||
|
||||
// Copy using Bun.file() with specific 0o600 permissions (more restrictive)
|
||||
// The mode option is honored for Bun.file() copy operations
|
||||
await Bun.write(destPath, Bun.file(sourcePath), { mode: 0o600 });
|
||||
|
||||
// Verify destination file has the specified permissions, not inherited from source
|
||||
const destStat = await stat(destPath);
|
||||
expect(destStat.mode & 0o777).toBe(0o600);
|
||||
// Also verify it's different from source (unless source happened to be 0o600)
|
||||
if (sourceMode !== 0o600) {
|
||||
expect(destStat.mode & 0o777).not.toBe(sourceMode);
|
||||
}
|
||||
});
|
||||
|
||||
test("Bun.write() respects mode option with createPath when copying via Bun.file()", async () => {
|
||||
using dir = tempDir("issue-25903-createPath", {});
|
||||
const sourcePath = `${dir}/source.txt`;
|
||||
const destPath = `${dir}/subdir/dest.txt`;
|
||||
|
||||
// Create source file
|
||||
await Bun.write(sourcePath, "hello world");
|
||||
|
||||
// Copy using Bun.file() to a path that requires directory creation, with specific permissions
|
||||
await Bun.write(destPath, Bun.file(sourcePath), { mode: 0o755, createPath: true });
|
||||
|
||||
// Verify destination file has the specified permissions
|
||||
const destStat = await stat(destPath);
|
||||
expect(destStat.mode & 0o777).toBe(0o755);
|
||||
});
|
||||
|
||||
test("Bun.write() uses default permissions when mode is not specified for Bun.file() copy", async () => {
|
||||
using dir = tempDir("issue-25903-default", {});
|
||||
const sourcePath = `${dir}/source.txt`;
|
||||
const destPath = `${dir}/dest.txt`;
|
||||
const baselinePath = `${dir}/baseline.txt`;
|
||||
|
||||
// Create source file
|
||||
await Bun.write(sourcePath, "hello world");
|
||||
|
||||
// Create a baseline file using default permissions (to determine what the default is)
|
||||
await Bun.write(baselinePath, "baseline");
|
||||
const baselineStat = await stat(baselinePath);
|
||||
const defaultMode = baselineStat.mode & 0o777;
|
||||
|
||||
// Copy using Bun.file() without specifying mode - should use default permissions
|
||||
await Bun.write(destPath, Bun.file(sourcePath));
|
||||
|
||||
// When mode is not specified, the default permission is used (same as creating a new file)
|
||||
// This test verifies that the destination doesn't inherit source permissions incorrectly
|
||||
const destStat = await stat(destPath);
|
||||
expect(destStat.mode & 0o777).toBe(defaultMode);
|
||||
});
|
||||
|
||||
test("Bun.write() respects mode when writing to PathLike from BunFile", async () => {
|
||||
using dir = tempDir("issue-25903-pathlike", {});
|
||||
const sourcePath = `${dir}/source.txt`;
|
||||
const destPath = `${dir}/dest.txt`;
|
||||
|
||||
// Create source file
|
||||
await Bun.write(sourcePath, "test content");
|
||||
|
||||
// Write with specific mode using path string as destination and Bun.file() as source
|
||||
await Bun.write(destPath, Bun.file(sourcePath), { mode: 0o700 });
|
||||
|
||||
const destStat = await stat(destPath);
|
||||
expect(destStat.mode & 0o777).toBe(0o700);
|
||||
});
|
||||
|
||||
test("Bun.write() respects mode when both destination and source are BunFile", async () => {
|
||||
using dir = tempDir("issue-25903-bunfile-dest", {});
|
||||
const sourcePath = `${dir}/source.txt`;
|
||||
const destPath = `${dir}/dest.txt`;
|
||||
|
||||
// Create source file
|
||||
await Bun.write(sourcePath, "test content for bunfile dest");
|
||||
|
||||
// Write with specific mode using Bun.file() as both destination and source
|
||||
await Bun.write(Bun.file(destPath), Bun.file(sourcePath), { mode: 0o700 });
|
||||
|
||||
const destStat = await stat(destPath);
|
||||
expect(destStat.mode & 0o777).toBe(0o700);
|
||||
});
|
||||
|
||||
test("Bun.write() respects mode when overwriting an existing file", async () => {
|
||||
using dir = tempDir("issue-25903-overwrite", {});
|
||||
const sourcePath = `${dir}/source.txt`;
|
||||
const destPath = `${dir}/dest.txt`;
|
||||
|
||||
// Create source file
|
||||
await Bun.write(sourcePath, "source content");
|
||||
|
||||
// Create destination file with default permissions
|
||||
await Bun.write(destPath, "original content");
|
||||
const originalStat = await stat(destPath);
|
||||
const originalMode = originalStat.mode & 0o777;
|
||||
|
||||
// Overwrite destination with different mode - should update permissions even for existing file
|
||||
await Bun.write(destPath, Bun.file(sourcePath), { mode: 0o600 });
|
||||
|
||||
const destStat = await stat(destPath);
|
||||
expect(destStat.mode & 0o777).toBe(0o600);
|
||||
// Verify the mode actually changed (unless original happened to be 0o600)
|
||||
if (originalMode !== 0o600) {
|
||||
expect(destStat.mode & 0o777).not.toBe(originalMode);
|
||||
}
|
||||
});
|
||||
|
||||
test("Bun.write() accepts mode: 0 (no permissions)", async () => {
|
||||
using dir = tempDir("issue-25903-mode-zero", {});
|
||||
const sourcePath = `${dir}/source.txt`;
|
||||
const destPath = `${dir}/dest.txt`;
|
||||
|
||||
// Create source file
|
||||
await Bun.write(sourcePath, "test content");
|
||||
|
||||
// Write with mode 0 (no permissions) - this should be accepted, not treated as "not specified"
|
||||
await Bun.write(destPath, Bun.file(sourcePath), { mode: 0o000 });
|
||||
|
||||
const destStat = await stat(destPath);
|
||||
expect(destStat.mode & 0o777).toBe(0o000);
|
||||
});
|
||||
}); // end describe.skipIf(isWindows)
|
||||
|
||||
// Note: The mode option is fully respected for Bun.file() copy operations (the fix for #25903).
|
||||
// For direct string/buffer writes, mode support depends on the write path used internally.
|
||||
// The empty file creation path respects mode, but other direct write paths may use
|
||||
// the default permission (0o664). The tests above validate the Bun.file() copy path.
|
||||
Reference in New Issue
Block a user