diff --git a/src/bun.js/api/bun/dns_resolver.zig b/src/bun.js/api/bun/dns_resolver.zig index f960f38828ab9a..8c365c2920fb09 100644 --- a/src/bun.js/api/bun/dns_resolver.zig +++ b/src/bun.js/api/bun/dns_resolver.zig @@ -2519,7 +2519,7 @@ pub const DNSResolver = struct { return globalThis.throwInvalidArgumentType("resolve", "name", "non-empty string"); } - const name = name_str.toSliceClone(globalThis, bun.default_allocator); + const name = try name_str.toSliceClone(globalThis, bun.default_allocator); switch (record_type) { RecordType.A => { @@ -2582,7 +2582,7 @@ pub const DNSResolver = struct { return globalThis.throwInvalidArgumentType("reverse", "ip", "non-empty string"); } - const ip_slice = ip_str.toSliceClone(globalThis, bun.default_allocator); + const ip_slice = try ip_str.toSliceClone(globalThis, bun.default_allocator); const ip = ip_slice.slice(); const channel: *c_ares.Channel = switch (this.getChannel()) { .result => |res| res, @@ -2718,7 +2718,7 @@ pub const DNSResolver = struct { return globalThis.throwInvalidArgumentType("resolveSrv", "hostname", "non-empty string"); } - const name = name_str.toSliceClone(globalThis, bun.default_allocator); + const name = try name_str.toSliceClone(globalThis, bun.default_allocator); return this.doResolveCAres(c_ares.struct_ares_srv_reply, "srv", name.slice(), globalThis); } @@ -2744,7 +2744,7 @@ pub const DNSResolver = struct { return .zero; }; - const name = name_str.toSliceClone(globalThis, bun.default_allocator); + const name = try name_str.toSliceClone(globalThis, bun.default_allocator); return this.doResolveCAres(c_ares.struct_ares_soa_reply, "soa", name.slice(), globalThis); } @@ -2774,7 +2774,7 @@ pub const DNSResolver = struct { return globalThis.throwInvalidArgumentType("resolveCaa", "hostname", "non-empty string"); } - const name = name_str.toSliceClone(globalThis, bun.default_allocator); + const name = try name_str.toSliceClone(globalThis, bun.default_allocator); return this.doResolveCAres(c_ares.struct_ares_caa_reply, "caa", name.slice(), globalThis); } @@ -2800,7 +2800,7 @@ pub const DNSResolver = struct { return .zero; }; - const name = name_str.toSliceClone(globalThis, bun.default_allocator); + const name = try name_str.toSliceClone(globalThis, bun.default_allocator); return this.doResolveCAres(c_ares.struct_hostent, "ns", name.slice(), globalThis); } @@ -2830,7 +2830,7 @@ pub const DNSResolver = struct { return globalThis.throwInvalidArgumentType("resolvePtr", "hostname", "non-empty string"); } - const name = name_str.toSliceClone(globalThis, bun.default_allocator); + const name = try name_str.toSliceClone(globalThis, bun.default_allocator); return this.doResolveCAres(c_ares.struct_hostent, "ptr", name.slice(), globalThis); } @@ -2860,7 +2860,7 @@ pub const DNSResolver = struct { return globalThis.throwInvalidArgumentType("resolveCname", "hostname", "non-empty string"); } - const name = name_str.toSliceClone(globalThis, bun.default_allocator); + const name = try name_str.toSliceClone(globalThis, bun.default_allocator); return this.doResolveCAres(c_ares.struct_hostent, "cname", name.slice(), globalThis); } @@ -2890,7 +2890,7 @@ pub const DNSResolver = struct { return globalThis.throwInvalidArgumentType("resolveMx", "hostname", "non-empty string"); } - const name = name_str.toSliceClone(globalThis, bun.default_allocator); + const name = try name_str.toSliceClone(globalThis, bun.default_allocator); return this.doResolveCAres(c_ares.struct_ares_mx_reply, "mx", name.slice(), globalThis); } @@ -2920,7 +2920,7 @@ pub const DNSResolver = struct { return globalThis.throwInvalidArgumentType("resolveNaptr", "hostname", "non-empty string"); } - const name = name_str.toSliceClone(globalThis, bun.default_allocator); + const name = try name_str.toSliceClone(globalThis, bun.default_allocator); return this.doResolveCAres(c_ares.struct_ares_naptr_reply, "naptr", name.slice(), globalThis); } @@ -2950,7 +2950,7 @@ pub const DNSResolver = struct { return globalThis.throwInvalidArgumentType("resolveTxt", "hostname", "non-empty string"); } - const name = name_str.toSliceClone(globalThis, bun.default_allocator); + const name = try name_str.toSliceClone(globalThis, bun.default_allocator); return this.doResolveCAres(c_ares.struct_ares_txt_reply, "txt", name.slice(), globalThis); } @@ -2980,7 +2980,7 @@ pub const DNSResolver = struct { return globalThis.throwInvalidArgumentType("resolveAny", "hostname", "non-empty string"); } - const name = name_str.toSliceClone(globalThis, bun.default_allocator); + const name = try name_str.toSliceClone(globalThis, bun.default_allocator); return this.doResolveCAres(c_ares.struct_any_reply, "any", name.slice(), globalThis); } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 8727946521c3b5..95c2ca06ec1cdb 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -610,7 +610,13 @@ pub const ZigString = extern struct { return !this.allocator.isNull(); } - pub fn clone(this: Slice, allocator: std.mem.Allocator) !Slice { + pub fn toOwned(this: Slice, allocator: std.mem.Allocator) OOM!Slice { + const duped = try allocator.dupe(u8, this.ptr[0..this.len]); + return .{ .allocator = .init(allocator), .ptr = duped.ptr, .len = this.len }; + } + + // TODO: this is identical to `cloneIfNeeded` + pub fn clone(this: Slice, allocator: std.mem.Allocator) OOM!Slice { if (this.isAllocated()) { return Slice{ .allocator = this.allocator, .ptr = this.ptr, .len = this.len }; } @@ -951,10 +957,10 @@ pub const ZigString = extern struct { }; } - pub fn toSliceClone(this: ZigString, allocator: std.mem.Allocator) Slice { + pub fn toSliceClone(this: ZigString, allocator: std.mem.Allocator) OOM!Slice { if (this.len == 0) return Slice.empty; - const buffer = this.toOwnedSlice(allocator) catch unreachable; + const buffer = try this.toOwnedSlice(allocator); return Slice{ .allocator = NullableAllocator.init(allocator), .ptr = buffer.ptr, @@ -1983,7 +1989,7 @@ pub const JSString = extern struct { this: *JSString, global: *JSGlobalObject, allocator: std.mem.Allocator, - ) ZigString.Slice { + ) JSError!ZigString.Slice { var str = ZigString.init(""); this.toZigString(global, &str); return str.toSliceClone(allocator); diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index c39153baa01acd..9b29a52dce8e02 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -3489,10 +3489,15 @@ pub const Blob = struct { // milliseconds since ECMAScript epoch last_modified: JSC.JSTimeType = JSC.init_timestamp, - pub fn unlink(this: *const FileStore, globalThis: *JSC.JSGlobalObject) JSValue { + pub fn unlink(this: *const FileStore, globalThis: *JSC.JSGlobalObject) bun.JSError!JSValue { return switch (this.pathlike) { .path => |path_like| JSC.Node.Async.unlink.create(globalThis, undefined, .{ - .path = .{ .encoded_slice = ZigString.init(path_like.slice()).toSliceClone(bun.default_allocator) }, + .path = .{ + .encoded_slice = switch (path_like) { + .encoded_slice => |slice| try slice.toOwned(bun.default_allocator), + else => try ZigString.init(path_like.slice()).toSliceClone(bun.default_allocator), + }, + }, }, globalThis.bunVM()), .fd => JSC.JSPromise.resolvedPromiseValue(globalThis, globalThis.createInvalidArgs("Is not possible to unlink a file descriptor", .{})), }; @@ -4750,15 +4755,23 @@ pub const Blob = struct { comptime { _ = Bun__Blob__getSizeForBindings; } - pub fn getStat(this: *Blob, globalThis: *JSC.JSGlobalObject, callback: *JSC.CallFrame) JSC.JSValue { + pub fn getStat(this: *Blob, globalThis: *JSC.JSGlobalObject, callback: *JSC.CallFrame) bun.JSError!JSC.JSValue { const store = this.store orelse return JSC.JSValue.jsUndefined(); // TODO: make this async for files return switch (store.data) { .file => |*file| { return switch (file.pathlike) { - .path => |path_like| JSC.Node.Async.stat.create(globalThis, undefined, .{ - .path = .{ .encoded_slice = ZigString.init(path_like.slice()).toSliceClone(bun.default_allocator) }, - }, globalThis.bunVM()), + .path => |path_like| { + return JSC.Node.Async.stat.create(globalThis, undefined, .{ + .path = .{ + .encoded_slice = switch (path_like) { + // it's already converted to utf8 + .encoded_slice => |slice| try slice.toOwned(bun.default_allocator), + else => try ZigString.init(path_like.slice()).toSliceClone(bun.default_allocator), + }, + }, + }, globalThis.bunVM()); + }, .fd => |fd| JSC.Node.Async.fstat.create(globalThis, undefined, .{ .fd = fd }, globalThis.bunVM()), }; }, diff --git a/test/js/bun/util/bun-file.test.ts b/test/js/bun/util/bun-file.test.ts new file mode 100644 index 00000000000000..33976872d1c79e --- /dev/null +++ b/test/js/bun/util/bun-file.test.ts @@ -0,0 +1,23 @@ +import { test, expect } from "bun:test"; +import { tmpdirSync } from "harness"; +import { join } from "path"; + +test("delete() and stat() should work with unicode paths", async () => { + const testDir = tmpdirSync(); + const filename = join(testDir, "🌟.txt"); + + expect(async () => { + await Bun.file(filename).delete(); + }).toThrow(`ENOENT: no such file or directory, unlink '${filename}'`); + + expect(async () => { + await Bun.file(filename).stat(); + }).toThrow(`ENOENT: no such file or directory, stat '${filename}'`); + + await Bun.write(filename, "HI"); + + expect(await Bun.file(filename).stat()).toMatchObject({ size: 2 }); + expect(await Bun.file(filename).delete()).toBe(undefined); + + expect(await Bun.file(filename).exists()).toBe(false); +});