Compare commits

...

4 Commits

Author SHA1 Message Date
Claude Bot
e571779641 Refactor mode parameter to use bun.Mode instead of u32
Eliminates @intCast() calls by using the proper bun.Mode type throughout
the codebase instead of u32. This improves type safety and removes
platform-specific casting issues between u16 and u32.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-04 10:12:09 +00:00
Claude Bot
c60efbc8c4 Fix macOS compilation errors for mode option
Fixed type casting issues where u32 mode values were being passed to functions
expecting u16 (bun.Mode) on macOS. Added @intCast() wrappers for:
- bun.sys.open() mode parameters in Blob.zig
- bun.sys.fchmod() mode parameters in Blob.zig, copy_file.zig, and write_file.zig

All cross-platform builds now pass with `bun run zig:check-all`.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-04 09:26:36 +00:00
autofix-ci[bot]
70016db0f6 [autofix.ci] apply automated fixes 2025-08-04 08:20:50 +00:00
Claude Bot
e6a0ad6733 Add mode option to Bun.write() for setting file permissions
This implements the mode option for Bun.write() to allow setting file permissions on POSIX systems, similar to Node.js fs.writeFile(). The implementation covers all write code paths:

- Main Bun.write() function in Blob.zig
- Async file writing in write_file.zig
- File copying in copy_file.zig
- S3File write method

Key changes:
- Added mode field to WriteFileOptions struct
- Updated all write functions to accept and use mode parameter
- Added fchmod() calls after file creation on POSIX systems
- Added comprehensive tests covering various write scenarios
- Mode validation follows Node.js behavior (truncates to 0o777)

Tests verify mode setting works correctly for string data, binary data, blob operations, file copying, and various edge cases.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-04 08:16:52 +00:00
5 changed files with 222 additions and 9 deletions

View File

