mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Compare commits
4 Commits
bun-v1.3.4
...
claude/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e571779641 | ||
|
|
c60efbc8c4 | ||
|
|
70016db0f6 | ||
|
|
e6a0ad6733 |
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user