const std = @import("std"); const Api = @import("../../api/schema.zig").Api; const bun = @import("../../global.zig"); const RequestContext = @import("../../http.zig").RequestContext; const MimeType = @import("../../http.zig").MimeType; const ZigURL = @import("../../url.zig").URL; const HTTPClient = @import("http"); const NetworkThread = HTTPClient.NetworkThread; const AsyncIO = NetworkThread.AsyncIO; const JSC = @import("javascript_core"); const js = JSC.C; const Method = @import("../../http/method.zig").Method; const FetchHeaders = JSC.FetchHeaders; const ObjectPool = @import("../../pool.zig").ObjectPool; const SystemError = JSC.SystemError; const Output = @import("../../global.zig").Output; const MutableString = @import("../../global.zig").MutableString; const strings = @import("../../global.zig").strings; const string = @import("../../global.zig").string; const default_allocator = @import("../../global.zig").default_allocator; const FeatureFlags = @import("../../global.zig").FeatureFlags; const ArrayBuffer = @import("../base.zig").ArrayBuffer; const Properties = @import("../base.zig").Properties; const NewClass = @import("../base.zig").NewClass; const d = @import("../base.zig").d; const castObj = @import("../base.zig").castObj; const getAllocator = @import("../base.zig").getAllocator; 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; const JSError = JSC.JSError; const JSGlobalObject = JSC.JSGlobalObject; const VirtualMachine = @import("../javascript.zig").VirtualMachine; const Task = JSC.Task; const JSPrinter = @import("../../js_printer.zig"); const picohttp = @import("picohttp"); const StringJoiner = @import("../../string_joiner.zig"); const uws = @import("uws"); pub const Response = struct { pub const Pool = struct { response_objects_pool: [127]JSC.C.JSObjectRef = undefined, response_objects_used: u8 = 0, pub fn get(this: *Pool, ptr: *Response) ?JSC.C.JSObjectRef { if (comptime JSC.is_bindgen) unreachable; if (this.response_objects_used > 0) { var result = this.response_objects_pool[this.response_objects_used - 1]; this.response_objects_used -= 1; if (JSC.C.JSObjectSetPrivate(result, JSPrivateDataPtr.init(ptr).ptr())) { return result; } else { JSC.C.JSValueUnprotect(VirtualMachine.vm.global.ref(), result); } } return null; } pub fn push(this: *Pool, globalThis: *JSC.JSGlobalObject, object: JSC.JSValue) void { var remaining = this.response_objects_pool[@minimum(this.response_objects_used, this.response_objects_pool.len)..]; if (remaining.len == 0) { JSC.C.JSValueUnprotect(globalThis.ref(), object.asObjectRef()); return; } if (object.as(Response)) |resp| { _ = JSC.C.JSObjectSetPrivate(object.asObjectRef(), null); _ = resp.body.use(); resp.finalize(); remaining[0] = object.asObjectRef(); this.response_objects_used += 1; } } }; pub const Constructor = JSC.NewConstructor( Response, .{ .@"constructor" = constructor, .@"json" = .{ .rfn = constructJSON }, .@"redirect" = .{ .rfn = constructRedirect }, .@"error" = .{ .rfn = constructError }, }, .{}, ); pub const Class = NewClass( Response, .{ .name = "Response" }, .{ .@"finalize" = finalize, .@"text" = .{ .rfn = Response.getText, .ts = d.ts{}, }, .@"json" = .{ .rfn = Response.getJSON, .ts = d.ts{}, }, .@"arrayBuffer" = .{ .rfn = Response.getArrayBuffer, .ts = d.ts{}, }, .@"blob" = .{ .rfn = Response.getBlob, .ts = d.ts{}, }, .@"clone" = .{ .rfn = doClone, .ts = d.ts{}, }, }, .{ .@"url" = .{ .@"get" = getURL, .ro = true, }, .@"ok" = .{ .@"get" = getOK, .ro = true, }, .@"status" = .{ .@"get" = getStatus, .ro = true, }, .@"statusText" = .{ .@"get" = getStatusText, .ro = true, }, .@"headers" = .{ .@"get" = getHeaders, .ro = true, }, .@"bodyUsed" = .{ .@"get" = getBodyUsed, .ro = true, }, .@"type" = .{ .@"get" = getResponseType, .ro = true, }, }, ); allocator: std.mem.Allocator, body: Body, url: string = "", status_text: string = "", redirected: bool = false, pub fn getBodyValue( this: *Response, ) *Body.Value { return &this.body.value; } pub inline fn statusCode(this: *const Response) u16 { return this.body.init.status_code; } pub fn redirectLocation(this: *const Response) ?[]const u8 { return this.header("location"); } pub fn header(this: *const Response, comptime name: []const u8) ?[]const u8 { return (this.body.init.headers orelse return null).get(name); } pub const Props = struct {}; pub fn writeFormat(this: *const Response, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { const Writer = @TypeOf(writer); try formatter.writeIndent(Writer, writer); try writer.print("Response ({}) {{\n", .{bun.fmt.size(this.body.len())}); { formatter.indent += 1; defer formatter.indent -|= 1; try formatter.writeIndent(Writer, writer); try writer.writeAll("ok: "); formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.isOK()), .BooleanObject, enable_ansi_colors); formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable; try writer.writeAll("\n"); try this.body.writeFormat(formatter, writer, enable_ansi_colors); formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable; try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); try writer.writeAll("url: \""); try writer.print(comptime Output.prettyFmt("{s}", enable_ansi_colors), .{this.url}); try writer.writeAll("\""); formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable; try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); try writer.writeAll("statusText: "); try JSPrinter.writeJSONString(this.status_text, Writer, writer, false); formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable; try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); try writer.writeAll("redirected: "); formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.redirected), .BooleanObject, enable_ansi_colors); } try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); try writer.writeAll("}"); } pub fn isOK(this: *const Response) bool { return this.body.init.status_code == 304 or (this.body.init.status_code >= 200 and this.body.init.status_code <= 299); } pub fn getURL( this: *Response, ctx: js.JSContextRef, _: js.JSValueRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { // https://developer.mozilla.org/en-US/docs/Web/API/Response/url return ZigString.init(this.url).toValueGC(ctx.ptr()).asObjectRef(); } pub fn getResponseType( this: *Response, ctx: js.JSContextRef, _: js.JSValueRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { if (this.body.init.status_code < 200) { return ZigString.init("error").toValue(ctx.ptr()).asObjectRef(); } return ZigString.init("basic").toValue(ctx.ptr()).asObjectRef(); } pub fn getBodyUsed( this: *Response, _: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return JSC.JSValue.jsBoolean(this.body.value == .Used).asRef(); } pub fn getStatusText( this: *Response, ctx: js.JSContextRef, _: js.JSValueRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { // https://developer.mozilla.org/en-US/docs/Web/API/Response/url return ZigString.init(this.status_text).withEncoding().toValueGC(ctx.ptr()).asObjectRef(); } pub fn getOK( this: *Response, ctx: js.JSContextRef, _: js.JSValueRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { // https://developer.mozilla.org/en-US/docs/Web/API/Response/ok return js.JSValueMakeBoolean(ctx, this.isOK()); } fn getOrCreateHeaders(this: *Response) *FetchHeaders { if (this.body.init.headers == null) { this.body.init.headers = FetchHeaders.createEmpty(); } return this.body.init.headers.?; } pub fn getHeaders( this: *Response, ctx: js.JSContextRef, _: js.JSValueRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return this.getOrCreateHeaders().toJS(ctx.ptr()).asObjectRef(); } pub fn doClone( this: *Response, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, _: js.ExceptionRef, ) js.JSValueRef { var cloned = this.clone(getAllocator(ctx), ctx.ptr()); var val = Response.makeMaybePooled(ctx, cloned); if (this.body.init.headers) |headers| { cloned.body.init.headers = headers.cloneThis(); } return val; } pub fn makeMaybePooled(ctx: js.JSContextRef, ptr: *Response) JSC.C.JSObjectRef { if (comptime JSC.is_bindgen) unreachable; var vm = ctx.bunVM(); if (vm.response_objects_pool) |pool| { if (pool.get(ptr)) |object| { JSC.C.JSValueUnprotect(ctx, object); return object; } } return Response.Class.make(ctx, ptr); } pub fn cloneInto( this: *const Response, new_response: *Response, allocator: std.mem.Allocator, globalThis: *JSGlobalObject, ) void { new_response.* = Response{ .allocator = allocator, .body = this.body.clone(allocator, globalThis), .url = allocator.dupe(u8, this.url) catch unreachable, .status_text = allocator.dupe(u8, this.status_text) catch unreachable, .redirected = this.redirected, }; } pub fn clone(this: *const Response, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) *Response { var new_response = allocator.create(Response) catch unreachable; this.cloneInto(new_response, allocator, globalThis); return new_response; } pub usingnamespace BlobInterface(@This()); pub fn getStatus( this: *Response, ctx: js.JSContextRef, _: js.JSValueRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { // https://developer.mozilla.org/en-US/docs/Web/API/Response/status return js.JSValueMakeNumber(ctx, @intToFloat(f64, this.body.init.status_code)); } pub fn finalize( this: *Response, ) void { this.body.deinit(this.allocator); var allocator = this.allocator; if (this.status_text.len > 0) { allocator.free(this.status_text); } if (this.url.len > 0) { allocator.free(this.url); } allocator.destroy(this); } pub fn mimeType(response: *const Response, request_ctx_: ?*const RequestContext) string { return mimeTypeWithDefault(response, MimeType.other, request_ctx_); } pub fn mimeTypeWithDefault(response: *const Response, default: MimeType, request_ctx_: ?*const RequestContext) string { if (response.header("content-type")) |content_type| { // Remember, we always lowercase it // hopefully doesn't matter here tho return content_type; } if (request_ctx_) |request_ctx| { if (request_ctx.url.extname.len > 0) { return MimeType.byExtension(request_ctx.url.extname).value; } } switch (response.body.value) { .Blob => |blob| { if (blob.content_type.len > 0) { return blob.content_type; } return default.value; }, .Used, .Locked, .Empty, .Error => return default.value, } } pub fn constructJSON( _: void, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, _: js.ExceptionRef, ) js.JSObjectRef { // https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4 var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments); // var response = getAllocator(ctx).create(Response) catch unreachable; var response = Response{ .body = Body{ .init = Body.Init{ .status_code = 200, }, .value = Body.Value.empty, }, .allocator = getAllocator(ctx), .url = "", }; const json_value = args.nextEat() orelse JSC.JSValue.zero; if (@enumToInt(json_value) != 0) { var zig_str = JSC.ZigString.init(""); // calling JSON.stringify on an empty string adds extra quotes // so this is correct json_value.jsonStringify(ctx.ptr(), 0, &zig_str); if (zig_str.len > 0) { var zig_str_slice = zig_str.toSlice(getAllocator(ctx)); if (zig_str_slice.allocated) { response.body.value = .{ .Blob = Blob.initWithAllASCII(zig_str_slice.mut(), zig_str_slice.allocator, ctx.ptr(), false), }; } else { response.body.value = .{ .Blob = Blob.initWithAllASCII(getAllocator(ctx).dupe(u8, zig_str_slice.slice()) catch unreachable, zig_str_slice.allocator, ctx.ptr(), true), }; } } } 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; } } } var headers_ref = response.getOrCreateHeaders(); headers_ref.putDefault("content-type", MimeType.json.value); var ptr = response.allocator.create(Response) catch unreachable; ptr.* = response; return Response.makeMaybePooled(ctx, ptr); } pub fn constructRedirect( _: void, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, _: js.ExceptionRef, ) js.JSObjectRef { // https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4 var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments); // var response = getAllocator(ctx).create(Response) catch unreachable; var response = Response{ .body = Body{ .init = Body.Init{ .status_code = 302, }, .value = Body.Value.empty, }, .allocator = getAllocator(ctx), .url = "", }; const url_string_value = args.nextEat() orelse JSC.JSValue.zero; var url_string = ZigString.init(""); if (@enumToInt(url_string_value) != 0) { url_string = url_string_value.getZigString(ctx.ptr()); } var url_string_slice = url_string.toSlice(getAllocator(ctx)); defer url_string_slice.deinit(); 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.init.headers = response.getOrCreateHeaders(); response.body.init.status_code = 302; var headers_ref = response.body.init.headers.?; headers_ref.put("location", url_string_slice.slice()); var ptr = response.allocator.create(Response) catch unreachable; ptr.* = response; return Response.makeMaybePooled(ctx, ptr); } pub fn constructError( _: void, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, _: js.ExceptionRef, ) js.JSObjectRef { var response = getAllocator(ctx).create(Response) catch unreachable; response.* = Response{ .body = Body{ .init = Body.Init{ .status_code = 0, }, .value = Body.Value.empty, }, .allocator = getAllocator(ctx), .url = "", }; return Response.makeMaybePooled( ctx, response, ); } pub fn constructor( ctx: js.JSContextRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSObjectRef { const body: Body = brk: { switch (arguments.len) { 0 => { break :brk Body.@"200"(ctx); }, 1 => { break :brk Body.extract(ctx, arguments[0], exception); }, else => { if (js.JSValueGetType(ctx, arguments[1]) == js.JSType.kJSTypeObject) { break :brk Body.extractWithInit(ctx, arguments[0], arguments[1], exception); } else { break :brk Body.extract(ctx, arguments[0], exception); } }, } unreachable; }; var response = getAllocator(ctx).create(Response) catch unreachable; response.* = Response{ .body = body, .allocator = getAllocator(ctx), .url = "", }; return Response.makeMaybePooled( ctx, response, ); } }; const null_fd = std.math.maxInt(JSC.Node.FileDescriptor); pub const Fetch = struct { const headers_string = "headers"; const method_string = "method"; var fetch_body_string: MutableString = undefined; var fetch_body_string_loaded = false; const JSType = js.JSType; const fetch_error_no_args = "fetch() expects a string but received no arguments."; const fetch_error_blank_url = "fetch() URL must not be a blank string."; const JSTypeErrorEnum = std.enums.EnumArray(JSType, string); const fetch_type_error_names: JSTypeErrorEnum = brk: { var errors = JSTypeErrorEnum.initUndefined(); errors.set(JSType.kJSTypeUndefined, "Undefined"); errors.set(JSType.kJSTypeNull, "Null"); errors.set(JSType.kJSTypeBoolean, "Boolean"); errors.set(JSType.kJSTypeNumber, "Number"); errors.set(JSType.kJSTypeString, "String"); errors.set(JSType.kJSTypeObject, "Object"); errors.set(JSType.kJSTypeSymbol, "Symbol"); break :brk errors; }; const fetch_type_error_string_values = .{ std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeUndefined)}), std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeNull)}), std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeBoolean)}), std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeNumber)}), std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeString)}), std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeObject)}), std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeSymbol)}), }; const fetch_type_error_strings: JSTypeErrorEnum = brk: { var errors = JSTypeErrorEnum.initUndefined(); errors.set( JSType.kJSTypeUndefined, std.mem.span(fetch_type_error_string_values[0]), ); errors.set( JSType.kJSTypeNull, std.mem.span(fetch_type_error_string_values[1]), ); errors.set( JSType.kJSTypeBoolean, std.mem.span(fetch_type_error_string_values[2]), ); errors.set( JSType.kJSTypeNumber, std.mem.span(fetch_type_error_string_values[3]), ); errors.set( JSType.kJSTypeString, std.mem.span(fetch_type_error_string_values[4]), ); errors.set( JSType.kJSTypeObject, std.mem.span(fetch_type_error_string_values[5]), ); errors.set( JSType.kJSTypeSymbol, std.mem.span(fetch_type_error_string_values[6]), ); break :brk errors; }; pub const Class = NewClass( void, .{ .name = "fetch" }, .{ .@"call" = .{ .rfn = Fetch.call, .ts = d.ts{}, }, }, .{}, ); pub const FetchTasklet = struct { promise: *JSInternalPromise = undefined, http: HTTPClient.AsyncHTTP = undefined, status: Status = Status.pending, javascript_vm: *VirtualMachine = undefined, global_this: *JSGlobalObject = undefined, empty_request_body: MutableString = undefined, // pooled_body: *BodyPool.Node = undefined, this_object: js.JSObjectRef = null, resolve: js.JSObjectRef = null, reject: js.JSObjectRef = null, context: FetchTaskletContext = undefined, response_buffer: MutableString = undefined, blob_store: ?*Blob.Store = null, const Pool = ObjectPool(FetchTasklet, init, true, 32); const BodyPool = ObjectPool(MutableString, MutableString.init2048, true, 8); pub const FetchTaskletContext = struct { tasklet: *FetchTasklet, }; pub fn init(_: std.mem.Allocator) anyerror!FetchTasklet { return FetchTasklet{}; } pub const Status = enum(u8) { pending, running, done, }; pub fn onDone(this: *FetchTasklet) void { if (comptime JSC.is_bindgen) unreachable; var args = [1]js.JSValueRef{undefined}; var callback_object = switch (this.http.state.load(.Monotonic)) { .success => this.resolve, .fail => this.reject, else => unreachable, }; args[0] = switch (this.http.state.load(.Monotonic)) { .success => this.onResolve().asObjectRef(), .fail => this.onReject().asObjectRef(), else => unreachable, }; _ = js.JSObjectCallAsFunction(this.global_this.ref(), callback_object, null, 1, &args, null); this.release(); } pub fn reset(_: *FetchTasklet) void {} pub fn release(this: *FetchTasklet) void { js.JSValueUnprotect(this.global_this.ref(), this.resolve); js.JSValueUnprotect(this.global_this.ref(), this.reject); js.JSValueUnprotect(this.global_this.ref(), this.this_object); this.global_this = undefined; this.javascript_vm = undefined; this.promise = undefined; this.status = Status.pending; // var pooled = this.pooled_body; // BodyPool.release(pooled); // this.pooled_body = undefined; this.http = undefined; this.this_object = null; this.resolve = null; this.reject = null; Pool.release(@fieldParentPtr(Pool.Node, "data", this)); } pub const FetchResolver = struct { pub fn call( _: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: usize, arguments: [*c]const js.JSValueRef, _: js.ExceptionRef, ) callconv(.C) js.JSObjectRef { return JSPrivateDataPtr.from(js.JSObjectGetPrivate(arguments[0])) .get(FetchTaskletContext).?.tasklet.onResolve().asObjectRef(); // return js.JSObjectGetPrivate(arguments[0]).? .tasklet.onResolve().asObjectRef(); } }; pub const FetchRejecter = struct { pub fn call( _: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: usize, arguments: [*c]const js.JSValueRef, _: js.ExceptionRef, ) callconv(.C) js.JSObjectRef { return JSPrivateDataPtr.from(js.JSObjectGetPrivate(arguments[0])) .get(FetchTaskletContext).?.tasklet.onReject().asObjectRef(); } }; pub fn onReject(this: *FetchTasklet) JSValue { if (this.blob_store) |store| { store.deref(); } const fetch_error = std.fmt.allocPrint( default_allocator, "fetch() failed – {s}\nurl: \"{s}\"", .{ @errorName(this.http.err orelse error.HTTPFail), this.http.url.href, }, ) catch unreachable; return ZigString.init(fetch_error).toErrorInstance(this.global_this); } pub fn onResolve(this: *FetchTasklet) JSValue { var allocator = default_allocator; var http_response = this.http.response.?; var response = allocator.create(Response) catch unreachable; if (this.blob_store) |store| { store.deref(); } response.* = Response{ .allocator = allocator, .url = allocator.dupe(u8, this.http.url.href) catch unreachable, .status_text = allocator.dupe(u8, http_response.status) catch unreachable, .redirected = this.http.redirect_count > 0, .body = .{ .init = .{ .headers = FetchHeaders.createFromPicoHeaders(this.global_this, http_response.headers), .status_code = @truncate(u16, http_response.status_code), }, .value = .{ .Blob = Blob.init(this.http.response_buffer.toOwnedSliceLeaky(), allocator, this.global_this), }, }, }; return JSValue.fromRef(Response.makeMaybePooled(@ptrCast(js.JSContextRef, this.global_this), response)); } pub fn get( allocator: std.mem.Allocator, method: Method, url: ZigURL, headers: Headers.Entries, headers_buf: string, request_body: ?*MutableString, timeout: usize, request_body_store: ?*Blob.Store, ) !*FetchTasklet.Pool.Node { var linked_list = FetchTasklet.Pool.get(allocator); linked_list.data.javascript_vm = VirtualMachine.vm; linked_list.data.empty_request_body = MutableString.init(allocator, 0) catch unreachable; // linked_list.data.pooled_body = BodyPool.get(allocator); linked_list.data.blob_store = request_body_store; linked_list.data.response_buffer = MutableString.initEmpty(allocator); linked_list.data.http = try HTTPClient.AsyncHTTP.init( allocator, method, url, headers, headers_buf, &linked_list.data.response_buffer, request_body orelse &linked_list.data.empty_request_body, timeout, ); linked_list.data.context = .{ .tasklet = &linked_list.data }; return linked_list; } pub fn queue( allocator: std.mem.Allocator, global: *JSGlobalObject, method: Method, url: ZigURL, headers: Headers.Entries, headers_buf: string, request_body: ?*MutableString, timeout: usize, request_body_store: ?*Blob.Store, ) !*FetchTasklet.Pool.Node { var node = try get(allocator, method, url, headers, headers_buf, request_body, timeout, request_body_store); node.data.promise = JSInternalPromise.create(global); node.data.global_this = global; node.data.http.callback = callback; var batch = NetworkThread.Batch{}; node.data.http.schedule(allocator, &batch); NetworkThread.global.pool.schedule(batch); VirtualMachine.vm.active_tasks +|= 1; return node; } pub fn callback(http_: *HTTPClient.AsyncHTTP) void { var task: *FetchTasklet = @fieldParentPtr(FetchTasklet, "http", http_); @atomicStore(Status, &task.status, Status.done, .Monotonic); task.javascript_vm.eventLoop().enqueueTaskConcurrent(Task.init(task)); } }; pub fn call( _: void, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSObjectRef { var globalThis = ctx.ptr(); if (arguments.len == 0) { const fetch_error = fetch_error_no_args; return JSPromise.rejectedPromiseValue(globalThis, ZigString.init(fetch_error).toErrorInstance(globalThis)).asRef(); } var headers: ?Headers = null; var body: MutableString = MutableString.initEmpty(bun.default_allocator); var method = Method.GET; var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments); var url: ZigURL = undefined; var first_arg = args.nextEat().?; var blob_store: ?*Blob.Store = null; if (first_arg.isString()) { var url_zig_str = ZigString.init(""); JSValue.fromRef(arguments[0]).toZigString(&url_zig_str, globalThis); var url_str = url_zig_str.slice(); if (url_str.len == 0) { const fetch_error = fetch_error_blank_url; return JSPromise.rejectedPromiseValue(globalThis, ZigString.init(fetch_error).toErrorInstance(globalThis)).asRef(); } if (url_str[0] == '/') { url_str = strings.append(getAllocator(ctx), VirtualMachine.vm.bundler.options.origin.origin, url_str) catch unreachable; } else { url_str = getAllocator(ctx).dupe(u8, url_str) catch unreachable; } NetworkThread.init() catch @panic("Failed to start network thread"); url = ZigURL.parse(url_str); if (arguments.len >= 2 and js.JSValueIsObject(ctx, arguments[1])) { var options = JSValue.fromRef(arguments[1]); if (options.get(ctx.ptr(), "method")) |method_| { var slice_ = method_.toSlice(ctx.ptr(), getAllocator(ctx)); defer slice_.deinit(); method = Method.which(slice_.slice()) orelse .GET; } if (options.get(ctx.ptr(), "headers")) |headers_| { if (headers_.as(FetchHeaders)) |headers__| { headers = Headers.from(headers__, bun.default_allocator) catch unreachable; // TODO: make this one pass } else if (FetchHeaders.createFromJS(ctx.ptr(), headers_)) |headers__| { headers = Headers.from(headers__, bun.default_allocator) catch unreachable; headers__.deref(); } } if (options.get(ctx.ptr(), "body")) |body__| { if (Blob.fromJS(ctx.ptr(), body__, true, false)) |new_blob| { if (new_blob.size > 0) { body = MutableString{ .list = std.ArrayListUnmanaged(u8){ .items = bun.constStrToU8(new_blob.sharedView()), .capacity = new_blob.size, }, .allocator = bun.default_allocator, }; blob_store = new_blob.store; } // transfer is unnecessary here because this is a new slice //new_blob.transfer(); } else |_| { return JSPromise.rejectedPromiseValue(globalThis, ZigString.init("fetch() received invalid body").toErrorInstance(globalThis)).asRef(); } } } } else if (first_arg.asCheckLoaded(Request)) |request| { url = ZigURL.parse(request.url.dupe(getAllocator(ctx)) catch unreachable); method = request.method; if (request.headers) |head| { headers = Headers.from(head, bun.default_allocator) catch unreachable; } var blob = request.body.use(); // TODO: make RequestBody _NOT_ a MutableString body = MutableString{ .list = std.ArrayListUnmanaged(u8){ .items = bun.constStrToU8(blob.sharedView()), .capacity = bun.constStrToU8(blob.sharedView()).len, }, .allocator = blob.allocator orelse bun.default_allocator, }; blob_store = blob.store; } else { const fetch_error = fetch_type_error_strings.get(js.JSValueGetType(ctx, arguments[0])); return JSPromise.rejectedPromiseValue(globalThis, ZigString.init(fetch_error).toErrorInstance(globalThis)).asRef(); } var header_entries: Headers.Entries = .{}; var header_buf: string = ""; if (headers) |head| { header_entries = head.entries; header_buf = head.buf.items; } var resolve = js.JSObjectMakeFunctionWithCallback(ctx, null, Fetch.FetchTasklet.FetchResolver.call); var reject = js.JSObjectMakeFunctionWithCallback(ctx, null, Fetch.FetchTasklet.FetchRejecter.call); js.JSValueProtect(ctx, resolve); js.JSValueProtect(ctx, reject); var request_body: ?*MutableString = null; if (body.list.items.len > 0) { var mutable = bun.default_allocator.create(MutableString) catch unreachable; mutable.* = body; request_body = mutable; } // var resolve = FetchTasklet.FetchResolver.Class.make(ctx: js.JSContextRef, ptr: *ZigType) var queued = FetchTasklet.queue( default_allocator, globalThis, method, url, header_entries, header_buf, request_body, std.time.ns_per_hour, blob_store, ) catch unreachable; queued.data.this_object = js.JSObjectMake(ctx, null, JSPrivateDataPtr.from(&queued.data.context).ptr()); js.JSValueProtect(ctx, queued.data.this_object); var promise = js.JSObjectMakeDeferredPromise(ctx, &resolve, &reject, exception); queued.data.reject = reject; queued.data.resolve = resolve; return promise; // queued.data.promise.create(globalThis: *JSGlobalObject) } }; // https://developer.mozilla.org/en-US/docs/Web/API/Headers pub const Headers = struct { pub usingnamespace HTTPClient.Headers; entries: Headers.Entries = .{}, buf: std.ArrayListUnmanaged(u8) = .{}, allocator: std.mem.Allocator, pub fn asStr(this: *const Headers, ptr: Api.StringPointer) []const u8 { return if (ptr.offset + ptr.length <= this.buf.items.len) this.buf.items[ptr.offset..][0..ptr.length] else ""; } pub fn from(headers_ref: *FetchHeaders, allocator: std.mem.Allocator) !Headers { var header_count: u32 = 0; var buf_len: u32 = 0; headers_ref.count(&header_count, &buf_len); var headers = Headers{ .entries = .{}, .buf = .{}, .allocator = allocator, }; headers.entries.ensureTotalCapacity(allocator, header_count) catch unreachable; headers.entries.len = header_count; headers.buf.ensureTotalCapacityPrecise(allocator, buf_len) catch unreachable; headers.buf.items.len = buf_len; var sliced = headers.entries.slice(); var names = sliced.items(.name); var values = sliced.items(.value); headers_ref.copyTo(names.ptr, values.ptr, headers.buf.items.ptr); return headers; } }; const PathOrBlob = union(enum) { path: JSC.Node.PathOrFileDescriptor, blob: Blob, pub fn fromJS(ctx: js.JSContextRef, args: *JSC.Node.ArgumentsSlice, exception: js.ExceptionRef) ?PathOrBlob { if (JSC.Node.PathOrFileDescriptor.fromJS(ctx, args, exception)) |path| { return PathOrBlob{ .path = .{ .path = .{ .string = bun.PathString.init((bun.default_allocator.dupeZ(u8, path.path.slice()) catch unreachable)[0..path.path.slice().len]), }, } }; } const arg = args.nextEat() orelse return null; if (arg.as(Blob)) |blob| { return PathOrBlob{ .blob = blob.dupe(), }; } return null; } }; pub const Blob = struct { size: SizeType = 0, offset: SizeType = 0, /// When set, the blob will be freed on finalization callbacks /// If the blob is contained in Response or Request, this must be null allocator: ?std.mem.Allocator = null, store: ?*Store = null, content_type: string = "", content_type_allocated: bool = false, /// JavaScriptCore strings are either latin1 or UTF-16 /// When UTF-16, they're nearly always due to non-ascii characters is_all_ascii: ?bool = null, globalThis: *JSGlobalObject = undefined, /// Max int of double precision /// 9 petabytes is probably enough for awhile /// We want to avoid coercing to a BigInt because that's a heap allocation /// and it's generally just harder to use pub const SizeType = u52; pub const max_size = std.math.maxInt(SizeType); const CopyFilePromiseHandler = struct { promise: *JSPromise, globalThis: *JSGlobalObject, pub fn run(handler: *@This(), blob_: Store.CopyFile.ResultType) void { var promise = handler.promise; var globalThis = handler.globalThis; bun.default_allocator.destroy(handler); var blob = blob_ catch |err| { var error_string = ZigString.init( std.fmt.allocPrint(bun.default_allocator, "Failed to write file \"{s}\"", .{std.mem.span(@errorName(err))}) catch unreachable, ); error_string.mark(); promise.reject(globalThis, error_string.toErrorInstance(globalThis)); return; }; var _blob = bun.default_allocator.create(Blob) catch unreachable; _blob.* = blob; _blob.allocator = bun.default_allocator; promise.resolve( globalThis, ); } }; const WriteFileWaitFromLockedValueTask = struct { file_blob: Blob, globalThis: *JSGlobalObject, promise: *JSPromise, pub fn thenWrap(this: *anyopaque, value: *Body.Value) void { then(bun.cast(*WriteFileWaitFromLockedValueTask, this), value); } pub fn then(this: *WriteFileWaitFromLockedValueTask, value: *Body.Value) void { var promise = this.promise; var globalThis = this.globalThis; var file_blob = this.file_blob; switch (value.*) { .Error => |err| { file_blob.detach(); _ = value.use(); bun.default_allocator.destroy(this); promise.reject(globalThis, err); }, .Used => { file_blob.detach(); _ = value.use(); bun.default_allocator.destroy(this); promise.reject(globalThis, ZigString.init("Body was used after it was consumed").toErrorInstance(globalThis)); }, .Empty, .Blob => { var blob = value.use(); // TODO: this should be one promise not two! const new_promise = writeFileWithSourceDestination(globalThis.ref(), &blob, &file_blob); if (JSC.JSValue.fromRef(new_promise.?).asPromise()) |_promise| { switch (_promise.status(globalThis.vm())) { .Pending => { promise.resolve( globalThis, JSC.JSValue.fromRef(new_promise.?), ); }, .Rejected => { promise.reject(globalThis, _promise.result(globalThis.vm())); }, else => { promise.resolve(globalThis, _promise.result(globalThis.vm())); }, } } else if (JSC.JSValue.fromRef(new_promise.?).asInternalPromise()) |_promise| { switch (_promise.status(globalThis.vm())) { .Pending => { promise.resolve( globalThis, JSC.JSValue.fromRef(new_promise.?), ); }, .Rejected => { promise.reject(globalThis, _promise.result(globalThis.vm())); }, else => { promise.resolve(globalThis, _promise.result(globalThis.vm())); }, } } file_blob.detach(); bun.default_allocator.destroy(this); }, .Locked => { value.Locked.callback = thenWrap; value.Locked.task = this; }, } } }; pub fn writeFileWithSourceDestination( ctx: JSC.C.JSContextRef, source_blob: *Blob, destination_blob: *Blob, ) js.JSObjectRef { const destination_type = std.meta.activeTag(destination_blob.store.?.data); // Writing an empty string to a file is a no-op if (source_blob.store == null) { destination_blob.detach(); return JSC.JSPromise.resolvedPromiseValue(ctx.ptr(), JSC.JSValue.jsNumber(0)).asObjectRef(); } const source_type = std.meta.activeTag(source_blob.store.?.data); if (destination_type == .file and source_type == .bytes) { var write_file_promise = bun.default_allocator.create(WriteFilePromise) catch unreachable; write_file_promise.* = .{ .promise = JSC.JSPromise.create(ctx.ptr()), .globalThis = ctx.ptr(), }; JSC.C.JSValueProtect(ctx, write_file_promise.promise.asValue(ctx.ptr()).asObjectRef()); var file_copier = Store.WriteFile.create( bun.default_allocator, destination_blob.*, source_blob.*, *WriteFilePromise, write_file_promise, WriteFilePromise.run, ) catch unreachable; var task = Store.WriteFile.WriteFileTask.createOnJSThread(bun.default_allocator, ctx.ptr(), file_copier) catch unreachable; task.schedule(); return write_file_promise.promise.asValue(ctx.ptr()).asObjectRef(); } // If this is file <> file, we can just copy the file else if (destination_type == .file and source_type == .file) { var file_copier = Store.CopyFile.create( bun.default_allocator, destination_blob.store.?, source_blob.store.?, destination_blob.offset, destination_blob.size, ctx.ptr(), ) catch unreachable; file_copier.schedule(); return file_copier.promise.asObjectRef(); } else if (destination_type == .bytes and source_type == .bytes) { // If this is bytes <> bytes, we can just duplicate it // this is an edgecase // it will happen if someone did Bun.write(new Blob([123]), new Blob([456])) // eventually, this could be like Buffer.concat var clone = source_blob.dupe(); clone.allocator = bun.default_allocator; var cloned = bun.default_allocator.create(Blob) catch unreachable; cloned.* = clone; return JSPromise.resolvedPromiseValue(ctx.ptr(), JSC.JSValue.fromRef(Blob.Class.make(ctx, cloned))).asObjectRef(); } else if (destination_type == .bytes and source_type == .file) { return JSPromise.resolvedPromiseValue( ctx.ptr(), JSC.JSValue.fromRef( source_blob.getSlice(ctx, undefined, undefined, &.{}, null), ), ).asObjectRef(); } unreachable; } pub fn writeFile( _: void, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSObjectRef { var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments); // accept a path or a blob var path_or_blob = PathOrBlob.fromJS(ctx, &args, exception) orelse { exception.* = JSC.toInvalidArguments("Bun.write expects a path, file descriptor or a blob", .{}, ctx).asObjectRef(); return null; }; // if path_or_blob is a path, convert it into a file blob var destination_blob: Blob = if (path_or_blob == .path) Blob.findOrCreateFileFromPath(path_or_blob.path, ctx.ptr()) else path_or_blob.blob.dupe(); if (destination_blob.store == null) { exception.* = JSC.toInvalidArguments("Writing to an empty blob is not implemented yet", .{}, ctx).asObjectRef(); return null; } var data = args.nextEat() orelse { exception.* = JSC.toInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}, ctx).asObjectRef(); return null; }; if (data.isUndefinedOrNull() or data.isEmpty()) { exception.* = JSC.toInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}, ctx).asObjectRef(); return null; } // TODO: implement a writeev() fast path var source_blob: Blob = brk: { if (data.as(Response)) |response| { switch (response.body.value) { .Used, .Empty, .Blob => { break :brk response.body.use(); }, .Error => { destination_blob.detach(); const err = response.body.value.Error; JSC.C.JSValueUnprotect(ctx, err.asObjectRef()); _ = response.body.value.use(); return JSC.JSPromise.rejectedPromiseValue(ctx.ptr(), err).asObjectRef(); }, .Locked => { var task = bun.default_allocator.create(WriteFileWaitFromLockedValueTask) catch unreachable; var promise = JSC.JSPromise.create(ctx.ptr()); task.* = WriteFileWaitFromLockedValueTask{ .globalThis = ctx.ptr(), .file_blob = destination_blob, .promise = promise, }; response.body.value.Locked.task = task; response.body.value.Locked.callback = WriteFileWaitFromLockedValueTask.thenWrap; return promise.asValue(ctx.ptr()).asObjectRef(); }, } } if (data.as(Request)) |request| { switch (request.body) { .Used, .Empty, .Blob => { break :brk request.body.use(); }, .Error => { destination_blob.detach(); const err = request.body.Error; JSC.C.JSValueUnprotect(ctx, err.asObjectRef()); _ = request.body.use(); return JSC.JSPromise.rejectedPromiseValue(ctx.ptr(), err).asObjectRef(); }, .Locked => { var task = bun.default_allocator.create(WriteFileWaitFromLockedValueTask) catch unreachable; var promise = JSC.JSPromise.create(ctx.ptr()); task.* = WriteFileWaitFromLockedValueTask{ .globalThis = ctx.ptr(), .file_blob = destination_blob, .promise = promise, }; request.body.Locked.task = task; request.body.Locked.callback = WriteFileWaitFromLockedValueTask.thenWrap; return promise.asValue(ctx.ptr()).asObjectRef(); }, } } break :brk Blob.fromJS( ctx.ptr(), data, false, false, ) catch |err| { if (err == error.InvalidArguments) { exception.* = JSC.toInvalidArguments( "Expected an Array", .{}, ctx, ).asObjectRef(); return null; } exception.* = JSC.toInvalidArguments( "Out of memory", .{}, ctx, ).asObjectRef(); return null; }; }; return writeFileWithSourceDestination(ctx, &source_blob, &destination_blob); } 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(ctx.bunVM(), arguments); defer args.deinit(); 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); }; const blob = Blob.findOrCreateFileFromPath(path, ctx.ptr()); var ptr = bun.default_allocator.create(Blob) catch unreachable; ptr.* = blob; ptr.allocator = bun.default_allocator; return Blob.Class.make(ctx, ptr); } pub fn findOrCreateFileFromPath(path_: JSC.Node.PathOrFileDescriptor, globalThis: *JSGlobalObject) Blob { var path = path_; var vm = globalThis.bunVM(); if (vm.getFileBlob(path)) |blob| { blob.ref(); return Blob.initWithStore(blob, globalThis); } switch (path) { .path => { path.path = .{ .string = bun.PathString.init( (bun.default_allocator.dupeZ(u8, path.path.slice()) catch unreachable)[0..path.path.slice().len], ), }; }, .fd => { switch (path.fd) { std.os.STDIN_FILENO => return Blob.initWithStore( vm.rareData().stdin(), globalThis, ), std.os.STDERR_FILENO => return Blob.initWithStore( vm.rareData().stderr(), globalThis, ), std.os.STDOUT_FILENO => return Blob.initWithStore( vm.rareData().stdout(), globalThis, ), else => {}, } }, } const result = Blob.initWithStore(Blob.Store.initFile(path, null, bun.default_allocator) catch unreachable, globalThis); vm.putFileBlob(path, result.store.?) catch unreachable; return result; } pub const Store = struct { data: Data, mime_type: MimeType = MimeType.other, ref_count: u32 = 0, is_all_ascii: ?bool = null, allocator: std.mem.Allocator, pub fn size(this: *const Store) SizeType { return switch (this.data) { .bytes => this.data.bytes.len, .file => Blob.max_size, }; } 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 { if (ptr == null) return; var this = bun.cast(*Store, ptr); this.deref(); } pub fn initFile(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?HTTPClient.MimeType, allocator: std.mem.Allocator) !*Store { var store = try allocator.create(Blob.Store); store.* = .{ .data = .{ .file = FileStore.init( pathlike, mime_type orelse brk: { if (pathlike == .path) { const sliced = pathlike.path.slice(); if (sliced.len > 0) { var extname = std.fs.path.extension(sliced); extname = std.mem.trim(u8, extname, "."); if (HTTPClient.MimeType.byExtensionNoDefault(extname)) |mime| { break :brk mime; } } } break :brk null; }, ) }, .allocator = allocator, .ref_count = 1, }; return store; } 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 sharedView(this: Store) []u8 { if (this.data == .bytes) return this.data.bytes.slice(); return &[_]u8{}; } pub fn deref(this: *Blob.Store) void { this.ref_count -= 1; if (this.ref_count == 0) { this.deinit(); } } 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 fn FileOpenerMixin(comptime This: type) type { return struct { const __opener_flags = std.os.O.NONBLOCK | std.os.O.CLOEXEC; const open_flags_ = if (@hasDecl(This, "open_flags")) This.open_flags | __opener_flags else std.os.O.RDONLY | __opener_flags; pub fn getFdMac(this: *This) AsyncIO.OpenError!JSC.Node.FileDescriptor { var buf: [bun.MAX_PATH_BYTES]u8 = undefined; var path_string = if (@hasField(This, "file_store")) this.file_store.pathlike.path else this.file_blob.store.?.data.file.pathlike.path; var path = path_string.sliceZ(&buf); this.opened_fd = switch (JSC.Node.Syscall.open(path, open_flags_, JSC.Node.default_permission)) { .result => |fd| fd, .err => |err| { this.errno = AsyncIO.asError(err.errno); this.system_error = err.withPath(path_string.slice()).toSystemError(); return @errSetCast(AsyncIO.OpenError, this.errno.?); }, }; return this.opened_fd; } pub fn getFd(this: *This) AsyncIO.OpenError!JSC.Node.FileDescriptor { if (this.opened_fd != null_fd) { return this.opened_fd; } if (comptime Environment.isMac) { return try this.getFdMac(); } else { return try this.getFdLinux(); } } pub fn getFdLinux(this: *This) AsyncIO.OpenError!JSC.Node.FileDescriptor { var aio = &AsyncIO.global; var buf: [bun.MAX_PATH_BYTES]u8 = undefined; var path_string = if (@hasField(This, "file_store")) this.file_store.pathlike.path else this.file_blob.store.?.data.file.pathlike.path; var path = path_string.sliceZ(&buf); aio.open( *This, this, onOpen, &this.open_completion, path, open_flags_, JSC.Node.default_permission, ); suspend { this.open_frame = @frame().*; } if (this.errno) |errno| { this.system_error = .{ .syscall = ZigString.init("open"), .code = ZigString.init(std.mem.span(@errorName(errno))), .path = ZigString.init(path_string.slice()), }; return @errSetCast(AsyncIO.OpenError, errno); } return this.opened_fd; } pub fn onOpen(this: *This, completion: *HTTPClient.NetworkThread.Completion, result: AsyncIO.OpenError!JSC.Node.FileDescriptor) void { this.opened_fd = result catch { this.errno = AsyncIO.asError(-completion.result); if (comptime Environment.isLinux) resume this.open_frame; return; }; if (comptime Environment.isLinux) resume this.open_frame; } }; } pub fn FileCloserMixin(comptime This: type) type { return struct { pub fn doClose(this: *This) AsyncIO.CloseError!void { var aio = &AsyncIO.global; aio.close( *This, this, onClose, &this.close_completion, this.opened_fd, ); this.opened_fd = null_fd; suspend { this.close_frame = @frame().*; } if (@hasField(This, "errno")) { if (this.errno) |errno| { return @errSetCast(AsyncIO.CloseError, errno); } } } pub fn onClose(this: *This, _: *HTTPClient.NetworkThread.Completion, result: AsyncIO.CloseError!void) void { result catch |err| { if (@hasField(This, "errno")) { this.errno = err; } resume this.close_frame; return; }; resume this.close_frame; } }; } 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: SizeType = 0, max_length: SizeType = Blob.max_size, open_frame: OpenFrameType = undefined, read_frame: @Frame(ReadFile.doRead) = undefined, close_frame: @Frame(ReadFile.doClose) = undefined, open_completion: HTTPClient.NetworkThread.Completion = undefined, opened_fd: JSC.Node.FileDescriptor = null_fd, read_completion: HTTPClient.NetworkThread.Completion = undefined, read_len: SizeType = 0, read_off: SizeType = 0, size: SizeType = 0, buffer: []u8 = undefined, runAsyncFrame: @Frame(ReadFile.runAsync) = undefined, close_completion: HTTPClient.NetworkThread.Completion = undefined, task: HTTPClient.NetworkThread.Task = undefined, system_error: ?JSC.SystemError = null, errno: ?anyerror = null, onCompleteCtx: *anyopaque = undefined, onCompleteCallback: OnReadFileCallback = undefined, convert_to_byte_blob: bool = false, pub const Read = struct { buf: []u8, is_temporary: bool = false, }; pub const ResultType = SystemError.Maybe(Read); pub const OnReadFileCallback = fn (ctx: *anyopaque, bytes: ResultType) void; pub usingnamespace FileOpenerMixin(ReadFile); pub usingnamespace FileCloserMixin(ReadFile); pub fn createWithCtx( allocator: std.mem.Allocator, store: *Store, onReadFileContext: *anyopaque, onCompleteCallback: OnReadFileCallback, off: SizeType, max_len: SizeType, ) !*ReadFile { var read_file = try allocator.create(ReadFile); read_file.* = ReadFile{ .file_store = store.data.file, .offset = off, .max_length = max_len, .store = store, .onCompleteCtx = onReadFileContext, .onCompleteCallback = onCompleteCallback, }; store.ref(); return read_file; } pub fn create( allocator: std.mem.Allocator, store: *Store, off: SizeType, max_len: SizeType, comptime Context: type, context: Context, comptime callback: fn (ctx: Context, bytes: ResultType) void, ) !*ReadFile { const Handler = struct { pub fn run(ptr: *anyopaque, bytes: ResultType) void { callback(bun.cast(Context, ptr), bytes); } }; return try ReadFile.createWithCtx(allocator, store, @ptrCast(*anyopaque, context), Handler.run, off, max_len); } pub fn doRead(this: *ReadFile) AsyncIO.ReadError!SizeType { 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| { this.system_error = JSC.SystemError{ .code = ZigString.init(std.mem.span(@errorName(errno))), .path = if (this.file_store.pathlike == .path) ZigString.init(this.file_store.pathlike.path.slice()) else ZigString.Empty, .syscall = ZigString.init("read"), }; return @errSetCast(AsyncIO.ReadError, errno); } return this.read_len; } pub const ReadFileTask = JSC.IOTask(@This()); pub fn then(this: *ReadFile, _: *JSC.JSGlobalObject) void { var cb = this.onCompleteCallback; var cb_ctx = this.onCompleteCtx; if (this.store == null and this.system_error != null) { var system_error = this.system_error.?; bun.default_allocator.destroy(this); cb(cb_ctx, ResultType{ .err = system_error }); return; } else if (this.store == null) { bun.default_allocator.destroy(this); cb(cb_ctx, ResultType{ .err = SystemError{ .code = ZigString.init("INTERNAL_ERROR"), .path = ZigString.Empty, .message = ZigString.init("assertion failure - store should not be null"), .syscall = ZigString.init("read"), } }); return; } var store = this.store.?; if (this.convert_to_byte_blob and this.file_store.pathlike == .path) { VirtualMachine.vm.removeFileBlob(this.file_store.pathlike); } if (this.system_error) |err| { bun.default_allocator.destroy(this); store.deref(); cb(cb_ctx, ResultType{ .err = err }); return; } var buf = this.buffer; const is_temporary = !this.convert_to_byte_blob; if (this.convert_to_byte_blob) { if (store.data == .bytes) { bun.default_allocator.free(this.buffer); buf = 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(buf, bun.default_allocator) }; } } bun.default_allocator.destroy(this); // Attempt to free it as soon as possible if (store.ref_count > 1) { store.deref(); cb(cb_ctx, .{ .result = .{ .buf = buf, .is_temporary = is_temporary } }); } else { cb(cb_ctx, .{ .result = .{ .buf = buf, .is_temporary = is_temporary } }); store.deref(); } } pub fn run(this: *ReadFile, task: *ReadFileTask) void { var frame = HTTPClient.getAllocator().create(@Frame(runAsync)) catch unreachable; _ = @asyncCall(std.mem.asBytes(frame), undefined, runAsync, .{ this, task }); } pub fn onRead(this: *ReadFile, completion: *HTTPClient.NetworkThread.Completion, result: AsyncIO.ReadError!usize) void { this.read_len = @truncate(SizeType, result catch |err| { if (@hasField(HTTPClient.NetworkThread.Completion, "result")) { this.errno = AsyncIO.asError(-completion.result); this.system_error = (JSC.Node.Syscall.Error{ .errno = @intCast(JSC.Node.Syscall.Error.Int, -completion.result), .syscall = .read, }).toSystemError(); } else { this.errno = err; this.system_error = .{ .code = ZigString.init(std.mem.span(@errorName(err))), .syscall = ZigString.init("read") }; } this.read_len = 0; resume this.read_frame; return; }); resume this.read_frame; } fn runAsync(this: *ReadFile, task: *ReadFileTask) void { this.runAsync_(); task.onFinish(); suspend { HTTPClient.getAllocator().destroy(@frame()); } } fn runAsync_(this: *ReadFile) void { if (this.file_store.pathlike == .fd) { this.opened_fd = this.file_store.pathlike.fd; } const fd = this.getFd() catch return; const needs_close = this.file_store.pathlike == .path and fd != null_fd and fd > 2; const stat: std.os.Stat = switch (JSC.Node.Syscall.fstat(fd)) { .result => |result| result, .err => |err| { this.errno = AsyncIO.asError(err.errno); this.system_error = err.toSystemError(); return; }, }; if (std.os.S.ISDIR(stat.mode)) { this.errno = error.EISDIR; this.system_error = JSC.SystemError{ .code = ZigString.init("EISDIR"), .path = if (this.file_store.pathlike == .path) ZigString.init(this.file_store.pathlike.path.slice()) else ZigString.Empty, .message = ZigString.init("Directories cannot be read like files"), .syscall = ZigString.init("read"), }; return; } if (stat.size > 0 and std.os.S.ISREG(stat.mode)) { this.size = @minimum( @truncate(SizeType, @intCast(SizeType, @maximum(@intCast(i64, stat.size), 0))), this.max_length, ); // read up to 4k at a time if // they didn't explicitly set a size and we're reading from something that's not a regular file } else if (stat.size == 0 and !std.os.S.ISREG(stat.mode)) { this.size = if (this.max_length == Blob.max_size) 4096 else 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; this.convert_to_byte_blob = std.os.S.ISREG(stat.mode) and this.file_store.pathlike == .path; 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 WriteFile = struct { const OpenFrameType = if (Environment.isMac) void else @Frame(WriteFile.getFdLinux); file_blob: Blob, bytes_blob: Blob, opened_fd: JSC.Node.FileDescriptor = null_fd, open_frame: OpenFrameType = undefined, write_frame: @Frame(WriteFile.doWrite) = undefined, close_frame: @Frame(WriteFile.doClose) = undefined, system_error: ?JSC.SystemError = null, errno: ?anyerror = null, open_completion: HTTPClient.NetworkThread.Completion = undefined, write_completion: HTTPClient.NetworkThread.Completion = undefined, close_completion: HTTPClient.NetworkThread.Completion = undefined, task: HTTPClient.NetworkThread.Task = undefined, onCompleteCtx: *anyopaque = undefined, onCompleteCallback: OnWriteFileCallback = undefined, wrote: usize = 0, pub const ResultType = SystemError.Maybe(SizeType); pub const OnWriteFileCallback = fn (ctx: *anyopaque, count: ResultType) void; pub usingnamespace FileOpenerMixin(WriteFile); pub usingnamespace FileCloserMixin(WriteFile); // Do not open with APPEND because we may use pwrite() pub const open_flags = std.os.O.WRONLY | std.os.O.CREAT | std.os.O.TRUNC; pub fn createWithCtx( allocator: std.mem.Allocator, file_blob: Blob, bytes_blob: Blob, onWriteFileContext: *anyopaque, onCompleteCallback: OnWriteFileCallback, ) !*WriteFile { var read_file = try allocator.create(WriteFile); read_file.* = WriteFile{ .file_blob = file_blob, .bytes_blob = bytes_blob, .onCompleteCtx = onWriteFileContext, .onCompleteCallback = onCompleteCallback, }; file_blob.store.?.ref(); bytes_blob.store.?.ref(); return read_file; } pub fn create( allocator: std.mem.Allocator, file_blob: Blob, bytes_blob: Blob, comptime Context: type, context: Context, comptime callback: fn (ctx: Context, bytes: ResultType) void, ) !*WriteFile { const Handler = struct { pub fn run(ptr: *anyopaque, bytes: ResultType) void { callback(bun.cast(Context, ptr), bytes); } }; return try WriteFile.createWithCtx( allocator, file_blob, bytes_blob, @ptrCast(*anyopaque, context), Handler.run, ); } pub fn doWrite( this: *WriteFile, buffer: []const u8, file_offset: u64, ) AsyncIO.WriteError!SizeType { var aio = &AsyncIO.global; this.wrote = 0; const fd = this.opened_fd; aio.write( *WriteFile, this, onWrite, &this.write_completion, fd, buffer, if (fd > 2) file_offset else 0, ); suspend { this.write_frame = @frame().*; } if (this.errno) |errno| { this.system_error = this.system_error orelse JSC.SystemError{ .code = ZigString.init(std.mem.span(@errorName(errno))), .syscall = ZigString.init("write"), }; return @errSetCast(AsyncIO.WriteError, errno); } return @truncate(SizeType, this.wrote); } pub const WriteFileTask = JSC.IOTask(@This()); pub fn then(this: *WriteFile, _: *JSC.JSGlobalObject) void { var cb = this.onCompleteCallback; var cb_ctx = this.onCompleteCtx; this.bytes_blob.store.?.deref(); this.file_blob.store.?.deref(); if (this.system_error) |err| { bun.default_allocator.destroy(this); cb(cb_ctx, .{ .err = err, }); return; } const wrote = this.wrote; bun.default_allocator.destroy(this); cb(cb_ctx, .{ .result = @truncate(SizeType, wrote) }); } pub fn run(this: *WriteFile, task: *WriteFileTask) void { var frame = HTTPClient.getAllocator().create(@Frame(runAsync)) catch unreachable; _ = @asyncCall(std.mem.asBytes(frame), undefined, runAsync, .{ this, task }); } fn runAsync(this: *WriteFile, task: *WriteFileTask) void { this._runAsync(); task.onFinish(); suspend { HTTPClient.getAllocator().destroy(@frame()); } } pub fn onWrite(this: *WriteFile, _: *HTTPClient.NetworkThread.Completion, result: AsyncIO.WriteError!usize) void { this.wrote += @truncate(SizeType, result catch |err| { this.errno = err; this.wrote = 0; resume this.write_frame; return; }); resume this.write_frame; } fn _runAsync(this: *WriteFile) void { const file = this.file_blob.store.?.data.file; if (file.pathlike == .fd) { this.opened_fd = file.pathlike.fd; } const fd = this.getFd() catch return; const needs_close = file.pathlike == .path and fd > 2; var remain = this.bytes_blob.sharedView(); var total_written: usize = 0; var file_offset = this.file_blob.offset; const end = @minimum(this.file_blob.size, remain.len); while (remain.len > 0 and total_written < end) { const wrote_len = this.doWrite(remain, file_offset) catch { if (needs_close) { this.doClose() catch {}; } this.wrote = @truncate(SizeType, total_written); return; }; remain = remain[wrote_len..]; total_written += wrote_len; file_offset += wrote_len; if (wrote_len == 0) break; } this.wrote = @truncate(SizeType, total_written); if (needs_close) { this.doClose() catch {}; } } }; pub const IOWhich = enum { source, destination, both, }; const unsupported_directory_error = SystemError{ .errno = @intCast(c_int, @enumToInt(bun.C.SystemErrno.EISDIR)), .message = ZigString.init("That doesn't work on folders"), .syscall = ZigString.init("fstat"), }; const unsupported_non_regular_file_error = SystemError{ .errno = @intCast(c_int, @enumToInt(bun.C.SystemErrno.ENOTSUP)), .message = ZigString.init("Non-regular files aren't supported yet"), .syscall = ZigString.init("fstat"), }; // blocking, but off the main thread pub const CopyFile = struct { destination_file_store: FileStore, source_file_store: FileStore, store: ?*Store = null, source_store: ?*Store = null, offset: SizeType = 0, size: SizeType = 0, max_length: SizeType = Blob.max_size, destination_fd: JSC.Node.FileDescriptor = null_fd, source_fd: JSC.Node.FileDescriptor = null_fd, system_error: ?SystemError = null, read_len: SizeType = 0, read_off: SizeType = 0, globalThis: *JSGlobalObject, pub const ResultType = anyerror!SizeType; pub const Callback = fn (ctx: *anyopaque, len: ResultType) void; pub const CopyFilePromiseTask = JSC.ConcurrentPromiseTask(CopyFile); pub const CopyFilePromiseTaskEventLoopTask = CopyFilePromiseTask.EventLoopTask; pub fn create( allocator: std.mem.Allocator, store: *Store, source_store: *Store, off: SizeType, max_len: SizeType, globalThis: *JSC.JSGlobalObject, ) !*CopyFilePromiseTask { var read_file = try allocator.create(CopyFile); read_file.* = CopyFile{ .store = store, .source_store = source_store, .offset = off, .max_length = max_len, .globalThis = globalThis, .destination_file_store = store.data.file, .source_file_store = source_store.data.file, }; store.ref(); source_store.ref(); return try CopyFilePromiseTask.createOnJSThread(allocator, globalThis, read_file); } const linux = std.os.linux; const darwin = std.os.darwin; pub fn deinit(this: *CopyFile) void { if (this.source_file_store.pathlike == .path) { if (this.source_file_store.pathlike.path == .string and this.system_error == null) { bun.default_allocator.free(bun.constStrToU8(this.source_file_store.pathlike.path.slice())); } } this.store.?.deref(); bun.default_allocator.destroy(this); } pub fn reject(this: *CopyFile, promise: *JSC.JSInternalPromise) void { var globalThis = this.globalThis; var system_error: SystemError = this.system_error orelse SystemError{}; if (this.source_file_store.pathlike == .path and system_error.path.len == 0) { system_error.path = ZigString.init(this.source_file_store.pathlike.path.slice()); system_error.path.mark(); } if (system_error.message.len == 0) { system_error.message = ZigString.init("Failed to copy file"); } var instance = system_error.toErrorInstance(this.globalThis); if (this.store) |store| { store.deref(); } promise.reject(globalThis, instance); } pub fn then(this: *CopyFile, promise: *JSC.JSInternalPromise) void { this.source_store.?.deref(); if (this.system_error != null) { this.reject(promise); return; } promise.resolve(this.globalThis, JSC.JSValue.jsNumberFromUint64(this.read_len)); } pub fn run(this: *CopyFile) void { this.runAsync(); } pub fn doClose(this: *CopyFile) void { const close_input = this.destination_file_store.pathlike != .fd and this.destination_fd != null_fd; const close_output = this.source_file_store.pathlike != .fd and this.source_fd != null_fd; if (close_input and close_output) { this.doCloseFile(.both); } else if (close_input) { this.doCloseFile(.destination); } else if (close_output) { this.doCloseFile(.source); } } const os = std.os; pub fn doCloseFile(this: *CopyFile, comptime which: IOWhich) void { switch (which) { .both => { _ = JSC.Node.Syscall.close(this.destination_fd); _ = JSC.Node.Syscall.close(this.source_fd); }, .destination => { _ = JSC.Node.Syscall.close(this.destination_fd); }, .source => { _ = JSC.Node.Syscall.close(this.source_fd); }, } } const O = if (Environment.isLinux) linux.O else std.os.O; const open_destination_flags = O.CLOEXEC | O.CREAT | O.WRONLY | O.TRUNC; const open_source_flags = O.CLOEXEC | O.RDONLY; pub fn doOpenFile(this: *CopyFile, comptime which: IOWhich) !void { // open source file first // if it fails, we don't want the extra destination file hanging out if (which == .both or which == .source) { this.source_fd = switch (JSC.Node.Syscall.open( this.source_file_store.pathlike.path.sliceZAssume(), open_source_flags, 0, )) { .result => |result| result, .err => |errno| { this.system_error = errno.toSystemError(); return AsyncIO.asError(errno.errno); }, }; } if (which == .both or which == .destination) { this.destination_fd = switch (JSC.Node.Syscall.open( this.destination_file_store.pathlike.path.sliceZAssume(), open_destination_flags, JSC.Node.default_permission, )) { .result => |result| result, .err => |errno| { if (which == .both) { _ = JSC.Node.Syscall.close(this.source_fd); this.source_fd = 0; } this.system_error = errno.toSystemError(); return AsyncIO.asError(errno.errno); }, }; } } const TryWith = enum { sendfile, copy_file_range, splice, pub const tag = std.EnumMap(TryWith, JSC.Node.Syscall.Tag).init(.{ .sendfile = .sendfile, .copy_file_range = .copy_file_range, .splice = .splice, }); }; pub fn doCopyFileRange( this: *CopyFile, comptime use: TryWith, comptime clear_append_if_invalid: bool, ) anyerror!void { this.read_off += this.offset; var remain = @as(usize, this.max_length); if (remain == max_size or remain == 0) { // sometimes stat lies // let's give it 4096 and see how it goes remain = 4096; } var total_written: usize = 0; const src_fd = this.source_fd; const dest_fd = this.destination_fd; defer { this.read_len = @truncate(SizeType, total_written); } var has_unset_append = false; while (true) { const written = switch (comptime use) { .copy_file_range => linux.copy_file_range(src_fd, null, dest_fd, null, remain, 0), .sendfile => linux.sendfile(dest_fd, src_fd, null, remain), .splice => bun.C.splice(src_fd, null, dest_fd, null, remain, 0), }; switch (linux.getErrno(written)) { .SUCCESS => {}, .INVAL => { if (comptime clear_append_if_invalid) { if (!has_unset_append) { // https://kylelaker.com/2018/08/31/stdout-oappend.html // make() can set STDOUT / STDERR to O_APPEND // this messes up sendfile() has_unset_append = true; const flags = linux.fcntl(dest_fd, linux.F.GETFL, 0); if ((flags & O.APPEND) != 0) { _ = linux.fcntl(dest_fd, linux.F.SETFL, flags ^ O.APPEND); continue; } } } this.system_error = (JSC.Node.Syscall.Error{ .errno = @intCast(JSC.Node.Syscall.Error.Int, @enumToInt(linux.E.INVAL)), .syscall = TryWith.tag.get(use).?, }).toSystemError(); return AsyncIO.asError(linux.E.INVAL); }, else => |errno| { this.system_error = (JSC.Node.Syscall.Error{ .errno = @intCast(JSC.Node.Syscall.Error.Int, @enumToInt(errno)), .syscall = TryWith.tag.get(use).?, }).toSystemError(); return AsyncIO.asError(errno); }, } // wrote zero bytes means EOF remain -|= written; total_written += written; if (written == 0 or remain == 0) break; } } pub fn doFCopyFile(this: *CopyFile) anyerror!void { switch (JSC.Node.Syscall.fcopyfile(this.source_fd, this.destination_fd, os.system.COPYFILE_DATA)) { .err => |errno| { this.system_error = errno.toSystemError(); return AsyncIO.asError(errno.errno); }, .result => {}, } } pub fn doClonefile(this: *CopyFile) anyerror!void { var source_buf: [bun.MAX_PATH_BYTES]u8 = undefined; var dest_buf: [bun.MAX_PATH_BYTES]u8 = undefined; switch (JSC.Node.Syscall.clonefile( this.source_file_store.pathlike.path.sliceZ(&source_buf), this.destination_file_store.pathlike.path.sliceZ( &dest_buf, ), )) { .err => |errno| { this.system_error = errno.toSystemError(); return AsyncIO.asError(errno.errno); }, .result => {}, } } pub fn runAsync(this: *CopyFile) void { // defer task.onFinish(); var stat_: ?std.os.Stat = null; if (this.destination_file_store.pathlike == .fd) { this.destination_fd = this.destination_file_store.pathlike.fd; } if (this.source_file_store.pathlike == .fd) { this.source_fd = this.source_file_store.pathlike.fd; } // Do we need to open both files? if (this.destination_fd == null_fd and this.source_fd == null_fd) { // First, we attempt to clonefile() on macOS // This is the fastest way to copy a file. if (comptime Environment.isMac) { if (this.offset == 0 and this.source_file_store.pathlike == .path and this.destination_file_store.pathlike == .path) { do_clonefile: { // stat the output file, make sure it: // 1. Exists switch (JSC.Node.Syscall.stat(this.source_file_store.pathlike.path.sliceZAssume())) { .result => |result| { stat_ = result; if (os.S.ISDIR(result.mode)) { this.system_error = unsupported_directory_error; return; } if (!os.S.ISREG(result.mode)) break :do_clonefile; }, .err => |err| { // If we can't stat it, we also can't copy it. this.system_error = err.toSystemError(); return; }, } if (this.doClonefile()) { if (this.max_length != Blob.max_size and this.max_length < @intCast(SizeType, stat_.?.size)) { // If this fails...well, there's not much we can do about it. _ = bun.C.truncate( this.destination_file_store.pathlike.path.sliceZAssume(), @intCast(std.os.off_t, this.max_length), ); this.read_len = @intCast(SizeType, this.max_length); } else { this.read_len = @intCast(SizeType, stat_.?.size); } return; } else |_| { // this may still fail, in which case we just continue trying with fcopyfile // it can fail when the input file already exists // or if the output is not a directory // or if it's a network volume this.system_error = null; } } } } this.doOpenFile(.both) catch return; // Do we need to open only one file? } else if (this.destination_fd == null_fd) { this.source_fd = this.source_file_store.pathlike.fd; this.doOpenFile(.destination) catch return; // Do we need to open only one file? } else if (this.source_fd == null_fd) { this.destination_fd = this.destination_file_store.pathlike.fd; this.doOpenFile(.source) catch return; } if (this.system_error != null) { return; } std.debug.assert(this.destination_fd != null_fd); std.debug.assert(this.source_fd != null_fd); if (this.destination_file_store.pathlike == .fd) {} const stat: std.os.Stat = stat_ orelse switch (JSC.Node.Syscall.fstat(this.source_fd)) { .result => |result| result, .err => |err| { this.doClose(); this.system_error = err.toSystemError(); return; }, }; if (os.S.ISDIR(stat.mode)) { this.system_error = unsupported_directory_error; this.doClose(); return; } if (stat.size != 0) { this.max_length = @maximum(@minimum(@intCast(SizeType, stat.size), this.max_length), this.offset) - this.offset; if (this.max_length == 0) { this.doClose(); return; } if (os.S.ISREG(stat.mode) and this.max_length > std.mem.page_size and this.max_length != Blob.max_size) { bun.C.preallocate_file(this.destination_fd, 0, this.max_length) catch {}; } } if (comptime Environment.isLinux) { // Bun.write(Bun.file("a"), Bun.file("b")) if (os.S.ISREG(stat.mode) and (os.S.ISREG(this.destination_file_store.mode) or this.destination_file_store.mode == 0)) { if (this.destination_file_store.is_atty orelse false) { this.doCopyFileRange(.copy_file_range, true) catch {}; } else { this.doCopyFileRange(.copy_file_range, false) catch {}; } this.doClose(); return; } // $ bun run foo.js | bun run bar.js if (os.S.ISFIFO(stat.mode) and os.S.ISFIFO(this.destination_file_store.mode)) { if (this.destination_file_store.is_atty orelse false) { this.doCopyFileRange(.splice, true) catch {}; } else { this.doCopyFileRange(.splice, false) catch {}; } this.doClose(); return; } if (os.S.ISREG(stat.mode) or os.S.ISCHR(stat.mode) or os.S.ISSOCK(stat.mode)) { if (this.destination_file_store.is_atty orelse false) { this.doCopyFileRange(.sendfile, true) catch {}; } else { this.doCopyFileRange(.sendfile, false) catch {}; } this.doClose(); return; } this.system_error = unsupported_non_regular_file_error; this.doClose(); return; } if (comptime Environment.isMac) { this.doFCopyFile() catch { this.doClose(); return; }; if (stat.size != 0 and @intCast(SizeType, stat.size) > this.max_length) { _ = darwin.ftruncate(this.destination_fd, @intCast(std.os.off_t, this.max_length)); } this.doClose(); } else { @compileError("TODO: implement copyfile"); } } }; }; pub const FileStore = struct { pathlike: JSC.Node.PathOrFileDescriptor, mime_type: HTTPClient.MimeType = HTTPClient.MimeType.other, is_atty: ?bool = null, mode: JSC.Node.Mode = 0, seekable: ?bool = null, max_size: SizeType = 0, 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: SizeType = 0, cap: SizeType = 0, allocator: std.mem.Allocator, pub fn init(bytes: []u8, allocator: std.mem.Allocator) ByteStore { return .{ .ptr = bytes.ptr, .len = @truncate(SizeType, bytes.len), .cap = @truncate(SizeType, 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: ByteStore) std.ArrayListUnmanaged(u8) { return .{ .items = this.ptr[0..this.len], .capacity = this.cap, }; } }; pub const Constructor = JSC.NewConstructor( Blob, .{ .constructor = .{ .rfn = constructor }, }, .{}, ); pub const Class = NewClass( Blob, .{ .name = "Blob" }, .{ .finalize = finalize, .text = .{ .rfn = getText, }, .json = .{ .rfn = getJSON, }, .arrayBuffer = .{ .rfn = getArrayBuffer, }, .slice = .{ .rfn = getSlice, }, .stream = .{ .rfn = getStream, } }, .{ .@"type" = .{ .get = getType, .set = setType, }, .@"size" = .{ .get = getSize, .ro = true, }, }, ); pub fn getStream( this: *Blob, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) JSC.C.JSValueRef { var recommended_chunk_size: SizeType = 0; if (arguments.len > 0) { if (!JSValue.c(arguments[0]).isNumber() and !JSValue.c(arguments[0]).isUndefinedOrNull()) { JSC.throwInvalidArguments("chunkSize must be a number", .{}, ctx, exception); return null; } recommended_chunk_size = @intCast(SizeType, @maximum(0, @truncate(i52, JSValue.c(arguments[0]).toInt64()))); } return JSC.WebCore.ReadableStream.fromBlob( ctx.ptr(), this, recommended_chunk_size, ).asObjectRef(); } fn promisified( value: JSC.JSValue, global: *JSGlobalObject, ) JSC.JSValue { if (value.isError()) { return JSC.JSPromise.rejectedPromiseValue(global, value); } if (value.jsType() == .JSPromise) return value; return JSC.JSPromise.resolvedPromiseValue(global, value); } pub fn getText( this: *Blob, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, _: js.ExceptionRef, ) JSC.C.JSObjectRef { return promisified(this.toString(ctx.ptr(), .clone), ctx.ptr()).asObjectRef(); } pub fn getTextTransfer( this: *Blob, ctx: js.JSContextRef, ) JSC.C.JSObjectRef { return promisified(this.toString(ctx.ptr(), .transfer), ctx.ptr()).asObjectRef(); } pub fn getJSON( this: *Blob, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, _: js.ExceptionRef, ) JSC.C.JSObjectRef { return promisified(this.toJSON(ctx.ptr(), .share), ctx.ptr()).asObjectRef(); } pub fn getArrayBufferTransfer( this: *Blob, ctx: js.JSContextRef, ) JSC.C.JSObjectRef { return promisified(this.toArrayBuffer(ctx.ptr(), .transfer), ctx.ptr()).asObjectRef(); } pub fn getArrayBuffer( this: *Blob, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, _: js.ExceptionRef, ) JSC.C.JSObjectRef { return promisified(this.toArrayBuffer(ctx.ptr(), .clone), ctx.ptr()).asObjectRef(); } /// https://w3c.github.io/FileAPI/#slice-method-algo /// The slice() method returns a new Blob object with bytes ranging from the /// optional start parameter up to but not including the optional end /// parameter, and with a type attribute that is the value of the optional /// contentType parameter. It must act as follows: pub fn getSlice( this: *Blob, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, args: []const js.JSValueRef, exception: js.ExceptionRef, ) JSC.C.JSObjectRef { if (this.size == 0) { return constructor(ctx, null, &[_]js.JSValueRef{}, exception); } // If the optional start parameter is not used as a parameter when making this call, let relativeStart be 0. var relativeStart: i64 = 0; // If the optional end parameter is not used as a parameter when making this call, let relativeEnd be size. var relativeEnd: i64 = @intCast(i64, this.size); var args_iter = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), args); if (args_iter.nextEat()) |start_| { const start = start_.toInt64(); if (start < 0) { // If the optional start parameter is negative, let relativeStart be start + size. relativeStart = @intCast(i64, @maximum(start + @intCast(i64, this.size), 0)); } else { // Otherwise, let relativeStart be start. relativeStart = @minimum(@intCast(i64, start), @intCast(i64, this.size)); } } if (args_iter.nextEat()) |end_| { const end = end_.toInt64(); // If end is negative, let relativeEnd be max((size + end), 0). if (end < 0) { // If the optional start parameter is negative, let relativeStart be start + size. relativeEnd = @intCast(i64, @maximum(end + @intCast(i64, this.size), 0)); } else { // Otherwise, let relativeStart be start. relativeEnd = @minimum(@intCast(i64, end), @intCast(i64, this.size)); } } var content_type: string = ""; if (args_iter.nextEat()) |content_type_| { if (content_type_.isString()) { var zig_str = content_type_.getZigString(ctx.ptr()); var slicer = zig_str.toSlice(bun.default_allocator); defer slicer.deinit(); var slice = slicer.slice(); var content_type_buf = getAllocator(ctx).alloc(u8, slice.len) catch unreachable; content_type = strings.copyLowercase(slice, content_type_buf); } } const len = @intCast(SizeType, @maximum(relativeEnd - relativeStart, 0)); // This copies over the is_all_ascii flag // which is okay because this will only be a <= slice var blob = this.dupe(); blob.offset = @intCast(SizeType, relativeStart); blob.size = len; blob.content_type = content_type; blob.content_type_allocated = content_type.len > 0; var blob_ = getAllocator(ctx).create(Blob) catch unreachable; blob_.* = blob; blob_.allocator = getAllocator(ctx); return Blob.Class.make(ctx, blob_); } pub fn getType( this: *Blob, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return ZigString.init(this.content_type).toValue(ctx.ptr()).asObjectRef(); } pub fn setType( this: *Blob, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, value: js.JSValueRef, _: js.ExceptionRef, ) bool { var zig_str = JSValue.fromRef(value).getZigString(ctx.ptr()); if (zig_str.is16Bit()) return false; var slice = zig_str.trimmedSlice(); if (strings.eql(slice, this.content_type)) return true; const prev_content_type = this.content_type; { defer if (this.content_type_allocated) bun.default_allocator.free(prev_content_type); var content_type_buf = getAllocator(ctx).alloc(u8, slice.len) catch unreachable; this.content_type = strings.copyLowercase(slice, content_type_buf); } this.content_type_allocated = true; return true; } pub fn getSize( this: *Blob, _: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { if (this.size == Blob.max_size) { this.resolveSize(); if (this.size == Blob.max_size and this.store != null) { return JSValue.jsNumberFromChar(0).asRef(); } } if (this.size < std.math.maxInt(i32)) { return JSValue.jsNumber(this.size).asRef(); } return JSC.JSValue.jsNumberFromUint64(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 != Blob.max_size) { this.offset = @minimum(store_size, offset); this.size = store_size - offset; } } } else { this.size = 0; } } pub fn constructor( ctx: js.JSContextRef, _: js.JSObjectRef, args: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSObjectRef { var blob: Blob = undefined; switch (args.len) { 0 => { var empty: []u8 = &[_]u8{}; blob = Blob.init(empty, getAllocator(ctx), ctx.ptr()); }, else => { blob = fromJS(ctx.ptr(), JSValue.fromRef(args[0]), false, true) catch |err| { if (err == error.InvalidArguments) { JSC.JSError(getAllocator(ctx), "new Blob() expects an Array", .{}, ctx, exception); return null; } JSC.JSError(getAllocator(ctx), "out of memory :(", .{}, ctx, exception); return null; }; if (args.len > 1) { var options = JSValue.fromRef(args[1]); if (options.isCell()) { // type, the ASCII-encoded string in lower case // representing the media type of the Blob. // Normative conditions for this member are provided // in the § 3.1 Constructors. if (options.get(ctx.ptr(), "type")) |content_type| { if (content_type.isString()) { var content_type_str = content_type.getZigString(ctx.ptr()); if (!content_type_str.is16Bit()) { var slice = content_type_str.trimmedSlice(); var content_type_buf = getAllocator(ctx).alloc(u8, slice.len) catch unreachable; blob.content_type = strings.copyLowercase(slice, content_type_buf); blob.content_type_allocated = true; } } } } } if (blob.content_type.len == 0) { blob.content_type = ""; } }, } var blob_ = getAllocator(ctx).create(Blob) catch unreachable; blob_.* = blob; blob_.allocator = getAllocator(ctx); return Blob.Class.make(ctx, blob_); } pub fn finalize(this: *Blob) void { this.deinit(); } pub fn initWithAllASCII(bytes: []u8, allocator: std.mem.Allocator, globalThis: *JSGlobalObject, is_all_ascii: bool) Blob { // avoid allocating a Blob.Store if the buffer is actually empty var store: ?*Blob.Store = null; if (bytes.len > 0) { store = Blob.Store.init(bytes, allocator) catch unreachable; store.?.is_all_ascii = is_all_ascii; } return Blob{ .size = @truncate(SizeType, bytes.len), .store = store, .allocator = null, .content_type = "", .globalThis = globalThis, .is_all_ascii = is_all_ascii, }; } pub fn init(bytes: []u8, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) Blob { return Blob{ .size = @truncate(SizeType, bytes.len), .store = if (bytes.len > 0) Blob.Store.init(bytes, allocator) catch unreachable else null, .allocator = null, .content_type = "", .globalThis = globalThis, }; } pub fn initWithStore(store: *Blob.Store, globalThis: *JSGlobalObject) Blob { return Blob{ .size = store.size(), .store = store, .allocator = null, .content_type = if (store.data == .file) store.data.file.mime_type.value else "", .globalThis = globalThis, }; } pub fn initEmpty(globalThis: *JSGlobalObject) Blob { return Blob{ .size = 0, .store = null, .allocator = null, .content_type = "", .globalThis = globalThis, }; } // Transferring doesn't change the reference count // It is a move inline fn transfer(this: *Blob) void { this.store = null; } pub fn detach(this: *Blob) void { 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 != null) this.store.?.ref(); var duped = this.*; duped.allocator = null; return duped; } pub fn deinit(this: *Blob) void { this.detach(); if (this.allocator) |alloc| { this.allocator = null; alloc.destroy(this); } } pub fn sharedView(this: *const Blob) []const u8 { if (this.size == 0 or this.store == null) return ""; 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.?.sharedView()[this.offset..][0..this.size]; } pub const Lifetime = JSC.WebCore.Lifetime; pub fn setIsASCIIFlag(this: *Blob, is_all_ascii: bool) void { this.is_all_ascii = is_all_ascii; // if this Blob represents the entire binary data // which will be pretty common // 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.offset == 0 and this.store.?.data == .bytes) { 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_: Blob.Store.ReadFile.ResultType) void { var promise = handler.promise; var blob = handler.context; blob.allocator = null; var globalThis = handler.globalThis; bun.default_allocator.destroy(handler); switch (bytes_) { .result => |result| { const bytes = result.buf; const is_temporary = result.is_temporary; if (blob.size > 0) blob.size = @minimum(@truncate(u32, bytes.len), blob.size); if (!is_temporary) { promise.resolve(globalThis, Function(&blob, globalThis, bytes, comptime lifetime)); } else { promise.resolve(globalThis, Function(&blob, globalThis, bytes, .temporary)); } }, .err => |err| { promise.reject(globalThis, err.toErrorInstance(globalThis)); }, } } }; } pub const WriteFilePromise = struct { promise: *JSPromise, globalThis: *JSGlobalObject, pub fn run(handler: *@This(), count: Blob.Store.WriteFile.ResultType) void { var promise = handler.promise; var globalThis = handler.globalThis; bun.default_allocator.destroy(handler); switch (count) { .err => |err| { promise.reject(globalThis, err.toErrorInstance(globalThis)); }, .result => |wrote| { promise.resolve(globalThis, JSC.JSValue.jsNumberFromUint64(wrote)); }, } } }; pub fn NewInternalReadFileHandler(comptime Context: type, comptime Function: anytype) type { return struct { pub fn run(handler: *anyopaque, bytes_: Store.ReadFile.ResultType) void { Function(bun.cast(Context, handler), 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.?, ctx, NewInternalReadFileHandler(Handler, Function).run, this.offset, this.size, ) 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 toStringWithBytes(this: *Blob, global: *JSGlobalObject, buf: []const u8, comptime lifetime: Lifetime) JSValue { // null == unknown // false == can't be const could_be_all_ascii = this.is_all_ascii orelse this.store.?.is_all_ascii; if (could_be_all_ascii == null or !could_be_all_ascii.?) { // 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| { if (lifetime != .temporary) this.setIsASCIIFlag(false); if (lifetime == .transfer) { this.detach(); } if (lifetime == .temporary) { bun.default_allocator.free(bun.constStrToU8(buf)); } return ZigString.toExternalU16(external.ptr, external.len, global); } if (lifetime != .temporary) this.setIsASCIIFlag(true); } switch (comptime lifetime) { // strings are immutable // we don't need to clone .clone => { this.store.?.ref(); return ZigString.init(buf).external(global, this.store.?, Store.external); }, .transfer => { var store = this.store.?; this.transfer(); return ZigString.init(buf).external(global, store, Store.external); }, // strings are immutable // sharing isn't really a thing .share => { this.store.?.ref(); return ZigString.init(buf).external(global, this.store.?, Store.external); }, .temporary => { return ZigString.init(buf).toExternalValue(global); }, } } pub fn toString(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue { if (this.needsToReadFile()) { return this.doReadFile(toStringWithBytes, lifetime, global); } const view_: []u8 = bun.constStrToU8(this.sharedView()); if (view_.len == 0) return ZigString.Empty.toValue(global); return toStringWithBytes(this, global, view_, lifetime); } pub fn toJSON(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue { if (this.needsToReadFile()) { return this.doReadFile(toJSONWithBytes, lifetime, global); } var view_ = this.sharedView(); if (view_.len == 0) return ZigString.Empty.toValue(global); return toJSONWithBytes(this, global, view_, lifetime); } pub fn toJSONWithBytes(this: *Blob, global: *JSGlobalObject, buf: []const u8, comptime lifetime: Lifetime) JSValue { // null == unknown // false == can't be const could_be_all_ascii = this.is_all_ascii orelse this.store.?.is_all_ascii; if (could_be_all_ascii == null or !could_be_all_ascii.?) { // if toUTF16Alloc returns null, it means there are no non-ASCII characters if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch null) |external| { if (comptime lifetime != .temporary) this.setIsASCIIFlag(false); return ZigString.toExternalU16(external.ptr, external.len, global).parseJSON(global); } if (comptime lifetime != .temporary) this.setIsASCIIFlag(true); } if (comptime lifetime == .temporary) { return ZigString.init(buf).toExternalValue( global, ).parseJSON(global); } else { return ZigString.init(buf).toValue( global, ).parseJSON(global); } } pub fn toArrayBufferWithBytes(this: *Blob, global: *JSGlobalObject, buf: []u8, comptime lifetime: Lifetime) JSValue { switch (comptime lifetime) { .clone => { var clone = bun.default_allocator.alloc(u8, buf.len) catch unreachable; @memcpy(clone.ptr, buf.ptr, buf.len); return JSC.ArrayBuffer.fromBytes(clone, .ArrayBuffer).toJS(global.ref(), null); }, .share => { this.store.?.ref(); return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJSWithContext( global.ref(), this.store.?, JSC.BlobArrayBuffer_deallocator, null, ); }, .transfer => { var store = this.store.?; this.transfer(); return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJSWithContext( global.ref(), store, JSC.BlobArrayBuffer_deallocator, null, ); }, .temporary => { return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJS( global.ref(), null, ); }, } } pub fn toArrayBuffer(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue { if (this.needsToReadFile()) { return this.doReadFile(toArrayBufferWithBytes, lifetime, global); } var view_ = this.sharedView(); if (view_.len == 0) return JSC.ArrayBuffer.fromBytes(&[_]u8{}, .ArrayBuffer).toJS(global.ref(), null); return toArrayBufferWithBytes(this, global, bun.constStrToU8(view_), lifetime); } pub inline fn fromJS( global: *JSGlobalObject, arg: JSValue, comptime move: bool, comptime require_array: bool, ) anyerror!Blob { return fromJSMovable(global, arg, move, require_array); } pub inline fn fromJSMove(global: *JSGlobalObject, arg: JSValue) anyerror!Blob { return fromJSWithoutDeferGC(global, arg, true, false); } pub inline fn fromJSClone(global: *JSGlobalObject, arg: JSValue) anyerror!Blob { return fromJSWithoutDeferGC(global, arg, false, true); } pub inline fn fromJSCloneOptionalArray(global: *JSGlobalObject, arg: JSValue) anyerror!Blob { return fromJSWithoutDeferGC(global, arg, false, false); } fn fromJSMovable( global: *JSGlobalObject, arg: JSValue, comptime move: bool, comptime require_array: bool, ) anyerror!Blob { const FromJSFunction = if (comptime move and !require_array) fromJSMove else if (!require_array) fromJSCloneOptionalArray else fromJSClone; const DeferCtx = struct { args: std.meta.ArgsTuple(@TypeOf(FromJSFunction)), ret: anyerror!Blob = undefined, pub fn run(ctx: ?*anyopaque) callconv(.C) void { var that = bun.cast(*@This(), ctx.?); that.ret = @call(.{}, FromJSFunction, that.args); } }; var ctx = DeferCtx{ .args = .{ global, arg, }, .ret = undefined, }; global.vm().deferGC(&ctx, DeferCtx.run); return ctx.ret; } fn fromJSWithoutDeferGC( global: *JSGlobalObject, arg: JSValue, comptime move: bool, comptime require_array: bool, ) anyerror!Blob { var current = arg; if (current.isUndefinedOrNull()) { return Blob{ .globalThis = global }; } var top_value = current; var might_only_be_one_thing = false; switch (current.jsTypeLoose()) { .Array, .DerivedArray => { var top_iter = JSC.JSArrayIterator.init(current, global); might_only_be_one_thing = top_iter.len == 1; if (top_iter.len == 0) { return Blob{ .globalThis = global }; } if (might_only_be_one_thing) { top_value = top_iter.next().?; } }, else => { might_only_be_one_thing = true; if (require_array) { return error.InvalidArguments; } }, } if (might_only_be_one_thing or !move) { // Fast path: one item, we don't need to join switch (top_value.jsTypeLoose()) { .Cell, .NumberObject, JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject, => { var sliced = top_value.toSlice(global, bun.default_allocator); const is_all_ascii = !sliced.allocated; if (!sliced.allocated and sliced.len > 0) { sliced.ptr = @ptrCast([*]const u8, (try bun.default_allocator.dupe(u8, sliced.slice())).ptr); sliced.allocated = true; } return Blob.initWithAllASCII(bun.constStrToU8(sliced.slice()), bun.default_allocator, global, is_all_ascii); }, JSC.JSValue.JSType.ArrayBuffer, JSC.JSValue.JSType.Int8Array, JSC.JSValue.JSType.Uint8Array, JSC.JSValue.JSType.Uint8ClampedArray, JSC.JSValue.JSType.Int16Array, JSC.JSValue.JSType.Uint16Array, JSC.JSValue.JSType.Int32Array, JSC.JSValue.JSType.Uint32Array, JSC.JSValue.JSType.Float32Array, JSC.JSValue.JSType.Float64Array, JSC.JSValue.JSType.BigInt64Array, JSC.JSValue.JSType.BigUint64Array, JSC.JSValue.JSType.DataView, => { var buf = try bun.default_allocator.dupe(u8, top_value.asArrayBuffer(global).?.byteSlice()); return Blob.init(buf, bun.default_allocator, global); }, else => { if (JSC.C.JSObjectGetPrivate(top_value.asObjectRef())) |priv| { var data = JSC.JSPrivateDataPtr.from(priv); switch (data.tag()) { .Blob => { var blob: *Blob = data.as(Blob); if (comptime move) { var _blob = blob.*; _blob.allocator = null; blob.transfer(); return _blob; } else { return blob.dupe(); } }, else => return Blob.initEmpty(global), } } }, } } var stack_allocator = std.heap.stackFallback(1024, bun.default_allocator); var stack_mem_all = stack_allocator.get(); var stack: std.ArrayList(JSValue) = std.ArrayList(JSValue).init(stack_mem_all); var joiner = StringJoiner{ .use_pool = false, .node_allocator = stack_mem_all }; var could_have_non_ascii = false; defer if (stack_allocator.fixed_buffer_allocator.end_index >= 1024) stack.deinit(); while (true) { switch (current.jsTypeLoose()) { .NumberObject, JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject, => { var sliced = current.toSlice(global, bun.default_allocator); could_have_non_ascii = could_have_non_ascii or sliced.allocated; joiner.append( sliced.slice(), 0, if (sliced.allocated) sliced.allocator else null, ); }, .Array, .DerivedArray => { var iter = JSC.JSArrayIterator.init(current, global); try stack.ensureUnusedCapacity(iter.len); var any_arrays = false; while (iter.next()) |item| { if (item.isUndefinedOrNull()) continue; // When it's a string or ArrayBuffer inside an array, we can avoid the extra push/pop // we only really want this for nested arrays // However, we must preserve the order // That means if there are any arrays // we have to restart the loop if (!any_arrays) { switch (item.jsTypeLoose()) { .NumberObject, .Cell, JSC.JSValue.JSType.String, JSC.JSValue.JSType.StringObject, JSC.JSValue.JSType.DerivedStringObject, => { var sliced = item.toSlice(global, bun.default_allocator); could_have_non_ascii = could_have_non_ascii or sliced.allocated; joiner.append( sliced.slice(), 0, if (sliced.allocated) sliced.allocator else null, ); continue; }, JSC.JSValue.JSType.ArrayBuffer, JSC.JSValue.JSType.Int8Array, JSC.JSValue.JSType.Uint8Array, JSC.JSValue.JSType.Uint8ClampedArray, JSC.JSValue.JSType.Int16Array, JSC.JSValue.JSType.Uint16Array, JSC.JSValue.JSType.Int32Array, JSC.JSValue.JSType.Uint32Array, JSC.JSValue.JSType.Float32Array, JSC.JSValue.JSType.Float64Array, JSC.JSValue.JSType.BigInt64Array, JSC.JSValue.JSType.BigUint64Array, JSC.JSValue.JSType.DataView, => { could_have_non_ascii = true; var buf = item.asArrayBuffer(global).?; joiner.append(buf.byteSlice(), 0, null); continue; }, .Array, .DerivedArray => { any_arrays = true; could_have_non_ascii = true; break; }, else => { if (JSC.C.JSObjectGetPrivate(item.asObjectRef())) |priv| { var data = JSC.JSPrivateDataPtr.from(priv); switch (data.tag()) { .Blob => { var blob: *Blob = data.as(Blob); could_have_non_ascii = could_have_non_ascii or !(blob.is_all_ascii orelse false); joiner.append(blob.sharedView(), 0, null); continue; }, else => {}, } } }, } } stack.appendAssumeCapacity(item); } }, JSC.JSValue.JSType.ArrayBuffer, JSC.JSValue.JSType.Int8Array, JSC.JSValue.JSType.Uint8Array, JSC.JSValue.JSType.Uint8ClampedArray, JSC.JSValue.JSType.Int16Array, JSC.JSValue.JSType.Uint16Array, JSC.JSValue.JSType.Int32Array, JSC.JSValue.JSType.Uint32Array, JSC.JSValue.JSType.Float32Array, JSC.JSValue.JSType.Float64Array, JSC.JSValue.JSType.BigInt64Array, JSC.JSValue.JSType.BigUint64Array, JSC.JSValue.JSType.DataView, => { var buf = current.asArrayBuffer(global).?; joiner.append(buf.slice(), 0, null); could_have_non_ascii = true; }, else => { outer: { if (JSC.C.JSObjectGetPrivate(current.asObjectRef())) |priv| { var data = JSC.JSPrivateDataPtr.from(priv); switch (data.tag()) { .Blob => { var blob: *Blob = data.as(Blob); could_have_non_ascii = could_have_non_ascii or !(blob.is_all_ascii orelse false); joiner.append(blob.sharedView(), 0, null); break :outer; }, else => {}, } } var sliced = current.toSlice(global, bun.default_allocator); could_have_non_ascii = could_have_non_ascii or sliced.allocated; joiner.append( sliced.slice(), 0, if (sliced.allocated) sliced.allocator else null, ); } }, } current = stack.popOrNull() orelse break; } var joined = try joiner.done(bun.default_allocator); if (!could_have_non_ascii) { return Blob.initWithAllASCII(joined, bun.default_allocator, global, true); } return Blob.init(joined, bun.default_allocator, global); } }; // https://developer.mozilla.org/en-US/docs/Web/API/Body pub const Body = struct { init: Init = Init{ .headers = null, .status_code = 200 }, value: Value = Value.empty, pub inline fn len(this: *const Body) usize { return this.slice().len; } pub fn slice(this: *const Body) []const u8 { return this.value.slice(); } pub fn use(this: *Body) Blob { return this.value.use(); } pub fn clone(this: Body, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) Body { return Body{ .init = this.init.clone(globalThis), .value = this.value.clone(allocator), }; } pub fn writeFormat(this: *const Body, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { const Writer = @TypeOf(writer); try formatter.writeIndent(Writer, writer); try writer.writeAll("bodyUsed: "); formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.value == .Used), .BooleanObject, enable_ansi_colors); try formatter.printComma(Writer, writer, enable_ansi_colors); try writer.writeAll("\n"); // if (this.init.headers) |headers| { // try formatter.writeIndent(Writer, writer); // try writer.writeAll("headers: "); // try headers.leak().writeFormat(formatter, writer, comptime enable_ansi_colors); // try writer.writeAll("\n"); // } try formatter.writeIndent(Writer, writer); try writer.writeAll("status: "); formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(this.init.status_code), .NumberObject, enable_ansi_colors); } pub fn deinit(this: *Body, _: std.mem.Allocator) void { if (this.init.headers) |headers| { headers.deref(); this.init.headers = null; } this.value.deinit(); } pub const Init = struct { headers: ?*FetchHeaders = null, status_code: u16, method: Method = Method.GET, pub fn clone(this: Init, _: *JSGlobalObject) Init { var that = this; var headers = this.headers; if (headers) |head| { that.headers = head.cloneThis(); } return that; } pub fn init(_: std.mem.Allocator, ctx: js.JSContextRef, init_ref: js.JSValueRef) !?Init { var result = Init{ .status_code = 200 }; var array = js.JSObjectCopyPropertyNames(ctx, init_ref); defer js.JSPropertyNameArrayRelease(array); const count = js.JSPropertyNameArrayGetCount(array); var i: usize = 0; while (i < count) : (i += 1) { var property_name_ref = js.JSPropertyNameArrayGetNameAtIndex(array, i); switch (js.JSStringGetLength(property_name_ref)) { "headers".len => { if (js.JSStringIsEqualToUTF8CString(property_name_ref, "headers")) { // only support headers as an object for now. if (js.JSObjectGetProperty(ctx, init_ref, property_name_ref, null)) |header_prop| { const header_val = JSValue.fromRef(header_prop); if (header_val.as(FetchHeaders)) |orig| { result.headers = orig.cloneThis(); } else { result.headers = FetchHeaders.createFromJS(ctx.ptr(), header_val); } } } }, "method".len => { if (js.JSStringIsEqualToUTF8CString(property_name_ref, "status")) { var value_ref = js.JSObjectGetProperty(ctx, init_ref, property_name_ref, null); var exception: js.JSValueRef = null; const number = js.JSValueToNumber(ctx, value_ref, &exception); if (exception != null or !std.math.isFinite(number)) continue; result.status_code = @truncate(u16, @floatToInt(u64, number)); } else if (js.JSStringIsEqualToUTF8CString(property_name_ref, "method")) { result.method = Method.which( JSC.JSValue.fromRef(init_ref).get(ctx.ptr(), "method").?.getZigString(ctx.ptr()).slice(), ) orelse Method.GET; } }, else => {}, } } if (result.headers == null and result.status_code < 200) return null; return result; } }; pub const PendingValue = struct { promise: ?JSValue = null, readable: ?JSC.WebCore.ReadableStream = null, // writable: JSC.WebCore.Sink global: *JSGlobalObject, task: ?*anyopaque = null, /// runs after the data is available. callback: ?fn (ctx: *anyopaque, value: *Value) void = null, /// conditionally runs when requesting data /// used in HTTP server to ignore request bodies unless asked for it onPull: ?fn (ctx: *anyopaque) void = null, deinit: bool = false, action: Action = Action.none, pub fn setPromise(value: *PendingValue, globalThis: *JSC.JSGlobalObject, action: Action) JSValue { value.action = action; if (value.readable) |*readable| { // switch (readable.ptr) { // .JavaScript // } switch (action) { .getText, .getJSON, .getBlob, .getArrayBuffer => { switch (readable.ptr) { .Blob => unreachable, else => {}, } value.promise = switch (action) { .getJSON => globalThis.readableStreamToJSON(readable.value), .getArrayBuffer => globalThis.readableStreamToArrayBuffer(readable.value), .getText => globalThis.readableStreamToText(readable.value), .getBlob => globalThis.readableStreamToBlob(readable.value), else => unreachable, }; value.promise.?.ensureStillAlive(); readable.value.unprotect(); // js now owns the memory value.readable = null; return value.promise.?; }, .none => {}, } } { var promise = JSC.JSPromise.create(globalThis); const promise_value = promise.asValue(globalThis); value.promise = promise_value; if (value.onPull) |onPull| { value.onPull = null; onPull(value.task.?); } return promise_value; } } pub const Action = enum { none, getText, getJSON, getArrayBuffer, getBlob, }; }; pub const Value = union(Tag) { Blob: Blob, Locked: PendingValue, Used: void, Empty: void, Error: JSValue, pub const Tag = enum { Blob, Locked, Used, Empty, Error, }; pub const empty = Value{ .Empty = .{} }; pub fn fromReadableStream(readable: JSC.WebCore.ReadableStream, globalThis: *JSGlobalObject) Value { if (readable.isLocked(globalThis)) { return .{ .Error = ZigString.init("Cannot use a locked ReadableStream").toErrorInstance(globalThis) }; } readable.value.protect(); return .{ .Locked = .{ .readable = readable, .global = globalThis, }, }; } pub fn resolve(this: *Value, new: *Value, global: *JSGlobalObject) void { if (this.* == .Locked) { var locked = &this.Locked; if (locked.readable) |readable| { readable.done(); locked.readable = null; } if (locked.callback) |callback| { locked.callback = null; callback(locked.task.?, new); } if (locked.promise) |promise| { locked.promise = null; var blob = new.use(); switch (locked.action) { .getText => { promise.asPromise().?.resolve(global, JSValue.fromRef(blob.getTextTransfer(global.ref()))); }, .getJSON => { promise.asPromise().?.resolve(global, blob.toJSON(global, .share)); blob.detach(); }, .getArrayBuffer => { promise.asPromise().?.resolve(global, JSValue.fromRef(blob.getArrayBufferTransfer(global.ref()))); }, .getBlob => { var ptr = bun.default_allocator.create(Blob) catch unreachable; ptr.* = blob; ptr.allocator = bun.default_allocator; promise.asPromise().?.resolve(global, JSC.JSValue.fromRef(Blob.Class.make(global.ref(), ptr))); }, else => { var ptr = bun.default_allocator.create(Blob) catch unreachable; ptr.* = blob; ptr.allocator = bun.default_allocator; promise.asInternalPromise().?.resolve(global, JSC.JSValue.fromRef(Blob.Class.make(global.ref(), ptr))); }, } JSC.C.JSValueUnprotect(global.ref(), promise.asObjectRef()); } } } pub fn slice(this: Value) []const u8 { return switch (this) { .Blob => this.Blob.sharedView(), else => "", }; } pub fn use(this: *Value) Blob { switch (this.*) { .Blob => { var new_blob = this.Blob; std.debug.assert(new_blob.allocator == null); // owned by Body this.* = .{ .Used = .{} }; return new_blob; }, else => { return Blob.initEmpty(undefined); }, } } pub fn toErrorInstance(this: *Value, error_instance: JSC.JSValue, global: *JSGlobalObject) void { if (this.* == .Locked) { var locked = this.Locked; locked.deinit = true; if (locked.promise) |promise| { if (promise.asInternalPromise()) |internal| { internal.reject(global, error_instance); } else if (promise.asPromise()) |internal| { internal.reject(global, error_instance); } JSC.C.JSValueUnprotect(global.ref(), promise.asObjectRef()); locked.promise = null; } if (locked.readable) |readable| { readable.done(); locked.readable = null; } this.* = .{ .Error = error_instance }; if (locked.callback) |callback| { locked.callback = null; callback(locked.task.?, this); } return; } this.* = .{ .Error = error_instance }; } pub fn toErrorString(this: *Value, comptime err: string, global: *JSGlobalObject) void { var error_str = ZigString.init(err); var error_instance = error_str.toErrorInstance(global); return this.toErrorInstance(error_instance, global); } 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)}, ) catch unreachable); 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) { if (this.Locked.readable) |*readable| { readable.done(); } this.Locked.deinit = true; return; } if (tag == .Blob) { this.Blob.deinit(); this.* = Value.empty; } if (tag == .Error) { JSC.C.JSValueUnprotect(VirtualMachine.vm.global.ref(), this.Error.asObjectRef()); } } pub fn clone(this: Value, _: std.mem.Allocator) Value { if (this == .Blob) { return Value{ .Blob = this.Blob.dupe() }; } return Value{ .Empty = .{} }; } }; pub fn @"404"(_: js.JSContextRef) Body { return Body{ .init = Init{ .headers = null, .status_code = 404, }, .value = Value.empty, }; } pub fn @"200"(_: js.JSContextRef) Body { return Body{ .init = Init{ .status_code = 200, }, .value = Value.empty, }; } pub fn extract(ctx: js.JSContextRef, body_ref: js.JSObjectRef, exception: js.ExceptionRef) Body { return extractBody( ctx, body_ref, false, null, exception, ); } pub fn extractWithInit(ctx: js.JSContextRef, body_ref: js.JSObjectRef, init_ref: js.JSValueRef, exception: js.ExceptionRef) Body { return extractBody( ctx, body_ref, true, init_ref, exception, ); } // https://github.com/WebKit/webkit/blob/main/Source/WebCore/Modules/fetch/FetchBody.cpp#L45 inline fn extractBody( ctx: js.JSContextRef, body_ref: js.JSObjectRef, comptime has_init: bool, init_ref: js.JSValueRef, exception: js.ExceptionRef, ) Body { var body = Body{ .init = Init{ .headers = null, .status_code = 200 }, }; const value = JSC.JSValue.fromRef(body_ref); var allocator = getAllocator(ctx); if (comptime has_init) { if (Init.init(allocator, ctx, init_ref.?)) |maybeInit| { if (maybeInit) |init_| { body.init = init_; } } else |_| {} } if (JSC.WebCore.ReadableStream.fromJS(value, ctx)) |readable| { switch (readable.ptr) { .Blob => |blob| { body.value = .{ .Blob = Blob.initWithStore(blob.store, ctx), }; blob.store.ref(); readable.done(); if (!blob.done) { blob.done = true; blob.deinit(); } return body; }, else => {}, } body.value = Body.Value.fromReadableStream(readable, ctx); return body; } body.value = .{ .Blob = Blob.fromJS(ctx.ptr(), value, true, false) catch |err| { if (err == error.InvalidArguments) { JSC.JSError(allocator, "Expected an Array", .{}, ctx, exception); return body; } JSC.JSError(allocator, "Out of memory", .{}, ctx, exception); return body; }, }; std.debug.assert(body.value.Blob.allocator == null); // owned by Body return body; } }; // https://developer.mozilla.org/en-US/docs/Web/API/Request pub const Request = struct { url: ZigString = ZigString.Empty, headers: ?*FetchHeaders = null, body: Body.Value = Body.Value{ .Empty = .{} }, method: Method = Method.GET, uws_request: ?*uws.Request = null, pub fn fromRequestContext(ctx: *RequestContext, global: *JSGlobalObject) !Request { var req = Request{ .url = ZigString.init(std.mem.span(ctx.getFullURL())), .body = Body.Value.empty, .method = ctx.method, .headers = FetchHeaders.createFromPicoHeaders(global, ctx.request.headers), }; req.url.mark(); return req; } pub fn mimeType(this: *const Request) string { if (this.headers) |headers| { // Remember, we always lowercase it // hopefully doesn't matter here tho if (headers.get("content-type")) |content_type| { return content_type; } } switch (this.body) { .Blob => |blob| { if (blob.content_type.len > 0) { return blob.content_type; } return MimeType.other.value; }, .Error, .Used, .Locked, .Empty => return MimeType.other.value, } } pub const Constructor = JSC.NewConstructor( Request, .{ .constructor = .{ .rfn = constructor }, }, .{}, ); pub const Class = NewClass( Request, .{ .name = "Request", .read_only = true, }, .{ .finalize = finalize, .text = .{ .rfn = Request.getText, }, .json = .{ .rfn = Request.getJSON, }, .arrayBuffer = .{ .rfn = Request.getArrayBuffer, }, .blob = .{ .rfn = Request.getBlob, }, .clone = .{ .rfn = Request.doClone, }, }, .{ .@"cache" = .{ .@"get" = getCache, .@"ro" = true, }, .@"credentials" = .{ .@"get" = getCredentials, .@"ro" = true, }, .@"destination" = .{ .@"get" = getDestination, .@"ro" = true, }, .@"headers" = .{ .@"get" = getHeaders, .@"ro" = true, }, .@"integrity" = .{ .@"get" = getIntegrity, .@"ro" = true, }, .@"method" = .{ .@"get" = getMethod, .@"ro" = true, }, .@"mode" = .{ .@"get" = getMode, .@"ro" = true, }, .@"redirect" = .{ .@"get" = getRedirect, .@"ro" = true, }, .@"referrer" = .{ .@"get" = getReferrer, .@"ro" = true, }, .@"referrerPolicy" = .{ .@"get" = getReferrerPolicy, .@"ro" = true, }, .@"url" = .{ .@"get" = getUrl, .@"ro" = true, }, .@"bodyUsed" = .{ .@"get" = getBodyUsed, .@"ro" = true, }, }, ); pub fn getCache( _: *Request, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return js.JSValueMakeString(ctx, ZigString.init(Properties.UTF8.default).toValueGC(ctx.ptr()).asRef()); } pub fn getCredentials( _: *Request, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return js.JSValueMakeString(ctx, ZigString.init(Properties.UTF8.include).toValueGC(ctx.ptr()).asRef()); } pub fn getDestination( _: *Request, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return js.JSValueMakeString(ctx, ZigString.init("").toValueGC(ctx.ptr()).asRef()); } pub fn getIntegrity( _: *Request, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return ZigString.Empty.toValueGC(ctx.ptr()).asRef(); } pub fn getMethod( this: *Request, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { const string_contents: string = switch (this.method) { .GET => Properties.UTF8.GET, .HEAD => Properties.UTF8.HEAD, .PATCH => Properties.UTF8.PATCH, .PUT => Properties.UTF8.PUT, .POST => Properties.UTF8.POST, .OPTIONS => Properties.UTF8.OPTIONS, else => "", }; return ZigString.init(string_contents).toValue(ctx.ptr()).asRef(); } pub fn getMode( _: *Request, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return ZigString.init(Properties.UTF8.navigate).toValue(ctx.ptr()).asRef(); } pub fn finalize(this: *Request) void { if (this.headers) |headers| { headers.deref(); this.headers = null; } if (this.url.isGloballyAllocated()) { bun.default_allocator.free(bun.constStrToU8(this.url.slice())); } bun.default_allocator.destroy(this); } pub fn getRedirect( _: *Request, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return ZigString.init(Properties.UTF8.follow).toValueGC(ctx.ptr()).asRef(); } pub fn getReferrer( this: *Request, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { if (this.headers) |headers_ref| { if (headers_ref.get("referrer")) |referrer| { return ZigString.init(referrer).toValueGC(ctx.ptr()).asRef(); } } return ZigString.init("").toValueGC(ctx.ptr()).asRef(); } pub fn getReferrerPolicy( _: *Request, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return ZigString.init("").toValueGC(ctx.ptr()).asRef(); } pub fn getUrl( this: *Request, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return this.url.toValueGC(ctx.ptr()).asObjectRef(); } pub fn constructor( ctx: js.JSContextRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSObjectRef { var request = Request{}; switch (arguments.len) { 0 => {}, 1 => { request.url = JSC.JSValue.fromRef(arguments[0]).getZigString(ctx.ptr()); }, else => { request.url = JSC.JSValue.fromRef(arguments[0]).getZigString(ctx.ptr()); if (Body.Init.init(getAllocator(ctx), ctx, arguments[1]) catch null) |req_init| { request.headers = req_init.headers; request.method = req_init.method; } if (JSC.JSValue.fromRef(arguments[1]).get(ctx.ptr(), "body")) |body_| { if (Blob.fromJS(ctx.ptr(), body_, true, false)) |blob| { if (blob.size > 0) { request.body = Body.Value{ .Blob = blob }; } } else |err| { if (err == error.InvalidArguments) { JSC.JSError(getAllocator(ctx), "Expected an Array", .{}, ctx, exception); return null; } JSC.JSError(getAllocator(ctx), "Invalid Body", .{}, ctx, exception); return null; } } }, } var request_ = getAllocator(ctx).create(Request) catch unreachable; request_.* = request; return Request.Class.make( ctx, request_, ); } pub fn getBodyValue( this: *Request, ) *Body.Value { return &this.body; } pub fn getBodyUsed( this: *Request, _: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return JSC.JSValue.jsBoolean(this.body == .Used).asRef(); } pub usingnamespace BlobInterface(@This()); pub fn doClone( this: *Request, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, _: js.ExceptionRef, ) js.JSValueRef { var cloned = this.clone(getAllocator(ctx), ctx.ptr()); return Request.Class.make(ctx, cloned); } pub fn getHeaders( this: *Request, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { if (this.headers == null) { if (this.uws_request) |req| { this.headers = FetchHeaders.createFromUWS(ctx.ptr(), req); } else { this.headers = FetchHeaders.createEmpty(); } } return this.headers.?.toJS(ctx.ptr()).asObjectRef(); } pub fn cloneInto( this: *const Request, req: *Request, allocator: std.mem.Allocator, globalThis: *JSGlobalObject, ) void { req.* = Request{ .body = this.body.clone(allocator), .url = ZigString.init(allocator.dupe(u8, this.url.slice()) catch unreachable), .method = this.method, }; if (this.headers) |head| { req.headers = head.cloneThis(); } else if (this.uws_request) |uws_req| { req.headers = FetchHeaders.createFromUWS(globalThis, uws_req); } } pub fn clone(this: *const Request, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) *Request { var req = allocator.create(Request) catch unreachable; this.cloneInto(req, allocator, globalThis); return req; } }; fn BlobInterface(comptime Type: type) type { return struct { pub fn getText( this: *Type, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, _: js.ExceptionRef, ) js.JSValueRef { var value: *Body.Value = this.getBodyValue(); if (value.* == .Locked) { return value.Locked.setPromise(ctx.ptr(), .getText).asObjectRef(); } var blob = this.body.use(); return blob.getTextTransfer(ctx); } pub fn getJSON( this: *Type, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSValueRef { var value: *Body.Value = this.getBodyValue(); if (value.* == .Locked) { return value.Locked.setPromise(ctx.ptr(), .getJSON).asObjectRef(); } var blob = this.body.use(); return blob.getJSON(ctx, null, null, &.{}, exception); } pub fn getArrayBuffer( this: *Type, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, _: js.ExceptionRef, ) js.JSValueRef { var value: *Body.Value = this.getBodyValue(); if (value.* == .Locked) { return value.Locked.setPromise(ctx.ptr(), .getArrayBuffer).asObjectRef(); } var blob = this.body.use(); return blob.getArrayBufferTransfer(ctx); } pub fn getBlob( this: *Type, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, _: js.ExceptionRef, ) js.JSValueRef { var value: *Body.Value = this.getBodyValue(); if (value.* == .Locked) { return value.Locked.setPromise(ctx.ptr(), .getBlob).asObjectRef(); } var blob = this.body.use(); var ptr = getAllocator(ctx).create(Blob) catch unreachable; ptr.* = blob; blob.allocator = getAllocator(ctx); return JSC.JSPromise.resolvedPromiseValue(ctx.ptr(), JSValue.fromRef(Blob.Class.make(ctx, ptr))).asObjectRef(); } // pub fn getBody( // this: *Type, // ctx: js.JSContextRef, // _: js.JSObjectRef, // _: js.JSObjectRef, // _: []const js.JSValueRef, // _: js.ExceptionRef, // ) js.JSValueRef { // var value: *Body.Value = this.getBodyValue(); // switch (value.*) { // .Empty => {}, // } // } }; } // https://github.com/WebKit/WebKit/blob/main/Source/WebCore/workers/service/FetchEvent.h pub const FetchEvent = struct { started_waiting_at: u64 = 0, response: ?*Response = null, request_context: ?*RequestContext = null, request: Request, pending_promise: ?*JSInternalPromise = null, onPromiseRejectionCtx: *anyopaque = undefined, onPromiseRejectionHandler: ?fn (ctx: *anyopaque, err: anyerror, fetch_event: *FetchEvent, value: JSValue) void = null, rejected: bool = false, pub const Class = NewClass( FetchEvent, .{ .name = "FetchEvent", .read_only = true, .ts = .{ .class = d.ts.class{ .interface = true } }, }, .{ .@"respondWith" = .{ .rfn = respondWith, .ts = d.ts{ .tsdoc = "Render the response in the active HTTP request", .@"return" = "void", .args = &[_]d.ts.arg{ .{ .name = "response", .@"return" = "Response" }, }, }, }, .@"waitUntil" = waitUntil, .finalize = finalize, }, .{ .@"client" = .{ .@"get" = getClient, .ro = true, .ts = d.ts{ .tsdoc = "HTTP client metadata. This is not implemented yet, do not use.", .@"return" = "undefined", }, }, .@"request" = .{ .@"get" = getRequest, .ro = true, .ts = d.ts{ .tsdoc = "HTTP request", .@"return" = "InstanceType", }, }, }, ); pub fn finalize( this: *FetchEvent, ) void { VirtualMachine.vm.allocator.destroy(this); } pub fn getClient( _: *FetchEvent, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { Output.prettyErrorln("FetchEvent.client is not implemented yet - sorry!!", .{}); Output.flush(); return js.JSValueMakeUndefined(ctx); } pub fn getRequest( this: *FetchEvent, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { var req = bun.default_allocator.create(Request) catch unreachable; req.* = this.request; return Request.Class.make( ctx, req, ); } // https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith pub fn respondWith( this: *FetchEvent, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSValueRef { var request_context = this.request_context orelse return js.JSValueMakeUndefined(ctx); if (request_context.has_called_done) return js.JSValueMakeUndefined(ctx); var globalThis = ctx.ptr(); // A Response or a Promise that resolves to a Response. Otherwise, a network error is returned to Fetch. if (arguments.len == 0 or !Response.Class.isLoaded() or !js.JSValueIsObject(ctx, arguments[0])) { JSError(getAllocator(ctx), "event.respondWith() must be a Response or a Promise.", .{}, ctx, exception); request_context.sendInternalError(error.respondWithWasEmpty) catch {}; return js.JSValueMakeUndefined(ctx); } var arg = arguments[0]; if (JSValue.fromRef(arg).as(Response) == null) { this.pending_promise = this.pending_promise orelse JSInternalPromise.resolvedPromise(globalThis, JSValue.fromRef(arguments[0])); } if (this.pending_promise) |promise| { VirtualMachine.vm.event_loop.waitForPromise(promise); switch (promise.status(ctx.ptr().vm())) { .Fulfilled => {}, else => { this.rejected = true; this.pending_promise = null; this.onPromiseRejectionHandler.?( this.onPromiseRejectionCtx, error.PromiseRejection, this, promise.result(globalThis.vm()), ); return js.JSValueMakeUndefined(ctx); }, } arg = promise.result(ctx.ptr().vm()).asRef(); } var response: *Response = GetJSPrivateData(Response, arg) orelse { this.rejected = true; this.pending_promise = null; JSError(getAllocator(ctx), "event.respondWith() expects Response or Promise", .{}, ctx, exception); this.onPromiseRejectionHandler.?(this.onPromiseRejectionCtx, error.RespondWithInvalidTypeInternal, this, JSValue.fromRef(exception.*)); return js.JSValueMakeUndefined(ctx); }; defer { if (!VirtualMachine.vm.had_errors) { Output.printElapsed(@intToFloat(f64, (request_context.timer.lap())) / std.time.ns_per_ms); Output.prettyError( " {s} - {d} transpiled, {d} imports\n", .{ request_context.matched_route.?.name, VirtualMachine.vm.transpiled_count, VirtualMachine.vm.resolved_count, }, ); } } defer this.pending_promise = null; var needs_mime_type = true; var content_length: ?usize = null; if (response.body.init.headers) |headers_ref| { var headers = Headers.from(headers_ref, request_context.allocator) catch unreachable; var i: usize = 0; while (i < headers.entries.len) : (i += 1) { var header = headers.entries.get(i); const name = headers.asStr(header.name); if (strings.eqlComptime(name, "content-type") and headers.asStr(header.value).len > 0) { needs_mime_type = false; } if (strings.eqlComptime(name, "content-length")) { content_length = std.fmt.parseInt(usize, headers.asStr(header.value), 10) catch null; continue; } // Some headers need to be managed by bun if (strings.eqlComptime(name, "transfer-encoding") or strings.eqlComptime(name, "content-encoding") or strings.eqlComptime(name, "strict-transport-security") or strings.eqlComptime(name, "content-security-policy")) { continue; } request_context.appendHeaderSlow( name, headers.asStr(header.value), ) catch unreachable; } } if (needs_mime_type) { request_context.appendHeader("Content-Type", response.mimeTypeWithDefault(MimeType.html, request_context)); } var blob = response.body.value.use(); defer blob.deinit(); const content_length_ = content_length orelse blob.size; if (content_length_ == 0) { request_context.sendNoContent() catch return js.JSValueMakeUndefined(ctx); return js.JSValueMakeUndefined(ctx); } if (FeatureFlags.strong_etags_for_built_files) { const did_send = request_context.writeETag(blob.sharedView()) catch false; if (did_send) { // defer getAllocator(ctx).destroy(str.ptr); return js.JSValueMakeUndefined(ctx); } } defer request_context.done(); request_context.writeStatusSlow(response.body.init.status_code) catch return js.JSValueMakeUndefined(ctx); request_context.prepareToSendBody(content_length_, false) catch return js.JSValueMakeUndefined(ctx); request_context.writeBodyBuf(blob.sharedView()) catch return js.JSValueMakeUndefined(ctx); return js.JSValueMakeUndefined(ctx); } // our implementation of the event listener already does this // so this is a no-op for us pub fn waitUntil( _: *FetchEvent, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, _: js.ExceptionRef, ) js.JSValueRef { return js.JSValueMakeUndefined(ctx); } };