diff options
Diffstat (limited to 'src/javascript/jsc')
-rw-r--r-- | src/javascript/jsc/api/html_rewriter.zig | 107 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/bindings.cpp | 17 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/bindings.zig | 10 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/headers-cpp.h | 2 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/headers.h | 6 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/headers.zig | 2 | ||||
-rw-r--r-- | src/javascript/jsc/javascript.zig | 24 | ||||
-rw-r--r-- | src/javascript/jsc/node/types.zig | 7 | ||||
-rw-r--r-- | src/javascript/jsc/webcore/response.zig | 718 |
9 files changed, 804 insertions, 89 deletions
diff --git a/src/javascript/jsc/api/html_rewriter.zig b/src/javascript/jsc/api/html_rewriter.zig index e314fc18d..3ea438556 100644 --- a/src/javascript/jsc/api/html_rewriter.zig +++ b/src/javascript/jsc/api/html_rewriter.zig @@ -208,20 +208,28 @@ pub const HTMLRewriter = struct { this.context.deinit(bun.default_allocator); } + pub fn beginTransform(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { + const new_context = this.context; + this.context = .{}; + return BufferOutputSink.init(new_context, global, response, this.builder); + } + + pub fn returnEmptyResponse(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { + var result = bun.default_allocator.create(Response) catch unreachable; + + response.cloneInto(result, getAllocator(global.ref())); + this.finalizeWithoutDestroy(); + return JSValue.fromRef(Response.makeMaybePooled(global.ref(), result)); + } + pub fn transform(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { var input = response.body.slice(); - if (input.len == 0) { - var result = bun.default_allocator.create(Response) catch unreachable; - - response.cloneInto(result, getAllocator(global.ref())); - this.finalizeWithoutDestroy(); - return JSValue.fromRef(Response.makeMaybePooled(global.ref(), result)); + if (input.len == 0 and !(response.body.value == .Blob and response.body.value.Blob.needsToReadFile())) { + return this.returnEmptyResponse(global, response); } - var new_context = this.context; - this.context = .{}; - return BufferOutputSink.init(new_context, global, response, this.builder); + return this.beginTransform(global, response); } pub const BufferOutputSink = struct { @@ -230,7 +238,7 @@ pub const HTMLRewriter = struct { rewriter: *LOLHTML.HTMLRewriter, context: LOLHTMLContext, response: *Response, - + input: JSC.WebCore.Blob = undefined, pub fn init(context: LOLHTMLContext, global: *JSGlobalObject, original: *Response, builder: *LOLHTML.HTMLRewriter.Builder) JSValue { var result = bun.default_allocator.create(Response) catch unreachable; var sink = bun.default_allocator.create(BufferOutputSink) catch unreachable; @@ -252,7 +260,7 @@ pub const HTMLRewriter = struct { sink.rewriter = builder.build( .UTF8, .{ - .preallocated_parsing_buffer_size = original.body.len(), + .preallocated_parsing_buffer_size = @maximum(original.body.len(), 1024), .max_allowed_memory_usage = std.math.maxInt(u32), }, false, @@ -282,19 +290,7 @@ pub const HTMLRewriter = struct { }, }, }; - { - var input = original.body.value.use(); - sink.bytes.growBy(input.sharedView().len) catch unreachable; - defer input.detach(); - sink.rewriter.write(input.sharedView()) catch { - sink.deinit(); - bun.default_allocator.destroy(result); - - return throwLOLHTMLError(global); - }; - } - // Hold off on cloning until we're actually done. result.body.init.headers = original.body.init.headers; result.body.init.method = original.body.init.method; result.body.init.status_code = original.body.init.status_code; @@ -302,17 +298,72 @@ pub const HTMLRewriter = struct { result.url = bun.default_allocator.dupe(u8, original.url) catch unreachable; result.status_text = bun.default_allocator.dupe(u8, original.status_text) catch unreachable; + var input: JSC.WebCore.Blob = original.body.value.use(); + + const is_pending = input.needsToReadFile(); + defer if (!is_pending) input.detach(); + + if (input.needsToReadFile()) { + input.doReadFileInternal(*BufferOutputSink, sink, onFinishedLoading, global); + } else if (sink.runOutputSink(input.sharedView())) |error_value| { + return error_value; + } + + // Hold off on cloning until we're actually done. + + return JSC.JSValue.fromRef( + Response.makeMaybePooled(sink.global.ref(), sink.response), + ); + } + + pub fn onFinishedLoading(sink: *BufferOutputSink, bytes: anyerror![]u8) void { + var input = sink.input; + defer input.detach(); + const data = bytes catch |err| { + if (sink.response.body.value == .Locked and sink.response.body.value.Locked.task == sink) { + sink.response.body.value = .{ .Empty = .{} }; + } + + sink.response.body.value.toError(err, sink.global); + sink.rewriter.end() catch {}; + sink.deinit(); + return; + }; + + _ = sink.runOutputSink(data, true); + } + + pub fn runOutputSink(sink: *BufferOutputSink, bytes: []const u8, is_async: bool) ?JSValue { + sink.bytes.growBy(bytes) catch unreachable; + var global = sink.global; + var response = sink.response; + sink.rewriter.write(bytes) catch { + sink.deinit(); + bun.default_allocator.destroy(sink); + + if (is_async) { + response.body.value.toErrorInstance(throwLOLHTMLError(global), global); + + return null; + } else { + return throwLOLHTMLError(global); + } + }; + sink.rewriter.end() catch { - result.finalize(); + if (!is_async) response.finalize(); sink.response = undefined; sink.deinit(); - return throwLOLHTMLError(global); + if (is_async) { + response.body.value.toErrorInstance(throwLOLHTMLError(global), global); + return null; + } else { + return throwLOLHTMLError(global); + } }; - return JSC.JSValue.fromRef( - Response.makeMaybePooled(sink.global.ref(), sink.response), - ); + return null; } pub const Sync = enum { suspended, pending, done }; diff --git a/src/javascript/jsc/bindings/bindings.cpp b/src/javascript/jsc/bindings/bindings.cpp index 85f7e6f1e..84d57a83c 100644 --- a/src/javascript/jsc/bindings/bindings.cpp +++ b/src/javascript/jsc/bindings/bindings.cpp @@ -246,6 +246,15 @@ unsigned char JSC__JSValue__jsType(JSC__JSValue JSValue0) return 0; } +JSC__JSValue JSC__JSPromise__asValue(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1) +{ + return JSC::JSValue::encode(JSC::JSValue(arg0)); +} +JSC__JSPromise* JSC__JSPromise__create(JSC__JSGlobalObject* arg0) +{ + return JSC::JSPromise::create(arg0->vm(), arg0->promiseStructure()); +} + // TODO: prevent this from allocating so much memory void JSC__JSValue___then(JSC__JSValue JSValue0, JSC__JSGlobalObject* globalObject, void* ctx, void (*ArgFn3)(JSC__JSGlobalObject* arg0, void* arg1, JSC__JSValue arg2, size_t arg3), void (*ArgFn4)(JSC__JSGlobalObject* arg0, void* arg1, JSC__JSValue arg2, size_t arg3)) { @@ -281,8 +290,12 @@ void JSC__JSValue___then(JSC__JSValue JSValue0, JSC__JSGlobalObject* globalObjec }); globalObject->vm().drainMicrotasks(); - JSC::JSPromise* promise = JSC::jsDynamicCast<JSC::JSPromise*>(globalObject->vm(), JSC::JSValue::decode(JSValue0).asCell()); - promise->performPromiseThen(globalObject, resolverFunction, rejecterFunction, JSC::jsUndefined()); + auto* cell = JSC::JSValue::decode(JSValue0).asCell(); + if (JSC::JSPromise* promise = JSC::jsDynamicCast<JSC::JSPromise*>(globalObject->vm(), cell)) { + promise->performPromiseThen(globalObject, resolverFunction, rejecterFunction, JSC::jsUndefined()); + } else if (JSC::JSInternalPromise* promise = JSC::jsDynamicCast<JSC::JSInternalPromise*>(globalObject->vm(), cell)) { + promise->then(globalObject, resolverFunction, rejecterFunction); + } } JSC__JSValue JSC__JSValue__parseJSON(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1) diff --git a/src/javascript/jsc/bindings/bindings.zig b/src/javascript/jsc/bindings/bindings.zig index da65d38d5..8e0f6cfcd 100644 --- a/src/javascript/jsc/bindings/bindings.zig +++ b/src/javascript/jsc/bindings/bindings.zig @@ -731,6 +731,14 @@ pub const JSPromise = extern struct { cppFn("rejectAsHandledException", .{ this, globalThis, value }); } + pub fn create(globalThis: *JSGlobalObject) *JSPromise { + return cppFn("create", .{globalThis}); + } + + pub fn asValue(this: *JSPromise, globalThis: *JSGlobalObject) JSValue { + return cppFn("asValue", .{ this, globalThis }); + } + pub const Extern = [_][]const u8{ "rejectWithCaughtException", "status", @@ -745,6 +753,8 @@ pub const JSPromise = extern struct { "rejectAsHandledException", "rejectedPromiseValue", "resolvedPromiseValue", + "asValue", + "create", }; }; diff --git a/src/javascript/jsc/bindings/headers-cpp.h b/src/javascript/jsc/bindings/headers-cpp.h index 3e8e7af5d..495e3cdb6 100644 --- a/src/javascript/jsc/bindings/headers-cpp.h +++ b/src/javascript/jsc/bindings/headers-cpp.h @@ -1,4 +1,4 @@ -//-- AUTOGENERATED FILE -- 1647769923 +//-- AUTOGENERATED FILE -- 1647847672 // clang-format off #pragma once diff --git a/src/javascript/jsc/bindings/headers.h b/src/javascript/jsc/bindings/headers.h index fb72e3798..55e560ac7 100644 --- a/src/javascript/jsc/bindings/headers.h +++ b/src/javascript/jsc/bindings/headers.h @@ -1,5 +1,5 @@ // clang-format: off -//-- AUTOGENERATED FILE -- 1647769923 +//-- AUTOGENERATED FILE -- 1647847672 #pragma once #include <stddef.h> @@ -295,6 +295,8 @@ CPP_DECL bJSC__SourceCode JSC__JSModuleRecord__sourceCode(JSC__JSModuleRecord* a #pragma mark - JSC::JSPromise +CPP_DECL JSC__JSValue JSC__JSPromise__asValue(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1); +CPP_DECL JSC__JSPromise* JSC__JSPromise__create(JSC__JSGlobalObject* arg0); CPP_DECL bool JSC__JSPromise__isHandled(const JSC__JSPromise* arg0, JSC__VM* arg1); CPP_DECL void JSC__JSPromise__reject(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1, JSC__JSValue JSValue2); CPP_DECL void JSC__JSPromise__rejectAsHandled(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1, JSC__JSValue JSValue2); @@ -426,7 +428,7 @@ CPP_DECL size_t WTF__String__length(WTF__String* arg0); #pragma mark - JSC::JSValue -CPP_DECL void JSC__JSValue___then(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, void* arg2, void (* ArgFn3)(JSC__JSGlobalObject* arg0, void* arg1, JSC__JSValue arg2, size_t arg3), void (* ArgFn4)(JSC__JSGlobalObject* arg0, void* arg1, JSC__JSValue arg2, size_t arg3)); +CPP_DECL void JSC__JSValue___then(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, void* arg2, void (* ArgFn3)(JSC__JSGlobalObject* arg0, void* arg1, JSC__JSValue JSValue2, size_t arg3), void (* ArgFn4)(JSC__JSGlobalObject* arg0, void* arg1, JSC__JSValue JSValue2, size_t arg3)); CPP_DECL bool JSC__JSValue__asArrayBuffer_(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, Bun__ArrayBuffer* arg2); CPP_DECL JSC__JSCell* JSC__JSValue__asCell(JSC__JSValue JSValue0); CPP_DECL JSC__JSInternalPromise* JSC__JSValue__asInternalPromise(JSC__JSValue JSValue0); diff --git a/src/javascript/jsc/bindings/headers.zig b/src/javascript/jsc/bindings/headers.zig index f920c74d9..bf8ab5d75 100644 --- a/src/javascript/jsc/bindings/headers.zig +++ b/src/javascript/jsc/bindings/headers.zig @@ -167,6 +167,8 @@ pub extern fn JSC__JSModuleLoader__linkAndEvaluateModule(arg0: [*c]JSC__JSGlobal pub extern fn JSC__JSModuleLoader__loadAndEvaluateModule(arg0: [*c]JSC__JSGlobalObject, arg1: [*c]const ZigString) [*c]JSC__JSInternalPromise; pub extern fn JSC__JSModuleLoader__loadAndEvaluateModuleEntryPoint(arg0: [*c]JSC__JSGlobalObject, arg1: [*c]const JSC__SourceCode) [*c]JSC__JSInternalPromise; pub extern fn JSC__JSModuleRecord__sourceCode(arg0: [*c]JSC__JSModuleRecord) bJSC__SourceCode; +pub extern fn JSC__JSPromise__asValue(arg0: [*c]JSC__JSPromise, arg1: [*c]JSC__JSGlobalObject) JSC__JSValue; +pub extern fn JSC__JSPromise__create(arg0: [*c]JSC__JSGlobalObject) [*c]JSC__JSPromise; pub extern fn JSC__JSPromise__isHandled(arg0: [*c]const JSC__JSPromise, arg1: [*c]JSC__VM) bool; pub extern fn JSC__JSPromise__reject(arg0: [*c]JSC__JSPromise, arg1: [*c]JSC__JSGlobalObject, JSValue2: JSC__JSValue) void; pub extern fn JSC__JSPromise__rejectAsHandled(arg0: [*c]JSC__JSPromise, arg1: [*c]JSC__JSGlobalObject, JSValue2: JSC__JSValue) void; diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig index d0dcb9ddc..5c4c1e5ca 100644 --- a/src/javascript/jsc/javascript.zig +++ b/src/javascript/jsc/javascript.zig @@ -297,12 +297,14 @@ pub fn IOTask(comptime Context: type) type { const AsyncTransformTask = @import("./api/transpiler.zig").TransformTask.AsyncTransformTask; const BunTimerTimeoutTask = Bun.Timer.Timeout.TimeoutTask; +const ReadFileTask = WebCore.Blob.Store.ReadFile.ReadFileTask; // const PromiseTask = JSInternalPromise.Completion.PromiseTask; pub const Task = TaggedPointerUnion(.{ FetchTasklet, Microtask, AsyncTransformTask, BunTimerTimeoutTask, + ReadFileTask, // PromiseTask, // TimeoutTasklet, }); @@ -478,6 +480,7 @@ pub const VirtualMachine = struct { event_loop: *EventLoop = undefined, ref_strings: JSC.RefString.Map = undefined, + file_blobs: JSC.WebCore.Blob.Store.Map, source_mappings: SavedSourceMap = undefined, response_objects_pool: ?*Response.Pool = null, @@ -525,6 +528,11 @@ pub const VirtualMachine = struct { transform_task.*.runFromJS(); finished += 1; }, + @field(Task.Tag, @typeName(ReadFileTask)) => { + var transform_task: *ReadFileTask = task.get(ReadFileTask).?; + transform_task.*.runFromJS(); + finished += 1; + }, else => unreachable, } } @@ -732,6 +740,7 @@ pub const VirtualMachine = struct { .macro_entry_points = @TypeOf(VirtualMachine.vm.macro_entry_points).init(allocator), .origin_timer = std.time.Timer.start() catch @panic("Please don't mess with timers."), .ref_strings = JSC.RefString.Map.init(allocator), + .file_blobs = JSC.WebCore.Blob.Store.Map.init(allocator), }; VirtualMachine.vm.regular_event_loop.tasks = EventLoop.Queue.init( default_allocator, @@ -784,6 +793,21 @@ pub const VirtualMachine = struct { _ = VirtualMachine.vm.ref_strings.remove(ref_string.hash); } + pub fn getFileBlob(this: *VirtualMachine, pathlike: JSC.Node.PathOrFileDescriptor) ?*JSC.WebCore.Blob.Store { + const hash = pathlike.hash(); + return this.file_blobs.get(hash); + } + + pub fn putFileBlob(this: *VirtualMachine, pathlike: JSC.Node.PathOrFileDescriptor, store: *JSC.WebCore.Blob.Store) !void { + const hash = pathlike.hash(); + try this.file_blobs.put(hash, store); + } + + pub fn removeFileBlob(this: *VirtualMachine, pathlike: JSC.Node.PathOrFileDescriptor) void { + const hash = pathlike.hash(); + _ = this.file_blobs.remove(hash); + } + pub fn refCountedResolvedSource(this: *VirtualMachine, code: []const u8, specifier: []const u8, source_url: []const u8, hash_: ?u32) ResolvedSource { var source = this.refCountedString(code, hash_, true); diff --git a/src/javascript/jsc/node/types.zig b/src/javascript/jsc/node/types.zig index dfb1b63ed..2195dd789 100644 --- a/src/javascript/jsc/node/types.zig +++ b/src/javascript/jsc/node/types.zig @@ -510,6 +510,13 @@ pub const PathOrFileDescriptor = union(Tag) { pub const Tag = enum { fd, path }; + pub fn hash(this: PathOrFileDescriptor) u64 { + return switch (this) { + .path => std.hash.Wyhash.hash(0, this.path.slice()), + .fd => std.hash.Wyhash.hash(0, std.mem.asBytes(&this.fd)), + }; + } + pub fn copyToStream(this: PathOrFileDescriptor, flags: FileSystemFlags, auto_close: bool, mode: Mode, allocator: std.mem.Allocator, stream: *Stream) !void { switch (this) { .fd => |fd| { diff --git a/src/javascript/jsc/webcore/response.zig b/src/javascript/jsc/webcore/response.zig index 6e9859352..bdecc6df4 100644 --- a/src/javascript/jsc/webcore/response.zig +++ b/src/javascript/jsc/webcore/response.zig @@ -30,6 +30,7 @@ const JSPrivateDataPtr = @import("../base.zig").JSPrivateDataPtr; const GetJSPrivateData = @import("../base.zig").GetJSPrivateData; const Environment = @import("../../../env.zig"); const ZigString = JSC.ZigString; +const IdentityContext = @import("../../../identity_context.zig").IdentityContext; const JSInternalPromise = JSC.JSInternalPromise; const JSPromise = JSC.JSPromise; const JSValue = JSC.JSValue; @@ -85,6 +86,7 @@ pub const Response = struct { .@"json" = .{ .rfn = constructJSON }, .@"redirect" = .{ .rfn = constructRedirect }, .@"error" = .{ .rfn = constructError }, + .@"file" = .{ .rfn = constructFile }, }, .{}, ); @@ -392,6 +394,63 @@ pub const Response = struct { } } + pub fn constructFile( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, + ) js.JSObjectRef { + var args = JSC.Node.ArgumentsSlice.from(arguments); + + var response = Response{ + .body = Body{ + .init = Body.Init{ + .headers = null, + .status_code = 200, + }, + .value = Body.Value.empty, + }, + .allocator = getAllocator(ctx), + .url = "", + }; + + var path = JSC.Node.PathOrFileDescriptor.fromJS(ctx, &args, exception) orelse { + exception.* = JSC.toInvalidArguments("Expected file path string or file descriptor", .{}, ctx).asObjectRef(); + return js.JSValueMakeUndefined(ctx); + }; + + if (path == .path) { + path.path = .{ .string = bun.PathString.init(bun.default_allocator.dupe(u8, path.path.slice()) catch unreachable) }; + } + + if (args.nextEat()) |init| { + if (init.isUndefinedOrNull()) {} else if (init.isNumber()) { + response.body.init.status_code = @intCast(u16, @minimum(@maximum(0, init.toInt32()), std.math.maxInt(u16))); + } else { + if (Body.Init.init(getAllocator(ctx), ctx, init.asObjectRef()) catch null) |_init| { + response.body.init = _init; + } + } + } + + response.body.value = .{ + .Blob = brk: { + if (VirtualMachine.vm.getFileBlob(path)) |blob| { + blob.ref(); + break :brk Blob.initWithStore(blob, ctx.ptr()); + } + + break :brk Blob.initWithStore(Blob.Store.initFile(path, null, bun.default_allocator) catch unreachable, ctx.ptr()); + }, + }; + + var ptr = response.allocator.create(Response) catch unreachable; + ptr.* = response; + return Response.makeMaybePooled(ctx, ptr); + } + pub fn constructJSON( _: void, ctx: js.JSContextRef, @@ -445,15 +504,12 @@ pub const Response = struct { } else { if (Body.Init.init(getAllocator(ctx), ctx, init.asObjectRef()) catch null) |_init| { response.body.init = _init; - if (response.body.init.status_code == 0) { - response.body.init.status_code = 200; - } } } } var headers_ref = response.getOrCreateHeaders().leak(); - headers_ref.putHeaderNormalized("content-type", MimeType.json.value, false); + headers_ref.putDefaultHeader("content-type", MimeType.json.value); var ptr = response.allocator.create(Response) catch unreachable; ptr.* = response; @@ -1419,10 +1475,25 @@ pub const Headers = struct { headers.putHeader(key_slice.slice(), value_slice.slice(), append); } - pub fn putHeaderNormalized(headers: *Headers, key: []const u8, value: []const u8, comptime append: bool) void { + pub fn putDefaultHeader( + headers: *Headers, + key: []const u8, + value: []const u8, + ) void { + return putHeaderNormalizedDefault(headers, key, value, false, true); + } + + pub fn putHeaderNormalizedDefault( + headers: *Headers, + key: []const u8, + value: []const u8, + comptime append: bool, + comptime default: bool, + ) void { if (headers.getHeaderIndex(key)) |header_i| { - const existing_value = headers.entries.items(.value)[header_i]; + if (comptime default) return; + const existing_value = headers.entries.items(.value)[header_i]; if (append) { const end = @truncate(u32, value.len + existing_value.length + 2); const offset = headers.buf.items.len; @@ -1447,6 +1518,10 @@ pub const Headers = struct { } } + pub fn putHeaderNormalized(headers: *Headers, key: []const u8, value: []const u8, comptime append: bool) void { + return putHeaderNormalizedDefault(headers, key, value, append, false); + } + pub fn getHeaderIndex(headers: *const Headers, key: string) ?u32 { for (headers.entries.items(.name)) |name, i| { if (name.length == key.len and strings.eqlInsensitive(key, headers.asStr(name))) { @@ -1649,27 +1724,29 @@ pub const Blob = struct { globalThis: *JSGlobalObject = undefined, pub const Store = struct { - ptr: [*]u8 = undefined, - len: u32 = 0, + data: Data, + + mime_type: MimeType = MimeType.other, ref_count: u32 = 0, - cap: u32 = 0, - allocator: std.mem.Allocator, is_all_ascii: ?bool = null, + allocator: std.mem.Allocator, - pub inline fn ref(this: *Store) void { - this.ref_count += 1; + pub fn size(this: *const Store) u32 { + return switch (this.data) { + .bytes => this.data.bytes.len, + .file => std.math.maxInt(i32), + }; } - pub fn init(bytes: []u8, allocator: std.mem.Allocator) !*Store { - var store = try allocator.create(Store); - store.* = .{ - .ptr = bytes.ptr, - .len = @truncate(u32, bytes.len), - .ref_count = 1, - .cap = @truncate(u32, bytes.len), - .allocator = allocator, - }; - return store; + pub const Map = std.HashMap(u64, *JSC.WebCore.Blob.Store, IdentityContext(u64), 80); + + pub const Data = union(enum) { + bytes: ByteStore, + file: FileStore, + }; + + pub fn ref(this: *Store) void { + this.ref_count += 1; } pub fn external(ptr: ?*anyopaque, _: ?*anyopaque, _: usize) callconv(.C) void { @@ -1678,48 +1755,399 @@ pub const Blob = struct { this.deref(); } - pub fn fromArrayList(list: std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !*Store { - var store = try allocator.create(Store); + pub fn initFile(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?HTTPClient.MimeType, allocator: std.mem.Allocator) !*Store { + var store = try allocator.create(Blob.Store); store.* = .{ - .ptr = list.items.ptr, - .len = @truncate(u32, list.items.len), - .ref_count = 1, - .cap = @truncate(u32, list.capacity), + .data = .{ .file = FileStore.init(pathlike, mime_type) }, .allocator = allocator, + .ref_count = 1, }; return store; } - pub fn leakSlice(this: *const Store) []const u8 { - return this.ptr[0..this.len]; + pub fn init(bytes: []u8, allocator: std.mem.Allocator) !*Store { + var store = try allocator.create(Blob.Store); + store.* = .{ + .data = .{ .bytes = ByteStore.init(bytes, allocator) }, + .allocator = allocator, + .ref_count = 1, + }; + return store; } - pub fn slice(this: *Store) []u8 { - this.ref_count += 1; - return this.leakSlice(); - } + pub fn sharedView(this: Store) []u8 { + if (this.data == .bytes) + return this.data.bytes.slice(); - pub fn isOnlyOneRef(this: *const Store) bool { - return this.ref_count <= 1; + return &[_]u8{}; } - pub fn deref(this: *Store) void { + pub fn deref(this: *Blob.Store) void { this.ref_count -= 1; if (this.ref_count == 0) { - var allocated_slice = this.ptr[0..this.cap]; - var allocator = this.allocator; - allocator.free(allocated_slice); - allocator.destroy(this); + this.deinit(); } } - pub fn asArrayList(this: *Store) std.ArrayListUnmanaged(u8) { - this.ref_count += 1; + pub fn deinit(this: *Blob.Store) void { + switch (this.data) { + .bytes => |*bytes| { + bytes.deinit(); + }, + .file => |file| { + VirtualMachine.vm.removeFileBlob(file.pathlike); + }, + } + + this.allocator.destroy(this); + } + + pub fn fromArrayList(list: std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !*Blob.Store { + return try Blob.Store.init(list.items, allocator); + } + + pub const ReadFile = struct { + const OpenFrameType = if (Environment.isMac) + void + else + @Frame(ReadFile.getFdLinux); + file_store: FileStore, + byte_store: ByteStore = ByteStore{ .allocator = bun.default_allocator }, + store: ?*Store = null, + offset: u32 = 0, + max_length: u32 = std.math.maxInt(u32), + open_frame: OpenFrameType = undefined, + read_frame: @Frame(ReadFile.doRead) = undefined, + close_frame: @Frame(ReadFile.doClose) = undefined, + errno: ?anyerror = null, + open_completion: HTTPClient.NetworkThread.Completion = undefined, + opened_fd: JSC.Node.FileDescriptor = undefined, + read_completion: HTTPClient.NetworkThread.Completion = undefined, + read_len: u32 = 0, + read_off: u32 = 0, + size: u32 = 0, + buffer: []u8 = undefined, + runAsyncFrame: @Frame(ReadFile.runAsync) = undefined, + close_completion: HTTPClient.NetworkThread.Completion = undefined, + task: HTTPClient.NetworkThread.Task = undefined, + + onReadFileCompleteCtx: *anyopaque = undefined, + onReadFileComplete: OnReadFileCallback = undefined, + + pub const OnReadFileCallback = fn (ctx: *anyopaque, bytes: anyerror![]u8) void; + + const AsyncIO = HTTPClient.NetworkThread.AsyncIO; + + pub fn createWithCtx( + allocator: std.mem.Allocator, + store: *Store, + onReadFileContext: *anyopaque, + onReadFileComplete: OnReadFileCallback, + off: u32, + max_len: u32, + ) !*ReadFile { + var read_file = try allocator.create(ReadFile); + read_file.* = ReadFile{ + .file_store = store.data.file, + .offset = off, + .max_length = max_len, + .store = store, + .onReadFileCompleteCtx = onReadFileContext, + .onReadFileComplete = onReadFileComplete, + }; + store.ref(); + return read_file; + } + + pub fn create( + allocator: std.mem.Allocator, + store: *Store, + off: u32, + max_len: u32, + comptime Context: type, + context: Context, + comptime callback: fn (ctx: Context, bytes: anyerror![]u8) void, + ) !*ReadFile { + const Handler = struct { + pub fn run(ptr: *anyopaque, bytes: anyerror![]u8) void { + callback(bun.cast(Context, ptr), bytes); + } + }; + + return try ReadFile.createWithCtx(allocator, store, @ptrCast(*anyopaque, context), Handler.run, off, max_len); + } + + pub fn getFdMac(this: *ReadFile) AsyncIO.OpenError!JSC.Node.FileDescriptor { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + this.opened_fd = AsyncIO.openSync( + this.file_store.pathlike.path.sliceZ(&buf), + std.os.O.RDONLY, + ) catch |err| { + this.errno = err; + return err; + }; + return this.opened_fd; + } + + pub fn getFd(this: *ReadFile) AsyncIO.OpenError!JSC.Node.FileDescriptor { + if (this.file_store.pathlike == .fd) { + return this.file_store.pathlike.fd; + } + + if (comptime Environment.isMac) { + return try this.getFdMac(); + } else { + return try this.getFdLinux(); + } + } + + pub fn getFdLinux(this: *ReadFile) AsyncIO.OpenError!JSC.Node.FileDescriptor { + var aio = &AsyncIO.global; + + aio.open( + *ReadFile, + this, + onOpen, + &this.open_completion, + this.file_store.pathlike.path.sliceZ(), + std.os.O.RDONLY, + 0, + ); + + suspend { + this.open_frame = @frame().*; + } + + if (this.errno) |errno| { + return @errSetCast(AsyncIO.OpenError, errno); + } + + return this.opened_fd; + } + + pub fn doRead(this: *ReadFile) AsyncIO.ReadError!u32 { + var aio = &AsyncIO.global; + + var remaining = this.buffer[this.read_off..]; + this.read_len = 0; + aio.read( + *ReadFile, + this, + onRead, + &this.read_completion, + this.opened_fd, + remaining[0..@minimum(remaining.len, this.max_length - this.read_off)], + this.offset + this.read_off, + ); + + suspend { + this.read_frame = @frame().*; + } + + if (this.errno) |errno| { + return @errSetCast(AsyncIO.ReadError, errno); + } + + return this.read_len; + } + + pub fn doClose(this: *ReadFile) AsyncIO.CloseError!void { + var aio = &AsyncIO.global; + + aio.close( + *ReadFile, + this, + onClose, + &this.close_completion, + this.opened_fd, + ); + this.opened_fd = 0; + + suspend { + this.close_frame = @frame().*; + } + + if (this.errno) |errno| { + return @errSetCast(AsyncIO.CloseError, errno); + } + } + + pub const ReadFileTask = JSC.IOTask(@This()); + + pub fn then(this: *ReadFile, _: *JSC.JSGlobalObject) void { + var cb = this.onReadFileComplete; + var cb_ctx = this.onReadFileCompleteCtx; + + var store = this.store orelse { + var _err = this.errno orelse error.MissingData; + this.byte_store.deinit(); + bun.default_allocator.destroy(this); + cb(cb_ctx, _err); + return; + }; + + defer store.deref(); + if (this.file_store.pathlike == .path) { + VirtualMachine.vm.removeFileBlob(this.file_store.pathlike); + } + if (this.errno) |err| { + bun.default_allocator.destroy(this); + cb(cb_ctx, err); + return; + } + + var bytes = this.buffer; + if (store.data == .bytes) { + bun.default_allocator.free(this.buffer); + bytes = store.data.bytes.slice(); + } else if (store.data == .file) { + if (this.file_store.pathlike == .path) { + if (this.file_store.pathlike.path == .string) { + bun.default_allocator.free(this.file_store.pathlike.path.slice()); + } + } + store.data = .{ .bytes = ByteStore.init(bytes, bun.default_allocator) }; + } + + bun.default_allocator.destroy(this); + cb(cb_ctx, bytes); + } + pub fn run(this: *ReadFile, task: *ReadFileTask) void { + this.runAsyncFrame = async this.runAsync(task); + } + + pub fn onOpen(this: *ReadFile, _: *HTTPClient.NetworkThread.Completion, result: AsyncIO.OpenError!JSC.Node.FileDescriptor) void { + this.opened_fd = result catch |err| { + this.errno = err; + if (comptime Environment.isLinux) resume this.open_frame; + return; + }; + + if (comptime Environment.isLinux) resume this.open_frame; + } + + pub fn onRead(this: *ReadFile, _: *HTTPClient.NetworkThread.Completion, result: AsyncIO.ReadError!usize) void { + this.read_len = @truncate(u32, result catch |err| { + this.errno = err; + this.read_len = 0; + resume this.read_frame; + return; + }); + + resume this.read_frame; + } + + pub fn onClose(this: *ReadFile, _: *HTTPClient.NetworkThread.Completion, result: AsyncIO.CloseError!void) void { + result catch |err| { + this.errno = err; + resume this.close_frame; + return; + }; + + resume this.close_frame; + } + + pub fn runAsync(this: *ReadFile, task: *ReadFileTask) void { + defer task.onFinish(); + + const fd = this.getFd() catch return; + const needs_close = this.file_store.pathlike == .path; + const stat: std.os.Stat = switch (JSC.Node.Syscall.fstat(fd)) { + .result => |result| result, + .err => |err| { + this.errno = AsyncIO.asError(err.errno); + return; + }, + }; + if (!std.os.S.ISREG(stat.mode)) { + this.errno = error.ENOTSUP; + return; + } + + this.size = @minimum( + @truncate(u32, @intCast(u64, @maximum(@intCast(i64, stat.size), 0))), + this.max_length, + ); + if (this.size == 0) { + this.buffer = &[_]u8{}; + this.byte_store = ByteStore.init(this.buffer, bun.default_allocator); + + if (needs_close) { + this.doClose() catch {}; + } + return; + } + var bytes = bun.default_allocator.alloc(u8, this.size) catch |err| { + this.errno = err; + if (needs_close) { + this.doClose() catch {}; + } + return; + }; + this.buffer = bytes; + + var remain = bytes; + while (remain.len > 0) { + var read_len = this.doRead() catch { + if (needs_close) { + this.doClose() catch {}; + } + return; + }; + this.read_off += read_len; + if (read_len == 0) break; + remain = remain[read_len..]; + } + + _ = bun.default_allocator.resize(bytes, this.read_off); + this.buffer = bytes[0..this.read_off]; + this.byte_store = ByteStore.init(this.buffer, bun.default_allocator); + } + }; + }; + + pub const FileStore = struct { + pathlike: JSC.Node.PathOrFileDescriptor, + mime_type: HTTPClient.MimeType = HTTPClient.MimeType.other, + + pub fn init(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?HTTPClient.MimeType) FileStore { + return .{ .pathlike = pathlike, .mime_type = mime_type orelse HTTPClient.MimeType.other }; + } + }; + + pub const ByteStore = struct { + ptr: [*]u8 = undefined, + len: u32 = 0, + cap: u32 = 0, + allocator: std.mem.Allocator, + + pub fn init(bytes: []u8, allocator: std.mem.Allocator) ByteStore { + return .{ + .ptr = bytes.ptr, + .len = @truncate(u32, bytes.len), + .cap = @truncate(u32, bytes.len), + .allocator = allocator, + }; + } + + pub fn fromArrayList(list: std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !*ByteStore { + return ByteStore.init(list.items, allocator); + } + + pub fn slice(this: ByteStore) []u8 { + return this.ptr[0..this.len]; + } + + pub fn deinit(this: *ByteStore) void { + this.allocator.free(this.ptr[0..this.cap]); + } + + pub fn asArrayList(this: ByteStore) std.ArrayListUnmanaged(u8) { return this.asArrayListLeak(); } - pub fn asArrayListLeak(this: *const Store) std.ArrayListUnmanaged(u8) { + pub fn asArrayListLeak(this: ByteStore) std.ArrayListUnmanaged(u8) { return .{ .items = this.ptr[0..this.len], .capacity = this.cap, @@ -1772,6 +2200,10 @@ pub const Blob = struct { if (value.isError()) { return JSC.JSPromise.rejectedPromiseValue(global, value); } + + if (value.jsType() == .JSPromise) + return value; + return JSC.JSPromise.resolvedPromiseValue(global, value); } @@ -1940,9 +2372,31 @@ pub const Blob = struct { _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { + if (this.size == std.math.maxInt(i32)) { + this.resolveSize(); + if (this.size == std.math.maxInt(i32) and this.store != null) { + return JSValue.jsNumber(@as(u32, 0)).asRef(); + } + } + return JSValue.jsNumber(@truncate(u32, this.size)).asRef(); } + pub fn resolveSize(this: *Blob) void { + if (this.store) |store| { + if (store.data == .bytes) { + const offset = this.offset; + const store_size = store.size(); + if (store_size != std.math.maxInt(i32)) { + this.offset = @minimum(store_size, offset); + this.size = store_size - offset; + } + } + } else { + this.size = 0; + } + } + pub fn constructor( ctx: js.JSContextRef, _: js.JSObjectRef, @@ -2025,6 +2479,16 @@ pub const Blob = struct { }; } + pub fn initWithStore(store: *Blob.Store, globalThis: *JSGlobalObject) Blob { + return Blob{ + .size = store.size(), + .store = store, + .allocator = null, + .content_type = "", + .globalThis = globalThis, + }; + } + pub fn initEmpty(globalThis: *JSGlobalObject) Blob { return Blob{ .size = 0, @@ -2042,20 +2506,15 @@ pub const Blob = struct { } pub fn detach(this: *Blob) void { - if (this.store) |store| { - store.deref(); - this.store = null; - } + if (this.store != null) this.store.?.deref(); + this.store = null; } /// This does not duplicate /// This creates a new view /// and increment the reference count pub fn dupe(this: *const Blob) Blob { - if (this.store) |store| { - store.ref(); - } - + if (this.store != null) this.store.?.ref(); return this.*; } @@ -2070,12 +2529,16 @@ pub const Blob = struct { pub fn sharedView(this: *const Blob) []const u8 { if (this.size == 0 or this.store == null) return ""; - return this.store.?.leakSlice()[this.offset..][0..this.size]; + var slice_ = this.store.?.sharedView(); + if (slice_.len == 0) return ""; + slice_ = slice_[this.offset..]; + + return slice_[0..@minimum(slice_.len, @as(usize, this.size))]; } pub fn view(this: *const Blob) []const u8 { if (this.size == 0 or this.store == null) return ""; - return this.store.?.slice()[this.offset..][0..this.size]; + return this.store.?.sharedView()[this.offset..][0..this.size]; } pub const Lifetime = enum { @@ -2091,12 +2554,100 @@ pub const Blob = struct { // we can update the store's is_all_ascii flag // and any other Blob that points to the same store // can skip checking the encoding - if (this.size > 0 and this.store != null and this.offset == 0) { + if (this.size > 0 and this.offset == 0) { this.store.?.is_all_ascii = is_all_ascii; } } + pub fn NewReadFileHandler(comptime Function: anytype, comptime lifetime: Lifetime) type { + return struct { + context: Blob, + promise: *JSPromise, + globalThis: *JSGlobalObject, + pub fn run(handler: *@This(), bytes_: anyerror![]u8) void { + var promise = handler.promise; + var blob = handler.context; + blob.allocator = null; + var globalThis = handler.globalThis; + bun.default_allocator.destroy(handler); + var bytes = bytes_ catch |err| { + var error_string = ZigString.init( + std.fmt.allocPrint(bun.default_allocator, "Failed to read file: {s}", .{std.mem.span(@errorName(err))}) catch unreachable, + ); + error_string.mark(); + blob.detach(); + + promise.reject(globalThis, error_string.toErrorInstance(globalThis)); + return; + }; + + if (blob.size > 0) + blob.size = @minimum(@truncate(u32, bytes.len), blob.size); + + promise.resolve(globalThis, Function(&blob, globalThis, comptime lifetime)); + } + }; + } + + pub fn NewInternalReadFileHandler(comptime Context: type, comptime Function: anytype) type { + return struct { + context: Context, + + pub fn run(handler: *anyopaque, bytes_: anyerror![]u8) void { + Function(bun.cast(Context, handler.context), bytes_); + } + }; + } + + pub fn doReadFileInternal(this: *Blob, comptime Handler: type, ctx: Handler, comptime Function: anytype, global: *JSGlobalObject) void { + var file_read = Store.ReadFile.createWithCtx( + bun.default_allocator, + this.store.?, + this.offset, + this.size, + Handler, + ctx, + Function, + ) catch unreachable; + var read_file_task = Store.ReadFile.ReadFileTask.createOnJSThread(bun.default_allocator, global, file_read) catch unreachable; + read_file_task.schedule(); + } + + pub fn doReadFile(this: *Blob, comptime Function: anytype, comptime lifetime: Lifetime, global: *JSGlobalObject) JSValue { + const Handler = NewReadFileHandler(Function, lifetime); + var promise = JSPromise.create(global); + + var handler = Handler{ + .context = this.*, + .promise = promise, + .globalThis = global, + }; + + var ptr = bun.default_allocator.create(Handler) catch unreachable; + ptr.* = handler; + var file_read = Store.ReadFile.create( + bun.default_allocator, + this.store.?, + this.offset, + this.size, + *Handler, + ptr, + Handler.run, + ) catch unreachable; + var read_file_task = Store.ReadFile.ReadFileTask.createOnJSThread(bun.default_allocator, global, file_read) catch unreachable; + read_file_task.schedule(); + return promise.asValue(global); + } + + pub fn needsToReadFile(this: *const Blob) bool { + return this.store != null and this.store.?.data == .file; + } + pub fn toString(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue { + if (this.needsToReadFile()) { + return this.doReadFile(toString, lifetime, global); + } + var view_: []const u8 = this.sharedView(); @@ -2112,10 +2663,11 @@ pub const Blob = struct { // if toUTF16Alloc returns null, it means there are no non-ASCII characters // instead of erroring, invalid characters will become a U+FFFD replacement character if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch unreachable) |external| { + this.setIsASCIIFlag(false); + if (lifetime == .transfer) { this.detach(); } - this.setIsASCIIFlag(false); return ZigString.toExternalU16(external.ptr, external.len, global); } @@ -2143,7 +2695,15 @@ pub const Blob = struct { } } + pub fn toJSONShare(this: *Blob, global: *JSGlobalObject, comptime _: Lifetime) JSValue { + return toJSON(this, global); + } + pub fn toJSON(this: *Blob, global: *JSGlobalObject) JSValue { + if (this.needsToReadFile()) { + return this.doReadFile(toJSONShare, .share, global); + } + var view_ = this.sharedView(); if (view_.len == 0) @@ -2171,6 +2731,10 @@ pub const Blob = struct { ).parseJSON(global); } pub fn toArrayBuffer(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue { + if (this.needsToReadFile()) { + return this.doReadFile(toArrayBuffer, lifetime, global); + } + var view_ = this.sharedView(); if (view_.len == 0) @@ -2603,6 +3167,7 @@ pub const Body = struct { promise: ?JSValue = null, global: *JSGlobalObject, task: ?*anyopaque = null, + callback: ?fn (ctx: *anyopaque, value: *Value) void = null, deinit: bool = false, }; @@ -2611,12 +3176,14 @@ pub const Body = struct { Locked: PendingValue, Used: void, Empty: void, + Error: JSValue, pub const Tag = enum { Blob, Locked, Used, Empty, + Error, }; pub const empty = Value{ .Empty = .{} }; @@ -2640,6 +3207,41 @@ pub const Body = struct { } } + pub fn toErrorInstance(this: *Value, error_instance: JSC.JSValue, global: *JSGlobalObject) void { + if (this.value == .Locked) { + var locked = this.Locked; + locked.deinit = true; + if (locked.promise) |promise| { + if (promise.asInternalPromise()) |internal| { + internal.reject(global, error_instance); + } + + JSC.C.JSValueUnprotect(global.ref(), promise.asObjectRef()); + locked.promise = null; + } + + this.* = .{ .Error = error_instance }; + if (locked.callback) |callback| { + locked.callback = null; + callback(locked.task.?, this); + } + return; + } + + this.* = .{ .Error = error_instance }; + } + + pub fn toError(this: *Value, err: anyerror, global: *JSGlobalObject) void { + var error_str = ZigString.init(std.fmt.allocPrint( + bun.default_allocator, + "Error reading file {s}", + .{@errorName(err)}, + )); + error_str.mark(); + var error_instance = error_str.toErrorInstance(global); + return this.toErrorInstance(error_instance, global); + } + pub fn deinit(this: *Value) void { const tag = @as(Tag, this.*); if (tag == .Locked) { @@ -2651,6 +3253,10 @@ pub const Body = struct { this.Blob.deinit(); this.* = Value.empty; } + + if (tag == .Error) { + JSC.C.JSValueUnprotect(VirtualMachine.vm.global.vm(), this.Error.asObjectRef()); + } } pub fn clone(this: Value, _: std.mem.Allocator) Value { |