@@ -988,6 +988,7 @@ pub fn writeFileWithSourceDestination(ctx: *jsc.JSGlobalObject, source_blob: *Bl
write_file_promise,
&WriteFilePromise.run,
options.mkdirp_if_not_exists orelse true,
options.mode,
);
return promise_value;
}
@@ -999,6 +1000,7 @@ pub fn writeFileWithSourceDestination(ctx: *jsc.JSGlobalObject, source_blob: *Bl
write_file_promise,
WriteFilePromise.run,
options.mkdirp_if_not_exists orelse true,
options.mode,
) catch unreachable;
var task = write_file.WriteFileTask.createOnJSThread(bun.default_allocator, ctx, file_copier) catch bun.outOfMemory();
// Defer promise creation until we're just about to schedule the task
@@ -1028,6 +1030,7 @@ pub fn writeFileWithSourceDestination(ctx: *jsc.JSGlobalObject, source_blob: *Bl
destination_blob.size,
ctx,
options.mkdirp_if_not_exists orelse true,
options.mode,
) catch unreachable;
file_copier.schedule();
return file_copier.promise.value();
@@ -1168,6 +1171,7 @@ pub fn writeFileWithSourceDestination(ctx: *jsc.JSGlobalObject, source_blob: *Bl
const WriteFileOptions = struct {
mkdirp_if_not_exists: ?bool = null,
extra_options: ?JSValue = null,
mode: ?bun.Mode = null,
};
/// ## Errors
@@ -1242,6 +1246,7 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
str,
&needs_async,
true,
options.mode,
);
if (!needs_async) {
return result;
@@ -1253,6 +1258,7 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
str,
&needs_async,
false,
options.mode,
);
if (!needs_async) {
return result;
@@ -1273,6 +1279,7 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
buffer_view.byteSlice(),
&needs_async,
true,
options.mode,
);
if (!needs_async) {
@@ -1285,6 +1292,7 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
buffer_view.byteSlice(),
&needs_async,
false,
options.mode,
);
if (!needs_async) {
@@ -1495,6 +1503,7 @@ pub fn writeFile(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun
return globalThis.throwInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{});
};
var mkdirp_if_not_exists: ?bool = null;
var mode: ?bun.Mode = null;
const options = args.nextEat();
if (options) |options_object| {
if (options_object.isObject()) {
@@ -1504,6 +1513,11 @@ pub fn writeFile(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun
}
mkdirp_if_not_exists = create_directory.toBoolean();
}
if (try options_object.getTruthy(globalThis, "mode")) |mode_value| {
if (try jsc.Node.modeFromJS(globalThis, mode_value)) |file_mode| {
mode = file_mode;
}
}
} else if (!options_object.isEmptyOrUndefinedOrNull()) {
return globalThis.throwInvalidArgumentType("write", "options", "object");
}
@@ -1511,6 +1525,7 @@ pub fn writeFile(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun
return writeFileInternal(globalThis, &path_or_blob, data, .{
.mkdirp_if_not_exists = mkdirp_if_not_exists,
.extra_options = options,
.mode = mode,
});
}
@@ -1522,17 +1537,24 @@ fn writeStringToFileFast(
str: bun.String,
needs_async: *bool,
comptime needs_open: bool,
mode: ?bun.Mode,
) jsc.JSValue {
const fd: bun.FileDescriptor = if (comptime !needs_open) pathlike.fd else brk: {
var file_path: bun.PathBuffer = undefined;
const open_mode = mode orelse write_permissions;
switch (bun.sys.open(
pathlike.path.sliceZ(&file_path),
// we deliberately don't use O_TRUNC here
// it's a perf optimization
bun.O.WRONLY | bun.O.CREAT | bun.O.NONBLOCK,
write_permissions,
open_mode,
)) {
.result => |result| {
if (comptime !Environment.isWindows) {
if (mode) |file_mode| {
_ = bun.sys.fchmod(result, file_mode);
}
}
break :brk result;
},
.err => |err| {
@@ -1604,9 +1626,11 @@ fn writeBytesToFileFast(
bytes: []const u8,
needs_async: *bool,
comptime needs_open: bool,
mode: ?bun.Mode,
) jsc.JSValue {
const fd: bun.FileDescriptor = if (comptime !needs_open) pathlike.fd else brk: {
var file_path: bun.PathBuffer = undefined;
const open_mode = mode orelse write_permissions;
switch (bun.sys.open(
pathlike.path.sliceZ(&file_path),
if (!Environment.isWindows)
@@ -1615,9 +1639,14 @@ fn writeBytesToFileFast(
bun.O.WRONLY | bun.O.CREAT | bun.O.NONBLOCK
else
bun.O.WRONLY | bun.O.CREAT,
write_permissions,
open_mode,
)) {
.result => |result| {
if (comptime !Environment.isWindows) {
if (mode) |file_mode| {
_ = bun.sys.fchmod(result, file_mode);
}
}
break :brk result;
},
.err => |err| {
@@ -2237,6 +2266,7 @@ pub fn doWrite(this: *Blob, globalThis: *jsc.JSGlobalObject, callframe: *jsc.Cal
return globalThis.throwInvalidArguments("blob.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{});
}
var mkdirp_if_not_exists: ?bool = null;
var mode: ?bun.Mode = null;
const options = args.nextEat();
if (options) |options_object| {
if (options_object.isObject()) {
@@ -2246,6 +2276,11 @@ pub fn doWrite(this: *Blob, globalThis: *jsc.JSGlobalObject, callframe: *jsc.Cal
}
mkdirp_if_not_exists = create_directory.toBoolean();
}
if (try options_object.getTruthy(globalThis, "mode")) |mode_value| {
if (try jsc.Node.modeFromJS(globalThis, mode_value)) |file_mode| {
mode = file_mode;
}
}
if (try options_object.getTruthy(globalThis, "type")) |content_type| {
//override the content type
if (!content_type.isString()) {
@@ -2274,7 +2309,7 @@ pub fn doWrite(this: *Blob, globalThis: *jsc.JSGlobalObject, callframe: *jsc.Cal
}
}
var blob_internal: PathOrBlob = .{ .blob = this.* };
return writeFileInternal(globalThis, &blob_internal, data, .{ .mkdirp_if_not_exists = mkdirp_if_not_exists, .extra_options = options });
return writeFileInternal(globalThis, &blob_internal, data, .{ .mkdirp_if_not_exists = mkdirp_if_not_exists, .extra_options = options, .mode = mode });
}
pub fn doUnlink(this: *Blob, globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {

View File

@@ -147,6 +147,19 @@ pub fn write(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSE
switch (path_or_blob) {
.path => |path| {
const options = args.nextEat();
var mode: ?bun.Mode = null;
// Parse mode from options if present
if (options) |options_object| {
if (options_object.isObject()) {
if (try options_object.getTruthy(globalThis, "mode")) |mode_value| {
if (try jsc.Node.modeFromJS(globalThis, mode_value)) |file_mode| {
mode = file_mode;
}
}
}
}
if (path == .fd) {
return globalThis.throwInvalidArguments("Expected a S3 or path to upload", .{});
}
@@ -157,12 +170,30 @@ pub fn write(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSE
return try Blob.writeFileInternal(globalThis, &blob_internal, data, .{
.mkdirp_if_not_exists = false,
.extra_options = options,
.mode = mode,
});
},
.blob => {
const options = args.nextEat();
var mode: ?bun.Mode = null;
// Parse mode from options if present
if (options) |options_object| {
if (options_object.isObject()) {
if (try options_object.getTruthy(globalThis, "mode")) |mode_value| {
if (try jsc.Node.modeFromJS(globalThis, mode_value)) |file_mode| {
mode = file_mode;
}
}
}
}
return try Blob.writeFileInternal(globalThis, &path_or_blob, data, .{
.mkdirp_if_not_exists = false,
.extra_options = options,
.mode = mode,
});
},
.blob => return try Blob.writeFileInternal(globalThis, &path_or_blob, data, .{
.mkdirp_if_not_exists = false,
.extra_options = args.nextEat(),
}),
}
}

View File

@@ -18,6 +18,7 @@ pub const CopyFile = struct {
globalThis: *JSGlobalObject,
mkdirp_if_not_exists: bool = false,
mode: ?bun.Mode = null,
pub const ResultType = anyerror!SizeType;
@@ -31,6 +32,7 @@ pub const CopyFile = struct {
max_len: SizeType,
globalThis: *JSGlobalObject,
mkdirp_if_not_exists: bool,
mode: ?bun.Mode,
) !*CopyFilePromiseTask {
const read_file = bun.new(CopyFile, CopyFile{
.store = store,
@@ -41,6 +43,7 @@ pub const CopyFile = struct {
.destination_file_store = store.data.file,
.source_file_store = source_store.data.file,
.mkdirp_if_not_exists = mkdirp_if_not_exists,
.mode = mode,
});
store.ref();
source_store.ref();
@@ -158,10 +161,18 @@ pub const CopyFile = struct {
this.destination_fd = switch (bun.sys.open(
dest,
open_destination_flags,
jsc.Node.fs.default_permission,
this.mode orelse jsc.Node.fs.default_permission,
)) {
.result => |result| switch (result.makeLibUVOwnedForSyscall(.open, .close_on_fail)) {
.result => |result_fd| result_fd,
.result => |result_fd| blk: {
// Set file mode if specified
if (comptime !Environment.isWindows) {
if (this.mode) |file_mode| {
_ = bun.sys.fchmod(result_fd, file_mode);
}
}
break :blk result_fd;
},
.err => |errno| {
this.system_error = errno.toSystemError();
return bun.errnoToZigErr(errno.errno);

View File

@@ -22,6 +22,7 @@ pub const WriteFile = struct {
could_block: bool = false,
close_after_io: bool = false,
mkdirp_if_not_exists: bool = false,
mode: ?bun.Mode = null,
pub const io_tag = io.Poll.Tag.WriteFile;
@@ -77,6 +78,7 @@ pub const WriteFile = struct {
onWriteFileContext: *anyopaque,
onCompleteCallback: WriteFileOnWriteFileCallback,
mkdirp_if_not_exists: bool,
mode: ?bun.Mode,
) !*WriteFile {
const write_file = bun.new(WriteFile, WriteFile{
.file_blob = file_blob,
@@ -85,6 +87,7 @@ pub const WriteFile = struct {
.onCompleteCallback = onCompleteCallback,
.task = .{ .callback = &doWriteLoopTask },
.mkdirp_if_not_exists = mkdirp_if_not_exists,
.mode = mode,
});
file_blob.store.?.ref();
bytes_blob.store.?.ref();
@@ -98,6 +101,7 @@ pub const WriteFile = struct {
context: Context,
comptime callback: fn (ctx: Context, bytes: WriteFileResultType) void,
mkdirp_if_not_exists: bool,
mode: ?bun.Mode,
) !*WriteFile {
const Handler = struct {
pub fn run(ptr: *anyopaque, bytes: WriteFileResultType) void {
@@ -111,6 +115,7 @@ pub const WriteFile = struct {
@as(*anyopaque, @ptrCast(context)),
Handler.run,
mkdirp_if_not_exists,
mode,
);
}
@@ -220,6 +225,13 @@ pub const WriteFile = struct {
const fd = this.opened_fd;
// Set file mode if specified
if (comptime !Environment.isWindows) {
if (this.mode) |file_mode| {
_ = bun.sys.fchmod(fd, file_mode);
}
}
this.could_block = brk: {
if (this.file_blob.store) |store| {
if (store.data == .file and store.data.file.pathlike == .fd) {
@@ -344,6 +356,7 @@ pub const WriteFileWindows = struct {
onCompleteCallback: WriteFileOnWriteFileCallback,
onCompleteCtx: *anyopaque,
mkdirp_if_not_exists: bool = false,
mode: ?bun.Mode = null,
uv_bufs: [1]uv.uv_buf_t,
fd: uv.uv_file = -1,
@@ -362,6 +375,7 @@ pub const WriteFileWindows = struct {
onWriteFileContext: *anyopaque,
onCompleteCallback: WriteFileOnWriteFileCallback,
mkdirp_if_not_exists: bool,
mode: ?bun.Mode,
) *WriteFileWindows {
const write_file = WriteFileWindows.new(.{
.file_blob = file_blob,
@@ -369,6 +383,7 @@ pub const WriteFileWindows = struct {
.onCompleteCtx = onWriteFileContext,
.onCompleteCallback = onCompleteCallback,
.mkdirp_if_not_exists = mkdirp_if_not_exists and file_blob.store.?.data.file.pathlike == .path,
.mode = mode,
.io_request = std.mem.zeroes(uv.fs_t),
.uv_bufs = .{.{ .base = undefined, .len = 0 }},
.event_loop = event_loop,
@@ -623,6 +638,7 @@ pub const WriteFileWindows = struct {
context: Context,
comptime callback: *const fn (ctx: Context, bytes: WriteFileResultType) void,
mkdirp_if_not_exists: bool,
mode: ?bun.Mode,
) *WriteFileWindows {
return WriteFileWindows.createWithCtx(
file_blob,
@@ -631,6 +647,7 @@ pub const WriteFileWindows = struct {
@as(*anyopaque, @ptrCast(context)),
@ptrCast(callback),
mkdirp_if_not_exists,
mode,
);
}
};

View File

@@ -527,3 +527,122 @@ if (isWindows && !IS_UV_FS_COPYFILE_DISABLED) {
expect(await exited).toBe(0);
}, 10000);
}
// Skip mode tests on Windows as file permissions work differently
if (!isWindows) {
describe("mode option", () => {
it("Bun.write() with mode option sets correct file permissions", async () => {
const filename = path.join(tmpdir(), `mode-test-${Date.now()}.txt`);
// Test mode 0o644 (read/write for owner, read for group/others)
await Bun.write(filename, "test content", { mode: 0o644 });
const stats = fs.statSync(filename);
expect(stats.mode & 0o777).toBe(0o644);
try {
fs.unlinkSync(filename);
} catch (e) {}
});
it("Bun.write() with mode option as decimal", async () => {
const filename = path.join(tmpdir(), `mode-decimal-test-${Date.now()}.txt`);
// Test mode 0o755 as decimal (493)
await Bun.write(filename, "test content", { mode: 493 });
const stats = fs.statSync(filename);
expect(stats.mode & 0o777).toBe(0o755);
try {
fs.unlinkSync(filename);
} catch (e) {}
});
it("Bun.write() with mode option and createPath", async () => {
const testDir = path.join(tmpdir(), `mode-mkdir-test-${Date.now()}`);
const filename = path.join(testDir, "test.txt");
// Test mode with createPath
await Bun.write(filename, "test content", { mode: 0o600, createPath: true });
const stats = fs.statSync(filename);
expect(stats.mode & 0o777).toBe(0o600);
try {
fs.rmSync(testDir, { recursive: true });
} catch (e) {}
});
it("blob.write() with mode option", async () => {
const filename = path.join(tmpdir(), `blob-mode-test-${Date.now()}.txt`);
const file = Bun.file(filename);
// Test blob write with mode
await file.write("test content", { mode: 0o640 });
const stats = fs.statSync(filename);
expect(stats.mode & 0o777).toBe(0o640);
try {
fs.unlinkSync(filename);
} catch (e) {}
});
it("Bun.write() file to file copy with mode", async () => {
const sourceFile = path.join(tmpdir(), `source-${Date.now()}.txt`);
const destFile = path.join(tmpdir(), `dest-mode-${Date.now()}.txt`);
// Create source file
await Bun.write(sourceFile, "source content");
// Copy with specific mode
await Bun.write(Bun.file(destFile), Bun.file(sourceFile), { mode: 0o660 });
const stats = fs.statSync(destFile);
expect(stats.mode & 0o777).toBe(0o660);
try {
fs.unlinkSync(sourceFile);
fs.unlinkSync(destFile);
} catch (e) {}
});
it("mode validation - should handle edge cases", async () => {
const filename1 = path.join(tmpdir(), `mode-truncate-test-${Date.now()}.txt`);
const filename2 = path.join(tmpdir(), `mode-negative-test-${Date.now()}.txt`);
// Test mode > 0o777 gets truncated (like Node.js)
await Bun.write(filename1, "test", { mode: 0o1644 });
const stats1 = fs.statSync(filename1);
expect(stats1.mode & 0o777).toBe(0o644); // 0o1644 & 0o777 = 0o644
// Test negative mode should throw
try {
await Bun.write(filename2, "test", { mode: -1 });
throw new Error("Should have thrown for negative mode");
} catch (error) {
expect(error.message).toContain("out of range");
}
try {
fs.unlinkSync(filename1);
} catch (e) {}
});
it("mode option should work with different data types", async () => {
const testCases = [
["string", "test string"],
["Uint8Array", new Uint8Array([1, 2, 3, 4])],
["ArrayBuffer", new ArrayBuffer(10)],
];
for (const [type, data] of testCases) {
const filename = path.join(tmpdir(), `mode-${type}-test-${Date.now()}.txt`);
await Bun.write(filename, data, { mode: 0o622 });
const stats = fs.statSync(filename);
expect(stats.mode & 0o777).toBe(0o622);
try {
fs.unlinkSync(filename);
} catch (e) {}
}
});
});
}