diff options
author | 2023-06-18 10:47:42 -0700 | |
---|---|---|
committer | 2023-06-18 10:47:42 -0700 | |
commit | fdb7940c4e435a5b7f5a368f4168d748baf6b5b6 (patch) | |
tree | ee23b0ee654cb7ff9307ccc1db961fed444e01f6 | |
parent | 873f615358b78f913b3f8fe6adda47da7e6e57ac (diff) | |
download | bun-fdb7940c4e435a5b7f5a368f4168d748baf6b5b6.tar.gz bun-fdb7940c4e435a5b7f5a368f4168d748baf6b5b6.tar.zst bun-fdb7940c4e435a5b7f5a368f4168d748baf6b5b6.zip |
Fix a bunch of bugs (#3352)
* Fix a bunch of bugs
* undo that one
* Fix crash in readdir()
* woops
* woops
* Add comment
* :scissors:
* Make `readlink()` and `realpath` use much less memory
* Update BunString.cpp
* woopsie
* Unnecessary
* Don't commit these
---------
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r-- | bench/snippets/realpath.mjs | 8 | ||||
-rw-r--r-- | src/bun.js/api/bun/dns_resolver.zig | 8 | ||||
-rw-r--r-- | src/bun.js/base.zig | 9 | ||||
-rw-r--r-- | src/bun.js/bindings/BunString.cpp | 57 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.cpp | 12 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.zig | 5 | ||||
-rw-r--r-- | src/bun.js/bindings/headers.h | 1 | ||||
-rw-r--r-- | src/bun.js/bindings/headers.zig | 1 | ||||
-rw-r--r-- | src/bun.js/event_loop.zig | 5 | ||||
-rw-r--r-- | src/bun.js/node/dir_iterator.zig | 15 | ||||
-rw-r--r-- | src/bun.js/node/node_fs.zig | 247 | ||||
-rw-r--r-- | src/bun.js/node/node_fs_binding.zig | 8 | ||||
-rw-r--r-- | src/bun.js/node/types.zig | 183 | ||||
-rw-r--r-- | src/bun.zig | 1 | ||||
-rw-r--r-- | src/comptime_string_map.zig | 37 | ||||
-rw-r--r-- | src/string.zig | 163 | ||||
-rw-r--r-- | src/string_immutable.zig | 10 | ||||
-rw-r--r-- | test/js/node/fs/fs.test.ts | 57 | ||||
-rw-r--r-- | test/js/third_party/prisma/.gitignore | 1 | ||||
-rw-r--r-- | test/js/third_party/prisma/prisma/sqlite/migrations/20230618153912_init/migration.sql | 18 | ||||
-rw-r--r-- | test/js/third_party/prisma/prisma/sqlite/migrations/migration_lock.toml | 3 |
21 files changed, 715 insertions, 134 deletions
diff --git a/bench/snippets/realpath.mjs b/bench/snippets/realpath.mjs index 9c3793d80..4793ee3d6 100644 --- a/bench/snippets/realpath.mjs +++ b/bench/snippets/realpath.mjs @@ -1,4 +1,10 @@ import { realpathSync } from "node:fs"; const count = parseInt(process.env.ITERATIONS || "1", 10) || 1; const arg = process.argv[process.argv.length - 1]; -for (let i = 0; i < count; i++) realpathSync(arg); +import { bench, run } from "./runner.mjs"; + +bench("realpathSync x " + count, () => { + for (let i = 0; i < count; i++) realpathSync(arg, "utf-8"); +}); + +await run(); diff --git a/src/bun.js/api/bun/dns_resolver.zig b/src/bun.js/api/bun/dns_resolver.zig index 82411701d..aec295056 100644 --- a/src/bun.js/api/bun/dns_resolver.zig +++ b/src/bun.js/api/bun/dns_resolver.zig @@ -385,7 +385,7 @@ pub const GetAddrInfo = struct { return .unspecified; if (value.isNumber()) { - return switch (value.to(i32)) { + return switch (value.coerce(i32, globalObject)) { 0 => .unspecified, 4 => .inet, 6 => .inet6, @@ -394,11 +394,11 @@ pub const GetAddrInfo = struct { } if (value.isString()) { - const str = value.getZigString(globalObject); - if (str.len == 0) + const str = value.toBunString(globalObject); + if (str.isEmpty()) return .unspecified; - return map.getWithEql(str, JSC.ZigString.eqlComptime) orelse return error.InvalidFamily; + return str.inMap(map) orelse return error.InvalidFamily; } return error.InvalidFamily; diff --git a/src/bun.js/base.zig b/src/bun.js/base.zig index 4e3e83bd7..038f7f38b 100644 --- a/src/bun.js/base.zig +++ b/src/bun.js/base.zig @@ -207,6 +207,15 @@ pub const To = struct { return array; }, + []const bun.String => { + defer { + for (value) |out| { + out.deref(); + } + bun.default_allocator.free(value); + } + return bun.String.toJSArray(context, value).asObjectRef(); + }, []const PathString, []const []const u8, []const []u8, [][]const u8, [][:0]const u8, [][:0]u8 => { if (value.len == 0) return JSC.C.JSObjectMakeArray(context, 0, null, exception); diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index 249f68435..f737342f4 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -4,6 +4,7 @@ #include "helpers.h" #include "simdutf.h" #include "wtf/text/ExternalStringImpl.h" +#include "GCDefferalContext.h" using namespace JSC; extern "C" void Bun__WTFStringImpl__deref(WTF::StringImpl* impl) @@ -125,6 +126,22 @@ extern "C" JSC::EncodedJSValue BunString__toJS(JSC::JSGlobalObject* globalObject return JSValue::encode(Bun::toJS(globalObject, *bunString)); } +extern "C" BunString BunString__fromUTF8(const char* bytes, size_t length) +{ + if (simdutf::validate_utf8(bytes, length)) { + size_t u16Length = simdutf::utf16_length_from_utf8(bytes, length); + UChar* ptr; + auto impl = WTF::StringImpl::createUninitialized(static_cast<unsigned int>(u16Length), ptr); + RELEASE_ASSERT(simdutf::convert_utf8_to_utf16(bytes, length, ptr) == u16Length); + impl->ref(); + return { BunStringTag::WTFStringImpl, { .wtf = &impl.leakRef() } }; + } + + auto str = WTF::String::fromUTF8ReplacingInvalidSequences(reinterpret_cast<const LChar*>(bytes), length); + str.impl()->ref(); + return Bun::fromString(str); +} + extern "C" BunString BunString__fromLatin1(const char* bytes, size_t length) { return { BunStringTag::WTFStringImpl, { .wtf = &WTF::StringImpl::create(bytes, length).leakRef() } }; @@ -136,8 +153,7 @@ extern "C" BunString BunString__fromBytes(const char* bytes, size_t length) return BunString__fromLatin1(bytes, length); } - auto str = WTF::String::fromUTF8ReplacingInvalidSequences(reinterpret_cast<const LChar*>(bytes), length); - return Bun::fromString(str); + return BunString__fromUTF8(bytes, length); } extern "C" BunString BunString__createExternal(const char* bytes, size_t length, bool isLatin1, void* ctx, void (*callback)(void* arg0, void* arg1, size_t arg2)) @@ -149,6 +165,43 @@ extern "C" BunString BunString__createExternal(const char* bytes, size_t length, return { BunStringTag::WTFStringImpl, { .wtf = &impl.leakRef() } }; } +extern "C" EncodedJSValue BunString__createArray( + JSC::JSGlobalObject* globalObject, + const BunString* ptr, size_t length) +{ + if (length == 0) + return JSValue::encode(JSC::constructEmptyArray(globalObject, nullptr)); + + auto& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + // We must do this or Bun.gc(true) in a loop creating large arrays of strings will crash due to GC'ing. + MarkedArgumentBuffer arguments; + JSC::ObjectInitializationScope scope(vm); + GCDeferralContext context(vm); + + arguments.fill(length, [&](JSC::JSValue* value) { + const BunString* end = ptr + length; + while (ptr != end) { + *value++ = Bun::toJS(globalObject, *ptr++); + } + }); + + if (JSC::JSArray* array = JSC::JSArray::tryCreateUninitializedRestricted( + scope, + globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), + length)) { + + for (size_t i = 0; i < length; ++i) { + array->initializeIndex(scope, i, arguments.at(i)); + } + return JSValue::encode(array); + } + + JSC::throwOutOfMemoryError(globalObject, throwScope); + RELEASE_AND_RETURN(throwScope, JSValue::encode(JSC::JSValue())); +} + extern "C" void BunString__toWTFString(BunString* bunString) { if (bunString->tag == BunStringTag::ZigString) { diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index e64162b34..74357f225 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1192,6 +1192,16 @@ void WebCore__DOMURL__pathname_(WebCore__DOMURL* domURL, ZigString* arg1) *arg1 = Zig::toZigString(pathname); } +BunString WebCore__DOMURL__fileSystemPath(WebCore__DOMURL* arg0) +{ + const WTF::URL& url = arg0->href(); + if (url.isLocalFile()) { + return Bun::toString(url.fileSystemPath()); + } + + return BunStringEmpty; +} + extern "C" JSC__JSValue ZigString__toJSONObject(const ZigString* strPtr, JSC::JSGlobalObject* globalObject) { auto str = Zig::toString(*strPtr); @@ -3690,12 +3700,10 @@ JSC__JSValue JSC__VM__runGC(JSC__VM* vm, bool sync) JSC::JSLockHolder lock(vm); vm->finalizeSynchronousJSExecution(); - WTF::releaseFastMallocFreeMemory(); if (sync) { vm->heap.collectNow(JSC::Sync, JSC::CollectionScope::Full); - WTF::releaseFastMallocFreeMemory(); } else { vm->heap.collectSync(JSC::CollectionScope::Full); } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index bd6c11fc4..c9c733ebc 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -933,6 +933,10 @@ pub const DOMURL = opaque { return out; } + pub fn fileSystemPath(this: *DOMURL) bun.String { + return shim.cppFn("fileSystemPath", .{this}); + } + pub fn pathname_(this: *DOMURL, out: *ZigString) void { return shim.cppFn("pathname_", .{ this, out }); } @@ -947,6 +951,7 @@ pub const DOMURL = opaque { "cast_", "href_", "pathname_", + "fileSystemPath", }; }; diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 216a4f6cc..92639843a 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -163,6 +163,7 @@ CPP_DECL JSC__JSValue ZigString__toTypeErrorInstance(const ZigString* arg0, JSC_ CPP_DECL JSC__JSValue ZigString__toValue(const ZigString* arg0, JSC__JSGlobalObject* arg1); CPP_DECL JSC__JSValue ZigString__toValueGC(const ZigString* arg0, JSC__JSGlobalObject* arg1); CPP_DECL WebCore__DOMURL* WebCore__DOMURL__cast_(JSC__JSValue JSValue0, JSC__VM* arg1); +CPP_DECL BunString WebCore__DOMURL__fileSystemPath(WebCore__DOMURL* arg0); CPP_DECL void WebCore__DOMURL__href_(WebCore__DOMURL* arg0, ZigString* arg1); CPP_DECL void WebCore__DOMURL__pathname_(WebCore__DOMURL* arg0, ZigString* arg1); diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index 082f21418..99af5aecc 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -99,6 +99,7 @@ pub extern fn ZigString__toTypeErrorInstance(arg0: [*c]const ZigString, arg1: *b pub extern fn ZigString__toValue(arg0: [*c]const ZigString, arg1: *bindings.JSGlobalObject) JSC__JSValue; pub extern fn ZigString__toValueGC(arg0: [*c]const ZigString, arg1: *bindings.JSGlobalObject) JSC__JSValue; pub extern fn WebCore__DOMURL__cast_(JSValue0: JSC__JSValue, arg1: *bindings.VM) ?*bindings.DOMURL; +pub extern fn WebCore__DOMURL__fileSystemPath(arg0: ?*bindings.DOMURL) BunString; pub extern fn WebCore__DOMURL__href_(arg0: ?*bindings.DOMURL, arg1: [*c]ZigString) void; pub extern fn WebCore__DOMURL__pathname_(arg0: ?*bindings.DOMURL, arg1: [*c]ZigString) void; pub extern fn WebCore__DOMFormData__append(arg0: ?*bindings.DOMFormData, arg1: [*c]ZigString, arg2: [*c]ZigString) void; diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index c2125e64f..8441bd064 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -348,7 +348,10 @@ pub const GarbageCollectionController = struct { pub fn processGCTimer(this: *GarbageCollectionController) void { var vm = this.bunVM().global.vm(); - const this_heap_size = vm.blockBytesAllocated(); + this.processGCTimerWithHeapSize(vm, vm.blockBytesAllocated()); + } + + pub fn processGCTimerWithHeapSize(this: *GarbageCollectionController, vm: *JSC.VM, this_heap_size: usize) void { const prev = this.gc_last_heap_size; switch (this.gc_timer_state) { diff --git a/src/bun.js/node/dir_iterator.zig b/src/bun.js/node/dir_iterator.zig index f1063621e..aa939679c 100644 --- a/src/bun.js/node/dir_iterator.zig +++ b/src/bun.js/node/dir_iterator.zig @@ -17,7 +17,12 @@ const mem = std.mem; const strings = @import("root").bun.strings; const Maybe = JSC.Maybe; const File = std.fs.File; -const Result = Maybe(?Entry); +const IteratorResult = struct { + name: PathString, + kind: Entry.Kind, +}; + +const Result = Maybe(?IteratorResult); const Entry = JSC.Node.Dirent; @@ -84,7 +89,7 @@ pub const Iterator = switch (builtin.os.tag) { else => Entry.Kind.Unknown, }; return .{ - .result = Entry{ + .result = IteratorResult{ .name = PathString.init(name), .kind = entry_kind, }, @@ -139,7 +144,7 @@ pub const Iterator = switch (builtin.os.tag) { else => Entry.Kind.Unknown, }; return .{ - .result = Entry{ + .result = IteratorResult{ .name = PathString.init(name), .kind = entry_kind, }, @@ -213,7 +218,7 @@ pub const Iterator = switch (builtin.os.tag) { break :blk Entry.Kind.File; }; return .{ - .result = Entry{ + .result = IteratorResult{ .name = PathString.init(name_utf8), .kind = kind, }, @@ -278,7 +283,7 @@ pub const Iterator = switch (builtin.os.tag) { .SOCKET_STREAM, .SOCKET_DGRAM => Entry.Kind.UnixDomainSocket, else => Entry.Kind.Unknown, }; - return Entry{ + return IteratorResult{ .name = name, .kind = entry_kind, }; diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index f0f9c4980..254d58455 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -65,6 +65,11 @@ pub const Arguments = struct { old_path: PathLike, new_path: PathLike, + pub fn deinit(this: @This()) void { + this.old_path.deinit(); + this.new_path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Rename { const old_path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -99,6 +104,10 @@ pub const Arguments = struct { path: PathOrFileDescriptor, len: JSC.WebCore.Blob.SizeType = 0, + pub fn deinit(this: @This()) void { + this.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Truncate { const path = PathOrFileDescriptor.fromJS(ctx, arguments, arguments.arena.allocator(), exception) orelse { if (exception.* == null) { @@ -131,6 +140,10 @@ pub const Arguments = struct { fd: FileDescriptor, len: ?JSC.WebCore.Blob.SizeType = null, + pub fn deinit(this: @This()) void { + _ = this; + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?FTruncate { const fd = JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { if (exception.* == null) { @@ -177,6 +190,10 @@ pub const Arguments = struct { uid: uid_t = 0, gid: gid_t = 0, + pub fn deinit(this: @This()) void { + this.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Chown { const path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -233,6 +250,8 @@ pub const Arguments = struct { uid: uid_t, gid: gid_t, + pub fn deinit(_: @This()) void {} + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Fchown { const fd = JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { if (exception.* == null) { @@ -303,6 +322,10 @@ pub const Arguments = struct { atime: TimeLike, mtime: TimeLike, + pub fn deinit(this: @This()) void { + this.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Lutimes { const path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -374,6 +397,10 @@ pub const Arguments = struct { path: PathLike, mode: Mode = 0x777, + pub fn deinit(this: @This()) void { + this.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Chmod { const path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -479,6 +506,10 @@ pub const Arguments = struct { path: PathLike, big_int: bool = false, + pub fn deinit(this: Stat) void { + this.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Stat { const path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -500,8 +531,8 @@ pub const Arguments = struct { if (next_val.isCallable(ctx.ptr().vm())) break :brk false; arguments.eat(); - if (next_val.getIfPropertyExists(ctx.ptr(), "bigint")) |big_int| { - break :brk big_int.toBoolean(); + if (next_val.getOptional(ctx.ptr(), "bigint", bool) catch false) |big_int| { + break :brk big_int; } } } @@ -518,6 +549,8 @@ pub const Arguments = struct { fd: FileDescriptor, big_int: bool = false, + pub fn deinit(_: @This()) void {} + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Fstat { const fd = JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { if (exception.* == null) { @@ -549,8 +582,8 @@ pub const Arguments = struct { if (next_val.isCallable(ctx.ptr().vm())) break :brk false; arguments.eat(); - if (next_val.getIfPropertyExists(ctx.ptr(), "bigint")) |big_int| { - break :brk big_int.toBoolean(); + if (next_val.getOptional(ctx.ptr(), "bigint", bool) catch false) |big_int| { + break :brk big_int; } } } @@ -569,6 +602,11 @@ pub const Arguments = struct { old_path: PathLike, new_path: PathLike, + pub fn deinit(this: Link) void { + this.old_path.deinit(); + this.new_path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Link { const old_path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -606,6 +644,11 @@ pub const Arguments = struct { old_path: PathLike, new_path: PathLike, + pub fn deinit(this: Symlink) void { + this.old_path.deinit(); + this.new_path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Symlink { const old_path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -658,6 +701,10 @@ pub const Arguments = struct { path: PathLike, encoding: Encoding = Encoding.utf8, + pub fn deinit(this: Readlink) void { + this.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Readlink { const path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -678,12 +725,12 @@ pub const Arguments = struct { switch (val.jsType()) { JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject => { - encoding = Encoding.fromStringValue(val, ctx.ptr()) orelse Encoding.utf8; + encoding = Encoding.fromJS(val, ctx.ptr()) orelse Encoding.utf8; }, else => { if (val.isObject()) { if (val.getIfPropertyExists(ctx.ptr(), "encoding")) |encoding_| { - encoding = Encoding.fromStringValue(encoding_, ctx.ptr()) orelse Encoding.utf8; + encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse Encoding.utf8; } } }, @@ -698,6 +745,10 @@ pub const Arguments = struct { path: PathLike, encoding: Encoding = Encoding.utf8, + pub fn deinit(this: Realpath) void { + this.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Realpath { const path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -718,12 +769,12 @@ pub const Arguments = struct { switch (val.jsType()) { JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject => { - encoding = Encoding.fromStringValue(val, ctx.ptr()) orelse Encoding.utf8; + encoding = Encoding.fromJS(val, ctx.ptr()) orelse Encoding.utf8; }, else => { if (val.isObject()) { if (val.getIfPropertyExists(ctx.ptr(), "encoding")) |encoding_| { - encoding = Encoding.fromStringValue(encoding_, ctx.ptr()) orelse Encoding.utf8; + encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse Encoding.utf8; } } }, @@ -775,6 +826,10 @@ pub const Arguments = struct { recursive: bool = false, retry_delay: c_uint = 100, + pub fn deinit(this: RmDir) void { + this.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?RmDir { const path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -789,18 +844,25 @@ pub const Arguments = struct { }; if (exception.* != null) return null; + var recursive = false; var force = false; if (arguments.next()) |val| { arguments.eat(); if (val.isObject()) { - if (val.get(ctx.ptr(), "recursive")) |boolean| { - recursive = boolean.toBoolean(); + if (val.getOptional(ctx.ptr(), "recursive", bool) catch { + path.deinit(); + return null; + }) |boolean| { + recursive = boolean; } - if (val.get(ctx.ptr(), "force")) |boolean| { - force = boolean.toBoolean(); + if (val.getOptional(ctx.ptr(), "force", bool) catch { + path.deinit(); + return null; + }) |boolean| { + force = boolean; } } } @@ -824,7 +886,11 @@ pub const Arguments = struct { /// @default mode: Mode = 0o777, - pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Mkdir { + pub fn deinit(this: Mkdir) void { + this.path.deinit(); + } + + pub fn fromJS(ctx: *JSC.JSGlobalObject, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Mkdir { const path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { JSC.throwInvalidArguments( @@ -846,8 +912,11 @@ pub const Arguments = struct { arguments.eat(); if (val.isObject()) { - if (val.getIfPropertyExists(ctx.ptr(), "recursive")) |recursive_| { - recursive = recursive_.toBoolean(); + if (val.getOptional(ctx.ptr(), "recursive", bool) catch { + path.deinit(); + return null; + }) |boolean| { + recursive = boolean; } if (val.getIfPropertyExists(ctx.ptr(), "mode")) |mode_| { @@ -868,6 +937,10 @@ pub const Arguments = struct { prefix: JSC.Node.SliceOrBuffer = .{ .buffer = .{ .buffer = JSC.ArrayBuffer.empty } }, encoding: Encoding = Encoding.utf8, + pub fn deinit(this: MkdirTemp) void { + this.prefix.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?MkdirTemp { const prefix_value = arguments.next() orelse return MkdirTemp{}; @@ -894,12 +967,12 @@ pub const Arguments = struct { switch (val.jsType()) { JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject => { - encoding = Encoding.fromStringValue(val, ctx.ptr()) orelse Encoding.utf8; + encoding = Encoding.fromJS(val, ctx.ptr()) orelse Encoding.utf8; }, else => { if (val.isObject()) { if (val.getIfPropertyExists(ctx.ptr(), "encoding")) |encoding_| { - encoding = Encoding.fromStringValue(encoding_, ctx.ptr()) orelse Encoding.utf8; + encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse Encoding.utf8; } } }, @@ -918,6 +991,10 @@ pub const Arguments = struct { encoding: Encoding = Encoding.utf8, with_file_types: bool = false, + pub fn deinit(this: Readdir) void { + this.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Readdir { const path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -941,16 +1018,19 @@ pub const Arguments = struct { switch (val.jsType()) { JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject => { - encoding = Encoding.fromStringValue(val, ctx.ptr()) orelse Encoding.utf8; + encoding = Encoding.fromJS(val, ctx.ptr()) orelse Encoding.utf8; }, else => { if (val.isObject()) { if (val.getIfPropertyExists(ctx.ptr(), "encoding")) |encoding_| { - encoding = Encoding.fromStringValue(encoding_, ctx.ptr()) orelse Encoding.utf8; + encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse Encoding.utf8; } - if (val.getIfPropertyExists(ctx.ptr(), "withFileTypes")) |with_file_types_| { - with_file_types = with_file_types_.toBoolean(); + if (val.getOptional(ctx.ptr(), "withFileTypes", bool) catch { + path.deinit(); + return null; + }) |with_file_types_| { + with_file_types = with_file_types_; } } }, @@ -968,6 +1048,8 @@ pub const Arguments = struct { pub const Close = struct { fd: FileDescriptor, + pub fn deinit(_: Close) void {} + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Close { const fd = JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { if (exception.* == null) { @@ -1004,6 +1086,10 @@ pub const Arguments = struct { flags: FileSystemFlags = FileSystemFlags.r, mode: Mode = default_permission, + pub fn deinit(this: Open) void { + this.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Open { const path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -1068,6 +1154,8 @@ pub const Arguments = struct { atime: TimeLike, mtime: TimeLike, + pub fn deinit(_: Futimes) void {} + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Futimes { const fd = JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { if (exception.* == null) { @@ -1152,6 +1240,8 @@ pub const Arguments = struct { pub const FSync = struct { fd: FileDescriptor, + pub fn deinit(_: FSync) void {} + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?FSync { const fd = JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { if (exception.* == null) { @@ -1216,6 +1306,8 @@ pub const Arguments = struct { position: ?ReadPosition = null, encoding: Encoding = Encoding.buffer, + pub fn deinit(_: Write) void {} + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Write { const fd = JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { if (exception.* == null) { @@ -1291,7 +1383,7 @@ pub const Arguments = struct { } if (current.isString()) { - args.encoding = Encoding.fromStringValue(current, ctx.ptr()) orelse Encoding.utf8; + args.encoding = Encoding.fromJS(current, ctx.ptr()) orelse Encoding.utf8; arguments.eat(); } }, @@ -1330,6 +1422,8 @@ pub const Arguments = struct { length: u64 = std.math.maxInt(u64), position: ?ReadPosition = null, + pub fn deinit(_: Read) void {} + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Read { const fd = JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { if (exception.* == null) { @@ -1460,6 +1554,10 @@ pub const Arguments = struct { flag: FileSystemFlags = FileSystemFlags.r, + pub fn deinit(self: ReadFile) void { + self.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?ReadFile { const path = PathOrFileDescriptor.fromJS(ctx, arguments, arguments.arena.allocator(), exception) orelse { if (exception.* == null) { @@ -1481,7 +1579,7 @@ pub const Arguments = struct { if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { - encoding = Encoding.fromStringValue(arg, ctx.ptr()) orelse { + encoding = Encoding.fromJS(arg, ctx.ptr()) orelse { if (exception.* == null) { JSC.throwInvalidArguments( "Invalid encoding", @@ -1495,7 +1593,7 @@ pub const Arguments = struct { } else if (arg.isObject()) { if (arg.getIfPropertyExists(ctx.ptr(), "encoding")) |encoding_| { if (!encoding_.isUndefinedOrNull()) { - encoding = Encoding.fromStringValue(encoding_, ctx.ptr()) orelse { + encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse { if (exception.* == null) { JSC.throwInvalidArguments( "Invalid encoding", @@ -1542,6 +1640,10 @@ pub const Arguments = struct { data: StringOrBuffer, dirfd: FileDescriptor = @intCast(FileDescriptor, std.fs.cwd().fd), + pub fn deinit(self: WriteFile) void { + self.file.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?WriteFile { const file = PathOrFileDescriptor.fromJS(ctx, arguments, arguments.arena.allocator(), exception) orelse { if (exception.* == null) { @@ -1589,7 +1691,7 @@ pub const Arguments = struct { if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { - encoding = Encoding.fromStringValue(arg, ctx.ptr()) orelse { + encoding = Encoding.fromJS(arg, ctx.ptr()) orelse { if (exception.* == null) { JSC.throwInvalidArguments( "Invalid encoding", @@ -1602,7 +1704,7 @@ pub const Arguments = struct { }; } else if (arg.isObject()) { if (arg.getTruthy(ctx.ptr(), "encoding")) |encoding_| { - encoding = Encoding.fromStringValue(encoding_, ctx.ptr()) orelse { + encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse { if (exception.* == null) { JSC.throwInvalidArguments( "Invalid encoding", @@ -1665,6 +1767,10 @@ pub const Arguments = struct { /// Number of directory entries that are buffered internally when reading from the directory. Higher values lead to better performance but higher memory usage. Default: 32 buffer_size: c_int = 32, + pub fn deinit(self: OpenDir) void { + self.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?OpenDir { const path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -1686,7 +1792,7 @@ pub const Arguments = struct { if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { - encoding = Encoding.fromStringValue(arg, ctx.ptr()) orelse { + encoding = Encoding.fromJS(arg, ctx.ptr()) orelse { if (exception.* == null) { JSC.throwInvalidArguments( "Invalid encoding", @@ -1700,7 +1806,7 @@ pub const Arguments = struct { } else if (arg.isObject()) { if (arg.getIfPropertyExists(ctx.ptr(), "encoding")) |encoding_| { if (!encoding_.isUndefinedOrNull()) { - encoding = Encoding.fromStringValue(encoding_, ctx.ptr()) orelse { + encoding = Encoding.fromJS(encoding_, ctx.ptr()) orelse { if (exception.* == null) { JSC.throwInvalidArguments( "Invalid encoding", @@ -1741,6 +1847,12 @@ pub const Arguments = struct { pub const Exists = struct { path: ?PathLike, + pub fn deinit(this: Exists) void { + if (this.path) |path| { + path.deinit(); + } + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Exists { return Exists{ .path = PathLike.fromJS(ctx, arguments, exception), @@ -1752,6 +1864,10 @@ pub const Arguments = struct { path: PathLike, mode: FileSystemFlags = FileSystemFlags.r, + pub fn deinit(this: Access) void { + this.path.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Access { const path = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -1805,6 +1921,10 @@ pub const Arguments = struct { highwater_mark: u32 = 64 * 1024, global_object: *JSC.JSGlobalObject, + pub fn deinit(this: CreateReadStream) void { + this.file.deinit(); + } + pub fn copyToState(this: CreateReadStream, state: *JSC.Node.Readable.State) void { state.encoding = this.encoding; state.highwater_mark = this.highwater_mark; @@ -1826,7 +1946,7 @@ pub const Arguments = struct { if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { - stream.encoding = Encoding.fromStringValue(arg, ctx.ptr()) orelse { + stream.encoding = Encoding.fromJS(arg, ctx.ptr()) orelse { if (exception.* != null) { JSC.throwInvalidArguments( "Invalid encoding", @@ -1853,7 +1973,7 @@ pub const Arguments = struct { } if (arg.getIfPropertyExists(ctx.ptr(), "encoding")) |encoding| { - stream.encoding = Encoding.fromStringValue(encoding, ctx.ptr()) orelse { + stream.encoding = Encoding.fromJS(encoding, ctx.ptr()) orelse { if (exception.* != null) { JSC.throwInvalidArguments( "Invalid encoding", @@ -1939,6 +2059,10 @@ pub const Arguments = struct { highwater_mark: u32 = 256 * 1024, global_object: *JSC.JSGlobalObject, + pub fn deinit(this: @This()) void { + this.file.deinit(); + } + pub fn copyToState(this: CreateWriteStream, state: *JSC.Node.Writable.State) void { state.encoding = this.encoding; state.highwater_mark = this.highwater_mark; @@ -1960,7 +2084,7 @@ pub const Arguments = struct { if (arguments.next()) |arg| { arguments.eat(); if (arg.isString()) { - stream.encoding = Encoding.fromStringValue(arg, ctx.ptr()) orelse { + stream.encoding = Encoding.fromJS(arg, ctx.ptr()) orelse { if (exception.* != null) { JSC.throwInvalidArguments( "Invalid encoding", @@ -1987,7 +2111,7 @@ pub const Arguments = struct { } if (arg.getIfPropertyExists(ctx.ptr(), "encoding")) |encoding| { - stream.encoding = Encoding.fromStringValue(encoding, ctx.ptr()) orelse { + stream.encoding = Encoding.fromJS(encoding, ctx.ptr()) orelse { if (exception.* != null) { JSC.throwInvalidArguments( "Invalid encoding", @@ -2057,6 +2181,8 @@ pub const Arguments = struct { pub const FdataSync = struct { fd: FileDescriptor, + pub fn deinit(_: FdataSync) void {} + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?FdataSync { const fd = JSC.Node.fileDescriptorFromJS(ctx, arguments.next() orelse { if (exception.* == null) { @@ -2093,6 +2219,11 @@ pub const Arguments = struct { dest: PathLike, mode: Constants.Copyfile, + fn deinit(this: CopyFile) void { + this.src.deinit(); + this.dest.deinit(); + } + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?CopyFile { const src = PathLike.fromJS(ctx, arguments, exception) orelse { if (exception.* == null) { @@ -2311,7 +2442,7 @@ const Return = struct { pub const Readdir = union(Tag) { with_file_types: []Dirent, buffers: []const Buffer, - files: []const JSC.ZigString, + files: []const bun.String, pub const Tag = enum { with_file_types, @@ -2323,7 +2454,7 @@ const Return = struct { return switch (this) { .with_file_types => JSC.To.JS.withType([]const Dirent, this.with_file_types, ctx, exception), .buffers => JSC.To.JS.withType([]const Buffer, this.buffers, ctx, exception), - .files => JSC.To.JS.withType([]const JSC.ZigString, this.files, ctx, exception), + .files => JSC.To.JS.withType([]const bun.String, this.files, ctx, exception), }; } }; @@ -2333,8 +2464,8 @@ const Return = struct { buffer: JSC.Node.Buffer, null_terminated: [:0]const u8, }; - pub const Readlink = StringOrBuffer; - pub const Realpath = StringOrBuffer; + pub const Readlink = JSC.Node.StringOrBunStringOrBuffer; + pub const Realpath = JSC.Node.StringOrBunStringOrBuffer; pub const RealpathNative = Realpath; pub const Rename = void; pub const Rmdir = void; @@ -3183,7 +3314,7 @@ pub const NodeFS = struct { return _readdir( this, args, - JSC.ZigString, + bun.String, flavor, ); } @@ -3206,7 +3337,7 @@ pub const NodeFS = struct { ) Maybe(Return.Readdir) { const file_type = comptime switch (ExpectedType) { Dirent => "with_file_types", - JSC.ZigString => "files", + bun.String => "files", Buffer => "buffers", else => unreachable, }; @@ -3234,13 +3365,13 @@ pub const NodeFS = struct { for (entries.items) |*item| { switch (comptime ExpectedType) { Dirent => { - bun.default_allocator.free(item.name.slice()); + item.name.deref(); }, Buffer => { item.destroy(); }, - JSC.ZigString => { - bun.default_allocator.free(item.slice()); + bun.String => { + item.deref(); }, else => unreachable, } @@ -3254,19 +3385,19 @@ pub const NodeFS = struct { }, .result => |ent| ent, }) |current| : (entry = iterator.next()) { + const utf8_name = current.name.slice(); switch (comptime ExpectedType) { Dirent => { entries.append(.{ - .name = PathString.init(bun.default_allocator.dupe(u8, current.name.slice()) catch unreachable), + .name = bun.String.create(utf8_name), .kind = current.kind, }) catch unreachable; }, Buffer => { - const slice = current.name.slice(); - entries.append(Buffer.fromString(slice, bun.default_allocator) catch unreachable) catch unreachable; + entries.append(Buffer.fromString(utf8_name, bun.default_allocator) catch unreachable) catch unreachable; }, - JSC.ZigString => { - entries.append(JSC.ZigString.dupeForJS(current.name.slice(), bun.default_allocator) catch unreachable) catch unreachable; + bun.String => { + entries.append(bun.String.create(utf8_name)) catch unreachable; }, else => unreachable, } @@ -3576,9 +3707,15 @@ pub const NodeFS = struct { .buffer => .{ .buffer = Buffer.fromString(outbuf[0..len], bun.default_allocator) catch unreachable, }, - else => .{ - .string = bun.default_allocator.dupe(u8, outbuf[0..len]) catch unreachable, - }, + else => if (args.path == .slice_with_underlying_string and + strings.eqlLong(args.path.slice_with_underlying_string.slice(), outbuf[0..len], true)) + .{ + .BunString = args.path.slice_with_underlying_string.underlying.dupeRef(), + } + else + .{ + .BunString = bun.String.create(outbuf[0..len]), + }, }, }; }, @@ -3630,9 +3767,15 @@ pub const NodeFS = struct { .buffer => .{ .buffer = Buffer.fromString(buf, bun.default_allocator) catch unreachable, }, - else => .{ - .string = bun.default_allocator.dupe(u8, buf) catch unreachable, - }, + else => if (args.path == .slice_with_underlying_string and + strings.eqlLong(args.path.slice_with_underlying_string.slice(), buf, true)) + .{ + .BunString = args.path.slice_with_underlying_string.underlying.dupeRef(), + } + else + .{ + .BunString = bun.String.create(buf), + }, }, }; }, diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig index 0e0875a18..74b769bf6 100644 --- a/src/bun.js/node/node_fs_binding.zig +++ b/src/bun.js/node/node_fs_binding.zig @@ -44,12 +44,16 @@ fn callSync(comptime FunctionEnum: NodeFSFunctionEnum) NodeFSFunction { const args = if (comptime Arguments != void) (Arguments.fromJS(globalObject, &slice, &exceptionref) orelse { - std.debug.assert(exceptionref != null); - globalObject.throwValue(JSC.JSValue.c(exceptionref)); + // we might've already thrown + if (exceptionref != null) + globalObject.throwValue(JSC.JSValue.c(exceptionref)); return .zero; }) else Arguments{}; + defer { + if (comptime Arguments != void and @hasDecl(Arguments, "deinit")) args.deinit(); + } const exception1 = JSC.JSValue.c(exceptionref); diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 95a2270a5..23ae60c69 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -248,6 +248,61 @@ pub const StringOrBuffer = union(Tag) { } }; +pub const StringOrBunStringOrBuffer = union(enum) { + BunString: bun.String, + string: string, + buffer: Buffer, + + pub fn toJS(this: StringOrBunStringOrBuffer, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) JSC.C.JSValueRef { + return switch (this) { + .string => { + if (this.string.len == 0) + return JSC.ZigString.Empty.toValue(ctx).asObjectRef(); + + const input = this.string; + if (strings.toUTF16Alloc(bun.default_allocator, input, false) catch null) |utf16| { + bun.default_allocator.free(bun.constStrToU8(input)); + return JSC.ZigString.toExternalU16(utf16.ptr, utf16.len, ctx.ptr()).asObjectRef(); + } + + return JSC.ZigString.init(input).toExternalValue(ctx.ptr()).asObjectRef(); + }, + .buffer => this.buffer.toJSObjectRef(ctx, exception), + .BunString => { + defer this.BunString.deref(); + return this.BunString.toJSConst(ctx).asObjectRef(); + }, + }; + } + + pub fn fromJS(global: *JSC.JSGlobalObject, allocator: std.mem.Allocator, value: JSC.JSValue, exception: JSC.C.ExceptionRef) ?StringOrBuffer { + return switch (value.jsType()) { + JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject, JSC.JSValue.JSType.Object => { + var zig_str = value.toSlice(global, allocator); + return StringOrBuffer{ .string = zig_str.slice() }; + }, + + .ArrayBuffer, + .Int8Array, + .Uint8Array, + .Uint8ClampedArray, + .Int16Array, + .Uint16Array, + .Int32Array, + .Uint32Array, + .Float32Array, + .Float64Array, + .BigInt64Array, + .BigUint64Array, + .DataView, + => StringOrBuffer{ + .buffer = Buffer.fromArrayBuffer(global, value, exception), + }, + else => null, + }; + } +}; + /// Like StringOrBuffer but actually returns a Node.js Buffer pub const StringOrNodeBuffer = union(Tag) { string: string, @@ -388,14 +443,7 @@ pub const SliceOrBuffer = union(Tag) { const encoding: Encoding = brk: { if (encoding_value.isEmptyOrUndefinedOrNull()) break :brk .utf8; - var encoding_str = encoding_value.toSlice(global, allocator); - if (encoding_str.len == 0) - break :brk .utf8; - - defer encoding_str.deinit(); - - // TODO: better error - break :brk Encoding.from(encoding_str.slice()) orelse return null; + break :brk Encoding.fromJS(encoding_value, global) orelse .utf8; }; if (encoding == .utf8) { @@ -431,6 +479,21 @@ pub const Encoding = enum(u8) { /// Refer to the buffer's encoding buffer, + pub const map = bun.ComptimeStringMap(Encoding, .{ + .{ "utf-8", Encoding.utf8 }, + .{ "utf8", Encoding.utf8 }, + .{ "ucs-2", Encoding.utf16le }, + .{ "utf16-le", Encoding.utf16le }, + .{ "utf16le", Encoding.utf16le }, + .{ "binary", Encoding.latin1 }, + .{ "latin1", Encoding.latin1 }, + .{ "ascii", Encoding.ascii }, + .{ "base64", Encoding.base64 }, + .{ "hex", Encoding.hex }, + .{ "buffer", Encoding.buffer }, + .{ "base64url", Encoding.base64url }, + }); + pub fn isBinaryToText(this: Encoding) bool { return switch (this) { .hex, .base64, .base64url => true, @@ -438,39 +501,18 @@ pub const Encoding = enum(u8) { }; } - const Eight = strings.ExactSizeMatcher(8); /// Caller must verify the value is a string - pub fn fromStringValue(value: JSC.JSValue, global: *JSC.JSGlobalObject) ?Encoding { - var sliced = value.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - return from(sliced.slice()); + pub fn fromJS(value: JSC.JSValue, global: *JSC.JSGlobalObject) ?Encoding { + if (bun.String.tryFromJS(value, global)) |str| { + return str.inMapCaseInsensitive(map); + } + + return null; } /// Caller must verify the value is a string pub fn from(slice: []const u8) ?Encoding { - return switch (slice.len) { - 0...2 => null, - else => switch (Eight.matchLower(slice)) { - Eight.case("utf-8"), Eight.case("utf8") => Encoding.utf8, - Eight.case("ucs-2"), Eight.case("ucs2") => Encoding.ucs2, - Eight.case("utf16-le"), Eight.case("utf16le") => Encoding.utf16le, - - // "binary" is an alias for "latin1" - Eight.case("binary"), Eight.case("latin1") => Encoding.latin1, - - Eight.case("ascii") => Encoding.ascii, - Eight.case("base64") => Encoding.base64, - Eight.case("hex") => Encoding.hex, - Eight.case("buffer") => Encoding.buffer, - else => null, - }, - "base64url".len => brk: { - if (strings.eqlCaseInsensitiveASCII(slice, "base64url", false)) { - break :brk Encoding.base64url; - } - break :brk null; - }, - }; + return strings.inMapCaseInsensitive(slice, map); } pub fn encodeWithSize(encoding: Encoding, globalThis: *JSC.JSGlobalObject, comptime size: usize, input: *const [size]u8) JSC.JSValue { @@ -557,17 +599,23 @@ pub fn CallbackTask(comptime Result: type) type { } pub const PathLike = union(Tag) { - string: PathString, + string: bun.PathString, buffer: Buffer, - url: void, + slice_with_underlying_string: bun.SliceWithUnderlyingString, - pub const Tag = enum { string, buffer, url }; + pub const Tag = enum { string, buffer, slice_with_underlying_string }; + + pub fn deinit(this: *const PathLike) void { + if (this.* == .slice_with_underlying_string) { + this.slice_with_underlying_string.deinit(); + } + } pub inline fn slice(this: PathLike) string { return switch (this) { .string => this.string.slice(), .buffer => this.buffer.slice(), - else => unreachable, // TODO: + .slice_with_underlying_string => this.slice_with_underlying_string.slice(), }; } @@ -603,6 +651,7 @@ pub const PathLike = union(Tag) { return switch (this) { .string => this.string.toJS(ctx, exception), .buffer => this.buffer.toJSObjectRef(ctx, exception), + .slice_with_underlying_string => this.slice_with_underlying_string.toJS(ctx).asObjectRef(), else => unreachable, }; } @@ -638,31 +687,35 @@ pub const PathLike = union(Tag) { JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject, => { - var zig_str = arg.toSlice(ctx, allocator); + var str = arg.toBunString(ctx); + + arguments.eat(); - if (!Valid.pathSlice(zig_str, ctx, exception)) { - zig_str.deinit(); + if (!Valid.pathStringLength(str.length(), ctx, exception)) { return null; } - arguments.eat(); - arg.ensureStillAlive(); + str.ref(); - return PathLike{ .string = PathString.init(zig_str.slice()) }; + return PathLike{ .slice_with_underlying_string = str.toSlice(allocator) }; }, else => { if (arg.as(JSC.DOMURL)) |domurl| { - var zig_str = domurl.pathname(); - if (!Valid.pathString(zig_str, ctx, exception)) return null; - - arguments.protectEat(); + const path_str: bun.String = domurl.fileSystemPath(); + if (path_str.isEmpty()) { + JSC.throwInvalidArguments("URL must be a non-empty \"file:\" path", .{}, ctx, exception); + return null; + } + arguments.eat(); - if (zig_str.is16Bit()) { - var printed = bun.asByteSlice(std.fmt.allocPrintZ(arguments.arena.allocator(), "{}", .{zig_str}) catch unreachable); - return PathLike{ .string = PathString.init(printed.ptr[0 .. printed.len + 1]) }; + if (!Valid.pathStringLength(path_str.length(), ctx, exception)) { + defer path_str.deref(); + return null; } - return PathLike{ .string = PathString.init(zig_str.slice()) }; + return PathLike{ + .slice_with_underlying_string = path_str.toSlice(allocator), + }; } return null; @@ -703,8 +756,8 @@ pub const Valid = struct { unreachable; } - pub fn pathString(zig_str: JSC.ZigString, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) bool { - switch (zig_str.len) { + pub fn pathStringLength(len: usize, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) bool { + switch (len) { 0 => { JSC.throwInvalidArguments("Invalid path string: can't be empty", .{}, ctx, exception); return false; @@ -725,6 +778,10 @@ pub const Valid = struct { unreachable; } + pub fn pathString(zig_str: JSC.ZigString, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) bool { + return pathStringLength(zig_str.len, ctx, exception); + } + pub fn pathBuffer(buffer: Buffer, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) bool { const slice = buffer.slice(); switch (slice.len) { @@ -900,6 +957,14 @@ pub const PathOrFileDescriptor = union(Tag) { pub const Tag = enum { fd, path }; + /// This will unref() the path string if it is a PathLike. + /// Does nothing for file descriptors, **does not** close file descriptors. + pub fn deinit(this: PathOrFileDescriptor) void { + if (this == .path) { + this.path.deinit(); + } + } + pub fn hash(this: JSC.Node.PathOrFileDescriptor) u64 { return switch (this) { .path => std.hash.Wyhash.hash(0, this.path.slice()), @@ -1339,7 +1404,7 @@ pub const Stats = union(enum) { /// closed after the iterator exits. /// @since v12.12.0 pub const Dirent = struct { - name: PathString, + name: bun.String, // not publicly exposed kind: Kind, @@ -1352,7 +1417,7 @@ pub const Dirent = struct { } pub fn getName(this: *Dirent, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - return JSC.ZigString.fromUTF8(this.name.slice()).toValueGC(globalObject); + return this.name.toJS(globalObject); } pub fn isBlockDevice( @@ -1406,7 +1471,7 @@ pub const Dirent = struct { } pub fn finalize(this: *Dirent) callconv(.C) void { - bun.default_allocator.free(this.name.slice()); + this.name.deref(); bun.default_allocator.destroy(this); } }; diff --git a/src/bun.zig b/src/bun.zig index ab3b3e18c..f2d76df53 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1525,6 +1525,7 @@ pub const StringPointer = Schema.Api.StringPointer; pub const StandaloneModuleGraph = @import("./standalone_bun.zig").StandaloneModuleGraph; pub const String = @import("./string.zig").String; +pub const SliceWithUnderlyingString = @import("./string.zig").SliceWithUnderlyingString; pub const WTF = struct { /// The String type from WebKit's WTF library. diff --git a/src/comptime_string_map.zig b/src/comptime_string_map.zig index 01b5de5b0..e60fcb1cb 100644 --- a/src/comptime_string_map.zig +++ b/src/comptime_string_map.zig @@ -77,6 +77,8 @@ pub fn ComptimeStringMapWithKeyType(comptime KeyType: type, comptime V: type, co break :blk k[0..]; }; + pub const Value = V; + pub fn keys() []const []const KeyType { return keys_list; } @@ -131,6 +133,25 @@ pub fn ComptimeStringMapWithKeyType(comptime KeyType: type, comptime V: type, co return null; } + pub fn getWithLengthAndEqlList(str: anytype, comptime len: usize, comptime eqls: anytype) ?V { + const end = comptime brk: { + var i = len_indexes[len]; + @setEvalBranchQuota(99999); + + while (i < kvs.len and kvs[i].key.len == len) : (i += 1) {} + + break :brk i; + }; + + const start = comptime len_indexes[len]; + const range = comptime keys()[start..end]; + if (eqls(str, range)) |k| { + return kvs[start + k].value; + } + + return null; + } + pub fn get(str: []const KeyType) ?V { if (str.len < precomputed.min_len or str.len > precomputed.max_len) return null; @@ -160,6 +181,22 @@ pub fn ComptimeStringMapWithKeyType(comptime KeyType: type, comptime V: type, co return null; } + + pub fn getWithEqlList(input: anytype, comptime eql: anytype) ?V { + const Input = @TypeOf(input); + const length = if (comptime std.meta.trait.isSlice(Input) or std.meta.trait.isZigString(Input)) input.len else input.length(); + if (length < precomputed.min_len or length > precomputed.max_len) + return null; + + comptime var i: usize = precomputed.min_len; + inline while (i <= precomputed.max_len) : (i += 1) { + if (length == i) { + return getWithLengthAndEqlList(input, i, eql); + } + } + + return null; + } }; } diff --git a/src/string.zig b/src/string.zig index f09428f69..cf4109d40 100644 --- a/src/string.zig +++ b/src/string.zig @@ -250,6 +250,15 @@ pub const String = extern struct { return BunString__fromBytes(bytes.ptr, bytes.len); } + pub fn isEmpty(this: String) bool { + return this.tag == .Empty or this.length() == 0; + } + + pub fn dupeRef(this: String) String { + this.ref(); + return this; + } + pub fn initWithType(comptime Type: type, value: Type) String { switch (comptime Type) { ZigString => return String{ .tag = .ZigString, .value = .{ .ZigString = value } }, @@ -337,6 +346,24 @@ pub const String = extern struct { return BunString__toJS(globalObject, this); } + pub fn toJSConst(this: *const String, globalObject: *bun.JSC.JSGlobalObject) JSC.JSValue { + JSC.markBinding(@src()); + var a = this.*; + return toJS(&a, globalObject); + } + + extern fn BunString__createArray( + globalObject: *bun.JSC.JSGlobalObject, + ptr: [*]const String, + len: usize, + ) JSC.JSValue; + + pub fn toJSArray(globalObject: *bun.JSC.JSGlobalObject, array: []const bun.String) JSC.JSValue { + JSC.markBinding(@src()); + + return BunString__createArray(globalObject, array.ptr, array.len); + } + pub fn toZigString(this: String) ZigString { if (this.tag == .StaticZigString or this.tag == .ZigString) { return this.value.ZigString; @@ -449,6 +476,13 @@ pub const String = extern struct { return ZigString.Slice.empty; } + pub fn toSlice(this: String, allocator: std.mem.Allocator) SliceWithUnderlyingString { + return SliceWithUnderlyingString{ + .utf8 = this.toUTF8(allocator), + .underlying = this, + }; + } + extern fn BunString__fromJS(globalObject: *JSC.JSGlobalObject, value: bun.JSC.JSValue, out: *String) bool; extern fn BunString__toJS(globalObject: *JSC.JSGlobalObject, in: *String) JSC.JSValue; extern fn BunString__toWTFString(this: *String) void; @@ -473,11 +507,114 @@ pub const String = extern struct { return this.toZigString().eqlComptime(value); } + pub fn is8Bit(this: String) bool { + return switch (this.tag) { + .WTFStringImpl => this.value.WTFStringImpl.is8Bit(), + .ZigString => !this.value.ZigString.is16Bit(), + else => true, + }; + } + + pub fn indexOfComptimeWithCheckLen(this: String, comptime values: []const []const u8, comptime check_len: usize) ?usize { + if (this.is8Bit()) { + const bytes = this.byteSlice(); + for (values, 0..) |val, i| { + if (bun.strings.eqlComptimeCheckLenWithType(u8, bytes, val, check_len)) { + return i; + } + } + + return null; + } + + const u16_bytes = this.byteSlice(); + inline for (values, 0..) |val, i| { + if (bun.strings.eqlComptimeCheckLenWithType(u16, u16_bytes, comptime bun.strings.toUTF16Literal(val), check_len)) { + return i; + } + } + + return null; + } + + pub fn indexOfComptimeArrayAssumeSameLength(this: String, comptime values: []const []const u8) ?usize { + if (this.is8Bit()) { + const bytes = this.byteSlice(); + + inline for (0..values.len) |i| { + std.debug.assert(bytes.len == values[i].len); + if (bun.strings.eqlComptimeCheckLenWithType(u8, bytes, values[i], false)) { + return i; + } + } + + return null; + } + + const u16_bytes = this.utf16(); + var buffer: [values[0].len]u8 = undefined; + inline for (0..values[0].len) |i| { + const uchar = u16_bytes[i]; + if (uchar > 255) + return null; + + buffer[i] = @intCast(u8, uchar); + } + + inline for (0..values.len) |i| { + if (bun.strings.eqlComptimeCheckLenWithType(u8, &buffer, values[i], false)) { + return i; + } + } + + return null; + } + + pub fn inMap(this: String, comptime ComptimeStringMap: anytype) ?ComptimeStringMap.Value { + return ComptimeStringMap.getWithEqlList(this, indexOfComptimeArrayAssumeSameLength); + } + + pub fn inMapCaseInsensitive(this: String, comptime ComptimeStringMap: anytype) ?ComptimeStringMap.Value { + return ComptimeStringMap.getWithEqlList(this, indexOfComptimeArrayCaseInsensitiveSameLength); + } + + pub fn indexOfComptimeArrayCaseInsensitiveSameLength(this: String, comptime values: []const []const u8) ?usize { + if (this.is8Bit()) { + const bytes = this.byteSlice(); + + inline for (0..values.len) |i| { + std.debug.assert(bytes.len == values[i].len); + if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(bytes, values[i])) { + return i; + } + } + + return null; + } + + const u16_bytes = this.utf16(); + const buffer: [values[0].len]u8 = brk: { + var bytes: [values[0].len]u8 = undefined; + for (&bytes, u16_bytes) |*byte, uchar| { + if (uchar > 255) + return null; + + byte.* = @intCast(u8, uchar); + } + break :brk bytes; + }; + + inline for (0..values.len) |i| { + if (bun.strings.eqlCaseInsensitiveASCIIIgnoreLength(&buffer, values[i])) { + return i; + } + } + + return null; + } + pub fn hasPrefixComptime(this: String, comptime value: []const u8) bool { - _ = value; - _ = this; - return false; - // return this.toZigString().substring(0, value.len).eqlComptime(value); + return this.toZigString().substring(0, value.len).eqlComptime(value); } pub fn isWTFAllocator(this: std.mem.Allocator) bool { @@ -492,3 +629,21 @@ pub const String = extern struct { return this.toZigString().eql(other.toZigString()); } }; + +pub const SliceWithUnderlyingString = struct { + utf8: ZigString.Slice, + underlying: String, + + pub fn deinit(this: SliceWithUnderlyingString) void { + this.utf8.deinit(); + this.underlying.deref(); + } + + pub fn slice(this: SliceWithUnderlyingString) []const u8 { + return this.utf8.slice(); + } + + pub fn toJS(this: SliceWithUnderlyingString, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return this.underlying.toJS(globalObject); + } +}; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 8aaa6f8e8..9e069bb99 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -74,6 +74,10 @@ pub inline fn containsComptime(self: string, comptime str: string) bool { } pub const includes = contains; +pub fn inMapCaseInsensitive(self: string, comptime ComptimeStringMap: anytype) ?ComptimeStringMap.Value { + return bun.String.static(self).inMapCaseInsensitive(ComptimeStringMap); +} + pub inline fn containsAny(in: anytype, target: string) bool { for (in) |str| if (contains(if (@TypeOf(str) == u8) &[1]u8{str} else bun.span(str), target)) return true; return false; @@ -1071,7 +1075,11 @@ pub fn index(self: string, str: string) i32 { } pub fn eqlUtf16(comptime self: string, other: []const u16) bool { - return std.mem.eql(u16, toUTF16Literal(self), other); + if (self.len != other.len) return false; + + if (self.len == 0) return true; + + return bun.C.memcmp(bun.cast([*]const u8, self.ptr), bun.cast([*]const u8, other.ptr), self.len * @sizeOf(u16)) == 0; } pub fn toUTF8Alloc(allocator: std.mem.Allocator, js: []const u16) !string { diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index e9cafe957..999547c93 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -26,6 +26,9 @@ import fs, { constants, Dirent, Stats, + realpathSync, + readlinkSync, + symlinkSync, } from "node:fs"; import _promises from "node:fs/promises"; @@ -126,6 +129,18 @@ describe("mkdirSync", () => { expect(tempdir.includes(mkdirSync(tempdir, { recursive: true })!)).toBe(true); expect(existsSync(tempdir)).toBe(true); }); + + it("throws for invalid options", () => { + const path = `${tmpdir()}/${Date.now()}.rm.dir2/foo/bar`; + + expect(() => + mkdirSync( + path, + // @ts-expect-error + { recursive: "lalala" }, + ), + ).toThrow("recursive must be a boolean"); + }); }); it("readdirSync on import.meta.dir", () => { @@ -319,7 +334,25 @@ describe("readFileSync", () => { it("works with a file url", () => { gc(); - const text = readFileSync(new URL("file://" + import.meta.dir + "/readFileSync.txt"), "utf8"); + const text = readFileSync(new URL("./readFileSync.txt", import.meta.url), "utf8"); + gc(); + expect(text).toBe("File read successfully"); + }); + + it("works with a file path which contains spaces", async () => { + gc(); + const outpath = join(tmpdir(), "read file sync with space characters " + Math.random().toString(32) + " .txt"); + await Bun.write(outpath, Bun.file(Bun.fileURLToPath(new URL("./readFileSync.txt", import.meta.url)))); + const text = readFileSync(outpath, "utf8"); + gc(); + expect(text).toBe("File read successfully"); + }); + + it("works with a file URL which contains spaces", async () => { + gc(); + const outpath = join(tmpdir(), "read file sync with space characters " + Math.random().toString(32) + " .txt"); + await Bun.write(outpath, Bun.file(Bun.fileURLToPath(new URL("./readFileSync.txt", import.meta.url)))); + const text = readFileSync(new URL(outpath, import.meta.url), "utf8"); gc(); expect(text).toBe("File read successfully"); }); @@ -458,6 +491,28 @@ describe("lstat", () => { }); }); +it("symlink", () => { + const actual = join(tmpdir(), Math.random().toString(32) + "-fs-symlink.txt"); + try { + unlinkSync(actual); + } catch (e) {} + + symlinkSync(import.meta.path, actual); + + expect(realpathSync(actual)).toBe(realpathSync(import.meta.path)); +}); + +it("readlink", () => { + const actual = join(tmpdir(), Math.random().toString(32) + "-fs-readlink.txt"); + try { + unlinkSync(actual); + } catch (e) {} + + symlinkSync(import.meta.path, actual); + + expect(readlinkSync(actual)).toBe(realpathSync(import.meta.path)); +}); + describe("stat", () => { it("file metadata is correct", () => { const fileStats = statSync(new URL("./fs-stream.js", import.meta.url).toString().slice("file://".length - 1)); diff --git a/test/js/third_party/prisma/.gitignore b/test/js/third_party/prisma/.gitignore new file mode 100644 index 000000000..cc4c7e795 --- /dev/null +++ b/test/js/third_party/prisma/.gitignore @@ -0,0 +1 @@ +**/client diff --git a/test/js/third_party/prisma/prisma/sqlite/migrations/20230618153912_init/migration.sql b/test/js/third_party/prisma/prisma/sqlite/migrations/20230618153912_init/migration.sql new file mode 100644 index 000000000..c4ab82f18 --- /dev/null +++ b/test/js/third_party/prisma/prisma/sqlite/migrations/20230618153912_init/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "testId" INTEGER NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT +); + +-- CreateTable +CREATE TABLE "Post" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "testId" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT, + "published" BOOLEAN NOT NULL DEFAULT false, + "authorId" INTEGER NOT NULL, + CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); diff --git a/test/js/third_party/prisma/prisma/sqlite/migrations/migration_lock.toml b/test/js/third_party/prisma/prisma/sqlite/migrations/migration_lock.toml new file mode 100644 index 000000000..e5e5c4705 --- /dev/null +++ b/test/js/third_party/prisma/prisma/sqlite/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite"
\ No newline at end of file |