diff options
author | 2022-03-21 02:21:51 -0700 | |
---|---|---|
committer | 2022-03-21 02:21:51 -0700 | |
commit | fa343fa8adb25a7e307e91a3cd3c2c3f24e0152b (patch) | |
tree | 63a7ed4a7d787dd2b61330f5c1e69b2af0737e39 | |
parent | 1f93de264f55e7a392bd34dbb9bda0b2365d7c88 (diff) | |
download | bun-fa343fa8adb25a7e307e91a3cd3c2c3f24e0152b.tar.gz bun-fa343fa8adb25a7e307e91a3cd3c2c3f24e0152b.tar.zst bun-fa343fa8adb25a7e307e91a3cd3c2c3f24e0152b.zip |
[bun.js] 1/? Implement `Response.file`
-rw-r--r-- | integration/bunjs-only-snippets/response.file.test.js | 49 | ||||
-rw-r--r-- | src/bundler/entry_points.zig | 3 | ||||
-rw-r--r-- | src/io/io_darwin.zig | 127 | ||||
-rw-r--r-- | src/io/io_linux.zig | 134 | ||||
-rw-r--r-- | src/javascript/jsc/api/html_rewriter.zig | 107 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/bindings.cpp | 17 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/bindings.zig | 10 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/headers-cpp.h | 2 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/headers.h | 6 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/headers.zig | 2 | ||||
-rw-r--r-- | src/javascript/jsc/javascript.zig | 24 | ||||
-rw-r--r-- | src/javascript/jsc/node/types.zig | 7 | ||||
-rw-r--r-- | src/javascript/jsc/webcore/response.zig | 718 |
13 files changed, 1103 insertions, 103 deletions
diff --git a/integration/bunjs-only-snippets/response.file.test.js b/integration/bunjs-only-snippets/response.file.test.js new file mode 100644 index 000000000..442a527f2 --- /dev/null +++ b/integration/bunjs-only-snippets/response.file.test.js @@ -0,0 +1,49 @@ +import fs from "fs"; +import { it, expect } from "bun:test"; +import path from "path"; +it("Response.file", async () => { + const file = path.join(import.meta.dir, "fetch.js.txt"); + expect(await Response.file(file).text()).toBe(fs.readFileSync(file, "utf8")); +}); + +it("Response.file as a blob", async () => { + const file = path.join(import.meta.url, "../fetch.js.txt"); + var response = Response.file(file); + var blob = await response.blob(); + expect(blob.size).toBe(0); + expect(await blob.text()).toBe(fs.readFileSync(file, "utf8")); + expect(blob.size).toBe(1256); + expect(await blob.text()).toBe(fs.readFileSync(file, "utf8")); + + const array = new Uint8Array(await blob.arrayBuffer()); + const text = fs.readFileSync(file, "utf8"); + for (let i = 0; i < text.length; i++) { + expect(array[i]).toBe(text.charCodeAt(i)); + } + expect(blob.size).toBe(1256); + blob = null; + response = null; + Bun.gc(true); + await new Promise((resolve) => setTimeout(resolve, 1)); +}); + +it("Response.file as a blob", async () => { + const file = path.join(import.meta.url, "../fetch.js.txt"); + var response = Response.file(file); + var blob = await response.blob(); + + expect(blob.size).toBe(0); + expect(await blob.text()).toBe(fs.readFileSync(file, "utf8")); + expect(blob.size).toBe(1256); + expect(await blob.text()).toBe(fs.readFileSync(file, "utf8")); + const array = new Uint8Array(await blob.arrayBuffer()); + const text = fs.readFileSync(file, "utf8"); + for (let i = 0; i < text.length; i++) { + expect(array[i]).toBe(text.charCodeAt(i)); + } + expect(blob.size).toBe(1256); + blob = null; + response = null; + Bun.gc(true); + await new Promise((resolve) => setTimeout(resolve, 1)); +}); diff --git a/src/bundler/entry_points.zig b/src/bundler/entry_points.zig index 376043122..252cfd9b1 100644 --- a/src/bundler/entry_points.zig +++ b/src/bundler/entry_points.zig @@ -189,7 +189,8 @@ pub const ServerEntryPoint = struct { \\if ('default' in start && "__internalIsCommonJSNamespace" in globalThis && __internalIsCommonJSNamespace(start)) {{ \\ entryNamespace = start.default(); \\}} - \\if(entryNamespace && 'serverless' in entryNamespace) Bun.startServer(entryNamespace.serverless); + \\if(typeof entryNamespace?.default?.fetch === 'function') Bun.startServer(entryNamespace.default.fetch); + \\if(typeof entryNamespace?.default?.cli === 'function') Bun.startServer(entryNamespace.default.cli); , .{ dir_to_use, diff --git a/src/io/io_darwin.zig b/src/io/io_darwin.zig index 086a8a116..c3175a0ed 100644 --- a/src/io/io_darwin.zig +++ b/src/io/io_darwin.zig @@ -127,7 +127,7 @@ pub const Errno = error{ Unexpected, }; -const errno_map: [108]Errno = brk: { +pub const errno_map: [108]Errno = brk: { var errors: [108]Errno = undefined; errors[1] = error.EPERM; errors[2] = error.ENOENT; @@ -244,7 +244,12 @@ const socklen_t = darwin.socklen_t; const system = darwin; pub fn asError(err: anytype) Errno { - return switch (@enumToInt(err)) { + const int = if (@typeInfo(@TypeOf(err)) == .Enum) + @enumToInt(err) + else + err; + + return switch (int) { 1...errno_map.len => |val| errno_map[@intCast(u8, val)], else => error.Unexpected, }; @@ -257,22 +262,72 @@ const c = std.c; const darwin = struct { pub usingnamespace os.darwin; pub extern "c" fn @"recvfrom$NOCANCEL"(sockfd: c.fd_t, noalias buf: *anyopaque, len: usize, flags: u32, noalias src_addr: ?*c.sockaddr, noalias addrlen: ?*c.socklen_t) isize; - pub extern "c" fn @"sendto$NOCANCEL"( - sockfd: c.fd_t, - buf: *const anyopaque, - len: usize, - flags: u32, - dest_addr: ?*const c.sockaddr, - addrlen: c.socklen_t, - ) isize; + pub extern "c" fn @"sendto$NOCANCEL"(sockfd: c.fd_t, buf: *const anyopaque, len: usize, flags: u32, dest_addr: ?*const c.sockaddr, addrlen: c.socklen_t) isize; pub extern "c" fn @"fcntl$NOCANCEL"(fd: c.fd_t, cmd: c_int, ...) c_int; pub extern "c" fn @"sendmsg$NOCANCEL"(sockfd: c.fd_t, msg: *const std.x.os.Socket.Message, flags: c_int) isize; pub extern "c" fn @"recvmsg$NOCANCEL"(sockfd: c.fd_t, msg: *std.x.os.Socket.Message, flags: c_int) isize; pub extern "c" fn @"connect$NOCANCEL"(sockfd: c.fd_t, sock_addr: *const c.sockaddr, addrlen: c.socklen_t) c_int; pub extern "c" fn @"accept$NOCANCEL"(sockfd: c.fd_t, noalias addr: ?*c.sockaddr, noalias addrlen: ?*c.socklen_t) c_int; pub extern "c" fn @"accept4$NOCANCEL"(sockfd: c.fd_t, noalias addr: ?*c.sockaddr, noalias addrlen: ?*c.socklen_t, flags: c_uint) c_int; + pub extern "c" fn @"open$NOCANCEL"(path: [*:0]const u8, oflag: c_uint, ...) c_int; }; +pub const OpenError = error{ + /// In WASI, this error may occur when the file descriptor does + /// not hold the required rights to open a new resource relative to it. + AccessDenied, + SymLinkLoop, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + NoDevice, + FileNotFound, + + /// The path exceeded `MAX_PATH_BYTES` bytes. + NameTooLong, + + /// Insufficient kernel memory was available, or + /// the named file is a FIFO and per-user hard limit on + /// memory allocation for pipes has been reached. + SystemResources, + + /// The file is too large to be opened. This error is unreachable + /// for 64-bit targets, as well as when opening directories. + FileTooBig, + + /// The path refers to directory but the `O.DIRECTORY` flag was not provided. + IsDir, + + /// A new path cannot be created because the device has no room for the new file. + /// This error is only reachable when the `O.CREAT` flag is provided. + NoSpaceLeft, + + /// A component used as a directory in the path was not, in fact, a directory, or + /// `O.DIRECTORY` was specified and the path was not a directory. + NotDir, + + /// The path already exists and the `O.CREAT` and `O.EXCL` flags were provided. + PathAlreadyExists, + DeviceBusy, + + /// The underlying filesystem does not support file locks + FileLocksNotSupported, + + BadPathName, + InvalidUtf8, + + /// One of these three things: + /// * pathname refers to an executable image which is currently being + /// executed and write access was requested. + /// * pathname refers to a file that is currently in use as a swap + /// file, and the O_TRUNC flag was specified. + /// * pathname refers to a file that is currently being read by the + /// kernel (e.g., for module/firmware loading), and write access was + /// requested. + FileBusy, + + WouldBlock, +} || Errno; + pub const Syscall = struct { pub fn close(fd: std.os.fd_t) CloseError!void { return switch (darwin.getErrno(darwin.@"close$NOCANCEL"(fd))) { @@ -283,6 +338,30 @@ pub const Syscall = struct { }; } + pub fn open(path: [*:0]const u8, oflag: c_uint) OpenError!fd_t { + const fd = darwin.@"open$NOCANCEL"(path, oflag); + return switch (darwin.getErrno(fd)) { + .SUCCESS => fd, + .ACCES => error.AccessDenied, + .FBIG => error.FileTooBig, + .OVERFLOW => error.FileTooBig, + .ISDIR => error.IsDir, + .LOOP => error.SymLinkLoop, + .MFILE => error.ProcessFdQuotaExceeded, + .NAMETOOLONG => error.NameTooLong, + .NFILE => error.SystemFdQuotaExceeded, + .NODEV => error.NoDevice, + .NOENT => error.FileNotFound, + .NOMEM => error.SystemResources, + .NOSPC => error.NoSpaceLeft, + .NOTDIR => error.NotDir, + .PERM => error.AccessDenied, + .EXIST => error.PathAlreadyExists, + .BUSY => error.DeviceBusy, + else => |err| asError(err), + }; + } + pub const SocketError = error{ /// Permission to create a socket of the specified type and/or /// pro‐tocol is denied. @@ -407,7 +486,7 @@ io_pending: FIFO(Completion) = .{}, last_event_fd: std.atomic.Atomic(u32) = std.atomic.Atomic(u32).init(32), pub fn hasNoWork(this: *IO) bool { - return this.io_inflight == 0 and this.io_pending.peek() == null and this.completed.peek() == null and this.timeouts.peek() == null; + return this.io_inflight == 0 and this.io_pending.peek() == null and this.completed.peek() == null and this.timeouts.peek() == null; } pub fn init(_: u12, _: u32) !IO { @@ -1014,6 +1093,32 @@ pub fn fsync( ); } +/// macOS does not support reading for readiness for open() +/// so we just run this blocking +pub fn open( + _: *IO, + comptime Context: type, + context: Context, + comptime callback: fn ( + context: Context, + completion: *Completion, + result: OpenError!fd_t, + ) void, + completion: *Completion, + file_path: [:0]const u8, + flags: os.mode_t, + _: os.mode_t, +) void { + callback(context, completion, openSync(file_path, flags)); +} + +pub fn openSync( + file_path: [:0]const u8, + flags: os.mode_t, +) OpenError!fd_t { + return Syscall.open(file_path, @intCast(c_uint, flags)); +} + pub const ReadError = error{ WouldBlock, NotOpenForReading, diff --git a/src/io/io_linux.zig b/src/io/io_linux.zig index f4ea7fd46..89961fe00 100644 --- a/src/io/io_linux.zig +++ b/src/io/io_linux.zig @@ -287,7 +287,7 @@ pub const Errno = error{ EHWPOISON, Unexpected, }; -const errno_error_map: [135]Errno = brk: { +pub const errno_map: [135]Errno = brk: { var errors: [135]Errno = undefined; errors[0] = error.Unexpected; errors[1] = error.EPERM; @@ -428,7 +428,7 @@ const errno_error_map: [135]Errno = brk: { }; pub fn asError(err: anytype) Errno { return switch (err) { - 1...errno_error_map.len => errno_error_map[@intCast(u8, err)], + 1...errno_map.len => errno_map[@intCast(u8, err)], else => error.Unexpected, }; } @@ -702,6 +702,15 @@ pub const Completion = struct { op.offset, ); }, + .open => |op| { + linux.io_uring_prep_openat( + sqe, + std.os.AT.FD_CWD, + op.path, + op.flags, + op.mode, + ); + }, .readev => { var op = &completion.operation.readev; linux.io_uring_prep_readv(sqe, op.socket, &op.iovecs, 0); @@ -768,6 +777,29 @@ pub const Completion = struct { } else assert(completion.result == 0); completion.callback(completion.context, completion, &result); }, + .open => { + const result = if (completion.result < 0) switch (-completion.result) { + .SUCCESS => unreachable, + .ACCES => error.AccessDenied, + .FBIG => error.FileTooBig, + .OVERFLOW => error.FileTooBig, + .ISDIR => error.IsDir, + .LOOP => error.SymLinkLoop, + .MFILE => error.ProcessFdQuotaExceeded, + .NAMETOOLONG => error.NameTooLong, + .NFILE => error.SystemFdQuotaExceeded, + .NODEV => error.NoDevice, + .NOENT => error.FileNotFound, + .NOMEM => error.SystemResources, + .NOSPC => error.NoSpaceLeft, + .NOTDIR => error.NotDir, + .PERM => error.AccessDenied, + .EXIST => error.PathAlreadyExists, + .BUSY => return error.DeviceBusy, + else => |errno| asError(errno), + } else @intCast(linux.fd_t, completion.result); + completion.callback(completion.context, completion, &result); + }, .connect => { const result = if (completion.result < 0) switch (-completion.result) { os.EAGAIN, os.EINPROGRESS, os.EINTR => { @@ -923,6 +955,11 @@ const Operation = union(enum) { fsync: struct { fd: os.fd_t, }, + open: struct { + path: [*:0]const u8, + flags: u32, + mode: os.mode_t, + }, read: struct { fd: os.fd_t, buffer: []u8, @@ -1348,6 +1385,99 @@ pub fn send( self.enqueue(completion); } +pub const OpenError = error{ + /// In WASI, this error may occur when the file descriptor does + /// not hold the required rights to open a new resource relative to it. + AccessDenied, + SymLinkLoop, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + NoDevice, + FileNotFound, + + /// The path exceeded `MAX_PATH_BYTES` bytes. + NameTooLong, + + /// Insufficient kernel memory was available, or + /// the named file is a FIFO and per-user hard limit on + /// memory allocation for pipes has been reached. + SystemResources, + + /// The file is too large to be opened. This error is unreachable + /// for 64-bit targets, as well as when opening directories. + FileTooBig, + + /// The path refers to directory but the `O.DIRECTORY` flag was not provided. + IsDir, + + /// A new path cannot be created because the device has no room for the new file. + /// This error is only reachable when the `O.CREAT` flag is provided. + NoSpaceLeft, + + /// A component used as a directory in the path was not, in fact, a directory, or + /// `O.DIRECTORY` was specified and the path was not a directory. + NotDir, + + /// The path already exists and the `O.CREAT` and `O.EXCL` flags were provided. + PathAlreadyExists, + DeviceBusy, + + /// The underlying filesystem does not support file locks + FileLocksNotSupported, + + BadPathName, + InvalidUtf8, + + /// One of these three things: + /// * pathname refers to an executable image which is currently being + /// executed and write access was requested. + /// * pathname refers to a file that is currently in use as a swap + /// file, and the O_TRUNC flag was specified. + /// * pathname refers to a file that is currently being read by the + /// kernel (e.g., for module/firmware loading), and write access was + /// requested. + FileBusy, + + WouldBlock, +} || Errno; + +pub fn open( + self: *IO, + comptime Context: type, + context: Context, + comptime callback: fn ( + context: Context, + completion: *Completion, + result: OpenError!linux.fd_t, + ) void, + completion: *Completion, + file_path: [:0]const u8, + flags: os.mode_t, + mode: os.mode_t, +) void { + completion.* = .{ + .io = self, + .context = context, + .callback = struct { + fn wrapper(ctx: ?*anyopaque, comp: *Completion, res: *const anyopaque) void { + callback( + @intToPtr(Context, @ptrToInt(ctx)), + comp, + @intToPtr(*const OpenError!linux.fd_t, @ptrToInt(res)).*, + ); + } + }.wrapper, + .operation = .{ + .open = .{ + .file_path = file_path, + .flags = flags, + .mode = mode, + }, + }, + }; + self.enqueue(completion); +} + pub fn writev( self: *IO, comptime Context: type, diff --git a/src/javascript/jsc/api/html_rewriter.zig b/src/javascript/jsc/api/html_rewriter.zig index e314fc18d..3ea438556 100644 --- a/src/javascript/jsc/api/html_rewriter.zig +++ b/src/javascript/jsc/api/html_rewriter.zig @@ -208,20 +208,28 @@ pub const HTMLRewriter = struct { this.context.deinit(bun.default_allocator); } + pub fn beginTransform(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { + const new_context = this.context; + this.context = .{}; + return BufferOutputSink.init(new_context, global, response, this.builder); + } + + pub fn returnEmptyResponse(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { + var result = bun.default_allocator.create(Response) catch unreachable; + + response.cloneInto(result, getAllocator(global.ref())); + this.finalizeWithoutDestroy(); + return JSValue.fromRef(Response.makeMaybePooled(global.ref(), result)); + } + pub fn transform(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { var input = response.body.slice(); - if (input.len == 0) { - var result = bun.default_allocator.create(Response) catch unreachable; - - response.cloneInto(result, getAllocator(global.ref())); - this.finalizeWithoutDestroy(); - return JSValue.fromRef(Response.makeMaybePooled(global.ref(), result)); + if (input.len == 0 and !(response.body.value == .Blob and response.body.value.Blob.needsToReadFile())) { + return this.returnEmptyResponse(global, response); } - var new_context = this.context; - this.context = .{}; - return BufferOutputSink.init(new_context, global, response, this.builder); + return this.beginTransform(global, response); } pub const BufferOutputSink = struct { @@ -230,7 +238,7 @@ pub const HTMLRewriter = struct { rewriter: *LOLHTML.HTMLRewriter, context: LOLHTMLContext, response: *Response, - + input: JSC.WebCore.Blob = undefined, pub fn init(context: LOLHTMLContext, global: *JSGlobalObject, original: *Response, builder: *LOLHTML.HTMLRewriter.Builder) JSValue { var result = bun.default_allocator.create(Response) catch unreachable; var sink = bun.default_allocator.create(BufferOutputSink) catch unreachable; @@ -252,7 +260,7 @@ pub const HTMLRewriter = struct { sink.rewriter = builder.build( .UTF8, .{ - .preallocated_parsing_buffer_size = original.body.len(), + .preallocated_parsing_buffer_size = @maximum(original.body.len(), 1024), .max_allowed_memory_usage = std.math.maxInt(u32), }, false, @@ -282,19 +290,7 @@ pub const HTMLRewriter = struct { }, }, }; - { - var input = original.body.value.use(); - sink.bytes.growBy(input.sharedView().len) catch unreachable; - defer input.detach(); - sink.rewriter.write(input.sharedView()) catch { - sink.deinit(); - bun.default_allocator.destroy(result); - - return throwLOLHTMLError(global); - }; - } - // Hold off on cloning until we're actually done. result.body.init.headers = original.body.init.headers; result.body.init.method = original.body.init.method; result.body.init.status_code = original.body.init.status_code; @@ -302,17 +298,72 @@ pub const HTMLRewriter = struct { result.url = bun.default_allocator.dupe(u8, original.url) catch unreachable; result.status_text = bun.default_allocator.dupe(u8, original.status_text) catch unreachable; + var input: JSC.WebCore.Blob = original.body.value.use(); + + const is_pending = input.needsToReadFile(); + defer if (!is_pending) input.detach(); + + if (input.needsToReadFile()) { + input.doReadFileInternal(*BufferOutputSink, sink, onFinishedLoading, global); + } else if (sink.runOutputSink(input.sharedView())) |error_value| { + return error_value; + } + + // Hold off on cloning until we're actually done. + + return JSC.JSValue.fromRef( + Response.makeMaybePooled(sink.global.ref(), sink.response), + ); + } + + pub fn onFinishedLoading(sink: *BufferOutputSink, bytes: anyerror![]u8) void { + var input = sink.input; + defer input.detach(); + const data = bytes catch |err| { + if (sink.response.body.value == .Locked and sink.response.body.value.Locked.task == sink) { + sink.response.body.value = .{ .Empty = .{} }; + } + + sink.response.body.value.toError(err, sink.global); + sink.rewriter.end() catch {}; + sink.deinit(); + return; + }; + + _ = sink.runOutputSink(data, true); + } + + pub fn runOutputSink(sink: *BufferOutputSink, bytes: []const u8, is_async: bool) ?JSValue { + sink.bytes.growBy(bytes) catch unreachable; + var global = sink.global; + var response = sink.response; + sink.rewriter.write(bytes) catch { + sink.deinit(); + bun.default_allocator.destroy(sink); + + if (is_async) { + response.body.value.toErrorInstance(throwLOLHTMLError(global), global); + + return null; + } else { + return throwLOLHTMLError(global); + } + }; + sink.rewriter.end() catch { - result.finalize(); + if (!is_async) response.finalize(); sink.response = undefined; sink.deinit(); - return throwLOLHTMLError(global); + if (is_async) { + response.body.value.toErrorInstance(throwLOLHTMLError(global), global); + return null; + } else { + return throwLOLHTMLError(global); + } }; - return JSC.JSValue.fromRef( - Response.makeMaybePooled(sink.global.ref(), sink.response), - ); + return null; } pub const Sync = enum { suspended, pending, done }; diff --git a/src/javascript/jsc/bindings/bindings.cpp b/src/javascript/jsc/bindings/bindings.cpp index 85f7e6f1e..84d57a83c 100644 --- a/src/javascript/jsc/bindings/bindings.cpp +++ b/src/javascript/jsc/bindings/bindings.cpp @@ -246,6 +246,15 @@ unsigned char JSC__JSValue__jsType(JSC__JSValue JSValue0) return 0; } +JSC__JSValue JSC__JSPromise__asValue(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1) +{ + return JSC::JSValue::encode(JSC::JSValue(arg0)); +} +JSC__JSPromise* JSC__JSPromise__create(JSC__JSGlobalObject* arg0) +{ + return JSC::JSPromise::create(arg0->vm(), arg0->promiseStructure()); +} + // TODO: prevent this from allocating so much memory void JSC__JSValue___then(JSC__JSValue JSValue0, JSC__JSGlobalObject* globalObject, void* ctx, void (*ArgFn3)(JSC__JSGlobalObject* arg0, void* arg1, JSC__JSValue arg2, size_t arg3), void (*ArgFn4)(JSC__JSGlobalObject* arg0, void* arg1, JSC__JSValue arg2, size_t arg3)) { @@ -281,8 +290,12 @@ void JSC__JSValue___then(JSC__JSValue JSValue0, JSC__JSGlobalObject* globalObjec }); globalObject->vm().drainMicrotasks(); - JSC::JSPromise* promise = JSC::jsDynamicCast<JSC::JSPromise*>(globalObject->vm(), JSC::JSValue::decode(JSValue0).asCell()); - promise->performPromiseThen(globalObject, resolverFunction, rejecterFunction, JSC::jsUndefined()); + auto* cell = JSC::JSValue::decode(JSValue0).asCell(); + if (JSC::JSPromise* promise = JSC::jsDynamicCast<JSC::JSPromise*>(globalObject->vm(), cell)) { + promise->performPromiseThen(globalObject, resolverFunction, rejecterFunction, JSC::jsUndefined()); + } else if (JSC::JSInternalPromise* promise = JSC::jsDynamicCast<JSC::JSInternalPromise*>(globalObject->vm(), cell)) { + promise->then(globalObject, resolverFunction, rejecterFunction); + } } JSC__JSValue JSC__JSValue__parseJSON(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1) diff --git a/src/javascript/jsc/bindings/bindings.zig b/src/javascript/jsc/bindings/bindings.zig index da65d38d5..8e0f6cfcd 100644 --- a/src/javascript/jsc/bindings/bindings.zig +++ b/src/javascript/jsc/bindings/bindings.zig @@ -731,6 +731,14 @@ pub const JSPromise = extern struct { cppFn("rejectAsHandledException", .{ this, globalThis, value }); } + pub fn create(globalThis: *JSGlobalObject) *JSPromise { + return cppFn("create", .{globalThis}); + } + + pub fn asValue(this: *JSPromise, globalThis: *JSGlobalObject) JSValue { + return cppFn("asValue", .{ this, globalThis }); + } + pub const Extern = [_][]const u8{ "rejectWithCaughtException", "status", @@ -745,6 +753,8 @@ pub const JSPromise = extern struct { "rejectAsHandledException", "rejectedPromiseValue", "resolvedPromiseValue", + "asValue", + "create", }; }; diff --git a/src/javascript/jsc/bindings/headers-cpp.h b/src/javascript/jsc/bindings/headers-cpp.h index 3e8e7af5d..495e3cdb6 100644 --- a/src/javascript/jsc/bindings/headers-cpp.h +++ b/src/javascript/jsc/bindings/headers-cpp.h @@ -1,4 +1,4 @@ -//-- AUTOGENERATED FILE -- 1647769923 +//-- AUTOGENERATED FILE -- 1647847672 // clang-format off #pragma once diff --git a/src/javascript/jsc/bindings/headers.h b/src/javascript/jsc/bindings/headers.h index fb72e3798..55e560ac7 100644 --- a/src/javascript/jsc/bindings/headers.h +++ b/src/javascript/jsc/bindings/headers.h @@ -1,5 +1,5 @@ // clang-format: off -//-- AUTOGENERATED FILE -- 1647769923 +//-- AUTOGENERATED FILE -- 1647847672 #pragma once #include <stddef.h> @@ -295,6 +295,8 @@ CPP_DECL bJSC__SourceCode JSC__JSModuleRecord__sourceCode(JSC__JSModuleRecord* a #pragma mark - JSC::JSPromise +CPP_DECL JSC__JSValue JSC__JSPromise__asValue(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1); +CPP_DECL JSC__JSPromise* JSC__JSPromise__create(JSC__JSGlobalObject* arg0); CPP_DECL bool JSC__JSPromise__isHandled(const JSC__JSPromise* arg0, JSC__VM* arg1); CPP_DECL void JSC__JSPromise__reject(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1, JSC__JSValue JSValue2); CPP_DECL void JSC__JSPromise__rejectAsHandled(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1, JSC__JSValue JSValue2); @@ -426,7 +428,7 @@ CPP_DECL size_t WTF__String__length(WTF__String* arg0); #pragma mark - JSC::JSValue -CPP_DECL void JSC__JSValue___then(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, void* arg2, void (* ArgFn3)(JSC__JSGlobalObject* arg0, void* arg1, JSC__JSValue arg2, size_t arg3), void (* ArgFn4)(JSC__JSGlobalObject* arg0, void* arg1, JSC__JSValue arg2, size_t arg3)); +CPP_DECL void JSC__JSValue___then(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, void* arg2, void (* ArgFn3)(JSC__JSGlobalObject* arg0, void* arg1, JSC__JSValue JSValue2, size_t arg3), void (* ArgFn4)(JSC__JSGlobalObject* arg0, void* arg1, JSC__JSValue JSValue2, size_t arg3)); CPP_DECL bool JSC__JSValue__asArrayBuffer_(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, Bun__ArrayBuffer* arg2); CPP_DECL JSC__JSCell* JSC__JSValue__asCell(JSC__JSValue JSValue0); CPP_DECL JSC__JSInternalPromise* JSC__JSValue__asInternalPromise(JSC__JSValue JSValue0); diff --git a/src/javascript/jsc/bindings/headers.zig b/src/javascript/jsc/bindings/headers.zig index f920c74d9..bf8ab5d75 100644 --- a/src/javascript/jsc/bindings/headers.zig +++ b/src/javascript/jsc/bindings/headers.zig @@ -167,6 +167,8 @@ pub extern fn JSC__JSModuleLoader__linkAndEvaluateModule(arg0: [*c]JSC__JSGlobal pub extern fn JSC__JSModuleLoader__loadAndEvaluateModule(arg0: [*c]JSC__JSGlobalObject, arg1: [*c]const ZigString) [*c]JSC__JSInternalPromise; pub extern fn JSC__JSModuleLoader__loadAndEvaluateModuleEntryPoint(arg0: [*c]JSC__JSGlobalObject, arg1: [*c]const JSC__SourceCode) [*c]JSC__JSInternalPromise; pub extern fn JSC__JSModuleRecord__sourceCode(arg0: [*c]JSC__JSModuleRecord) bJSC__SourceCode; +pub extern fn JSC__JSPromise__asValue(arg0: [*c]JSC__JSPromise, arg1: [*c]JSC__JSGlobalObject) JSC__JSValue; +pub extern fn JSC__JSPromise__create(arg0: [*c]JSC__JSGlobalObject) [*c]JSC__JSPromise; pub extern fn JSC__JSPromise__isHandled(arg0: [*c]const JSC__JSPromise, arg1: [*c]JSC__VM) bool; pub extern fn JSC__JSPromise__reject(arg0: [*c]JSC__JSPromise, arg1: [*c]JSC__JSGlobalObject, JSValue2: JSC__JSValue) void; pub extern fn JSC__JSPromise__rejectAsHandled(arg0: [*c]JSC__JSPromise, arg1: [*c]JSC__JSGlobalObject, JSValue2: JSC__JSValue) void; diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig index d0dcb9ddc..5c4c1e5ca 100644 --- a/src/javascript/jsc/javascript.zig +++ b/src/javascript/jsc/javascript.zig @@ -297,12 +297,14 @@ pub fn IOTask(comptime Context: type) type { const AsyncTransformTask = @import("./api/transpiler.zig").TransformTask.AsyncTransformTask; const BunTimerTimeoutTask = Bun.Timer.Timeout.TimeoutTask; +const ReadFileTask = WebCore.Blob.Store.ReadFile.ReadFileTask; // const PromiseTask = JSInternalPromise.Completion.PromiseTask; pub const Task = TaggedPointerUnion(.{ FetchTasklet, Microtask, AsyncTransformTask, BunTimerTimeoutTask, + ReadFileTask, // PromiseTask, // TimeoutTasklet, }); @@ -478,6 +480,7 @@ pub const VirtualMachine = struct { event_loop: *EventLoop = undefined, ref_strings: JSC.RefString.Map = undefined, + file_blobs: JSC.WebCore.Blob.Store.Map, source_mappings: SavedSourceMap = undefined, response_objects_pool: ?*Response.Pool = null, @@ -525,6 +528,11 @@ pub const VirtualMachine = struct { transform_task.*.runFromJS(); finished += 1; }, + @field(Task.Tag, @typeName(ReadFileTask)) => { + var transform_task: *ReadFileTask = task.get(ReadFileTask).?; + transform_task.*.runFromJS(); + finished += 1; + }, else => unreachable, } } @@ -732,6 +740,7 @@ pub const VirtualMachine = struct { .macro_entry_points = @TypeOf(VirtualMachine.vm.macro_entry_points).init(allocator), .origin_timer = std.time.Timer.start() catch @panic("Please don't mess with timers."), .ref_strings = JSC.RefString.Map.init(allocator), + .file_blobs = JSC.WebCore.Blob.Store.Map.init(allocator), }; VirtualMachine.vm.regular_event_loop.tasks = EventLoop.Queue.init( default_allocator, @@ -784,6 +793,21 @@ pub const VirtualMachine = struct { _ = VirtualMachine.vm.ref_strings.remove(ref_string.hash); } + pub fn getFileBlob(this: *VirtualMachine, pathlike: JSC.Node.PathOrFileDescriptor) ?*JSC.WebCore.Blob.Store { + const hash = pathlike.hash(); + return this.file_blobs.get(hash); + } + + pub fn putFileBlob(this: *VirtualMachine, pathlike: JSC.Node.PathOrFileDescriptor, store: *JSC.WebCore.Blob.Store) !void { + const hash = pathlike.hash(); + try this.file_blobs.put(hash, store); + } + + pub fn removeFileBlob(this: *VirtualMachine, pathlike: JSC.Node.PathOrFileDescriptor) void { + const hash = pathlike.hash(); + _ = this.file_blobs.remove(hash); + } + pub fn refCountedResolvedSource(this: *VirtualMachine, code: []const u8, specifier: []const u8, source_url: []const u8, hash_: ?u32) ResolvedSource { var source = this.refCountedString(code, hash_, true); diff --git a/src/javascript/jsc/node/types.zig b/src/javascript/jsc/node/types.zig index dfb1b63ed..2195dd789 100644 --- a/src/javascript/jsc/node/types.zig +++ b/src/javascript/jsc/node/types.zig @@ -510,6 +510,13 @@ pub const PathOrFileDescriptor = union(Tag) { pub const Tag = enum { fd, path }; + pub fn hash(this: PathOrFileDescriptor) u64 { + return switch (this) { + .path => std.hash.Wyhash.hash(0, this.path.slice()), + .fd => std.hash.Wyhash.hash(0, std.mem.asBytes(&this.fd)), + }; + } + pub fn copyToStream(this: PathOrFileDescriptor, flags: FileSystemFlags, auto_close: bool, mode: Mode, allocator: std.mem.Allocator, stream: *Stream) !void { switch (this) { .fd => |fd| { diff --git a/src/javascript/jsc/webcore/response.zig b/src/javascript/jsc/webcore/response.zig index 6e9859352..bdecc6df4 100644 --- a/src/javascript/jsc/webcore/response.zig +++ b/src/javascript/jsc/webcore/response.zig @@ -30,6 +30,7 @@ const JSPrivateDataPtr = @import("../base.zig").JSPrivateDataPtr; const GetJSPrivateData = @import("../base.zig").GetJSPrivateData; const Environment = @import("../../../env.zig"); const ZigString = JSC.ZigString; +const IdentityContext = @import("../../../identity_context.zig").IdentityContext; const JSInternalPromise = JSC.JSInternalPromise; const JSPromise = JSC.JSPromise; const JSValue = JSC.JSValue; @@ -85,6 +86,7 @@ pub const Response = struct { .@"json" = .{ .rfn = constructJSON }, .@"redirect" = .{ .rfn = constructRedirect }, .@"error" = .{ .rfn = constructError }, + .@"file" = .{ .rfn = constructFile }, }, .{}, ); @@ -392,6 +394,63 @@ pub const Response = struct { } } + pub fn constructFile( + _: void, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, + ) js.JSObjectRef { + var args = JSC.Node.ArgumentsSlice.from(arguments); + + var response = Response{ + .body = Body{ + .init = Body.Init{ + .headers = null, + .status_code = 200, + }, + .value = Body.Value.empty, + }, + .allocator = getAllocator(ctx), + .url = "", + }; + + var path = JSC.Node.PathOrFileDescriptor.fromJS(ctx, &args, exception) orelse { + exception.* = JSC.toInvalidArguments("Expected file path string or file descriptor", .{}, ctx).asObjectRef(); + return js.JSValueMakeUndefined(ctx); + }; + + if (path == .path) { + path.path = .{ .string = bun.PathString.init(bun.default_allocator.dupe(u8, path.path.slice()) catch unreachable) }; + } + + if (args.nextEat()) |init| { + if (init.isUndefinedOrNull()) {} else if (init.isNumber()) { + response.body.init.status_code = @intCast(u16, @minimum(@maximum(0, init.toInt32()), std.math.maxInt(u16))); + } else { + if (Body.Init.init(getAllocator(ctx), ctx, init.asObjectRef()) catch null) |_init| { + response.body.init = _init; + } + } + } + + response.body.value = .{ + .Blob = brk: { + if (VirtualMachine.vm.getFileBlob(path)) |blob| { + blob.ref(); + break :brk Blob.initWithStore(blob, ctx.ptr()); + } + + break :brk Blob.initWithStore(Blob.Store.initFile(path, null, bun.default_allocator) catch unreachable, ctx.ptr()); + }, + }; + + var ptr = response.allocator.create(Response) catch unreachable; + ptr.* = response; + return Response.makeMaybePooled(ctx, ptr); + } + pub fn constructJSON( _: void, ctx: js.JSContextRef, @@ -445,15 +504,12 @@ pub const Response = struct { } else { if (Body.Init.init(getAllocator(ctx), ctx, init.asObjectRef()) catch null) |_init| { response.body.init = _init; - if (response.body.init.status_code == 0) { - response.body.init.status_code = 200; - } } } } var headers_ref = response.getOrCreateHeaders().leak(); - headers_ref.putHeaderNormalized("content-type", MimeType.json.value, false); + headers_ref.putDefaultHeader("content-type", MimeType.json.value); var ptr = response.allocator.create(Response) catch unreachable; ptr.* = response; @@ -1419,10 +1475,25 @@ pub const Headers = struct { headers.putHeader(key_slice.slice(), value_slice.slice(), append); } - pub fn putHeaderNormalized(headers: *Headers, key: []const u8, value: []const u8, comptime append: bool) void { + pub fn putDefaultHeader( + headers: *Headers, + key: []const u8, + value: []const u8, + ) void { + return putHeaderNormalizedDefault(headers, key, value, false, true); + } + + pub fn putHeaderNormalizedDefault( + headers: *Headers, + key: []const u8, + value: []const u8, + comptime append: bool, + comptime default: bool, + ) void { if (headers.getHeaderIndex(key)) |header_i| { - const existing_value = headers.entries.items(.value)[header_i]; + if (comptime default) return; + const existing_value = headers.entries.items(.value)[header_i]; if (append) { const end = @truncate(u32, value.len + existing_value.length + 2); const offset = headers.buf.items.len; @@ -1447,6 +1518,10 @@ pub const Headers = struct { } } + pub fn putHeaderNormalized(headers: *Headers, key: []const u8, value: []const u8, comptime append: bool) void { + return putHeaderNormalizedDefault(headers, key, value, append, false); + } + pub fn getHeaderIndex(headers: *const Headers, key: string) ?u32 { for (headers.entries.items(.name)) |name, i| { if (name.length == key.len and strings.eqlInsensitive(key, headers.asStr(name))) { @@ -1649,27 +1724,29 @@ pub const Blob = struct { globalThis: *JSGlobalObject = undefined, pub const Store = struct { - ptr: [*]u8 = undefined, - len: u32 = 0, + data: Data, + + mime_type: MimeType = MimeType.other, ref_count: u32 = 0, - cap: u32 = 0, - allocator: std.mem.Allocator, is_all_ascii: ?bool = null, + allocator: std.mem.Allocator, - pub inline fn ref(this: *Store) void { - this.ref_count += 1; + pub fn size(this: *const Store) u32 { + return switch (this.data) { + .bytes => this.data.bytes.len, + .file => std.math.maxInt(i32), + }; } - pub fn init(bytes: []u8, allocator: std.mem.Allocator) !*Store { - var store = try allocator.create(Store); - store.* = .{ - .ptr = bytes.ptr, - .len = @truncate(u32, bytes.len), - .ref_count = 1, - .cap = @truncate(u32, bytes.len), - .allocator = allocator, - }; - return store; + pub const Map = std.HashMap(u64, *JSC.WebCore.Blob.Store, IdentityContext(u64), 80); + + pub const Data = union(enum) { + bytes: ByteStore, + file: FileStore, + }; + + pub fn ref(this: *Store) void { + this.ref_count += 1; } pub fn external(ptr: ?*anyopaque, _: ?*anyopaque, _: usize) callconv(.C) void { @@ -1678,48 +1755,399 @@ pub const Blob = struct { this.deref(); } - pub fn fromArrayList(list: std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !*Store { - var store = try allocator.create(Store); + pub fn initFile(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?HTTPClient.MimeType, allocator: std.mem.Allocator) !*Store { + var store = try allocator.create(Blob.Store); store.* = .{ - .ptr = list.items.ptr, - .len = @truncate(u32, list.items.len), - .ref_count = 1, - .cap = @truncate(u32, list.capacity), + .data = .{ .file = FileStore.init(pathlike, mime_type) }, .allocator = allocator, + .ref_count = 1, }; return store; } - pub fn leakSlice(this: *const Store) []const u8 { - return this.ptr[0..this.len]; + pub fn init(bytes: []u8, allocator: std.mem.Allocator) !*Store { + var store = try allocator.create(Blob.Store); + store.* = .{ + .data = .{ .bytes = ByteStore.init(bytes, allocator) }, + .allocator = allocator, + .ref_count = 1, + }; + return store; } - pub fn slice(this: *Store) []u8 { - this.ref_count += 1; - return this.leakSlice(); - } + pub fn sharedView(this: Store) []u8 { + if (this.data == .bytes) + return this.data.bytes.slice(); - pub fn isOnlyOneRef(this: *const Store) bool { - return this.ref_count <= 1; + return &[_]u8{}; } - pub fn deref(this: *Store) void { + pub fn deref(this: *Blob.Store) void { this.ref_count -= 1; if (this.ref_count == 0) { - var allocated_slice = this.ptr[0..this.cap]; - var allocator = this.allocator; - allocator.free(allocated_slice); - allocator.destroy(this); + this.deinit(); } } - pub fn asArrayList(this: *Store) std.ArrayListUnmanaged(u8) { - this.ref_count += 1; + pub fn deinit(this: *Blob.Store) void { + switch (this.data) { + .bytes => |*bytes| { + bytes.deinit(); + }, + .file => |file| { + VirtualMachine.vm.removeFileBlob(file.pathlike); + }, + } + + this.allocator.destroy(this); + } + + pub fn fromArrayList(list: std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !*Blob.Store { + return try Blob.Store.init(list.items, allocator); + } + + pub const ReadFile = struct { + const OpenFrameType = if (Environment.isMac) + void + else + @Frame(ReadFile.getFdLinux); + file_store: FileStore, + byte_store: ByteStore = ByteStore{ .allocator = bun.default_allocator }, + store: ?*Store = null, + offset: u32 = 0, + max_length: u32 = std.math.maxInt(u32), + open_frame: OpenFrameType = undefined, + read_frame: @Frame(ReadFile.doRead) = undefined, + close_frame: @Frame(ReadFile.doClose) = undefined, + errno: ?anyerror = null, + open_completion: HTTPClient.NetworkThread.Completion = undefined, + opened_fd: JSC.Node.FileDescriptor = undefined, + read_completion: HTTPClient.NetworkThread.Completion = undefined, + read_len: u32 = 0, + read_off: u32 = 0, + size: u32 = 0, + buffer: []u8 = undefined, + runAsyncFrame: @Frame(ReadFile.runAsync) = undefined, + close_completion: HTTPClient.NetworkThread.Completion = undefined, + task: HTTPClient.NetworkThread.Task = undefined, + + onReadFileCompleteCtx: *anyopaque = undefined, + onReadFileComplete: OnReadFileCallback = undefined, + + pub const OnReadFileCallback = fn (ctx: *anyopaque, bytes: anyerror![]u8) void; + + const AsyncIO = HTTPClient.NetworkThread.AsyncIO; + + pub fn createWithCtx( + allocator: std.mem.Allocator, + store: *Store, + onReadFileContext: *anyopaque, + onReadFileComplete: OnReadFileCallback, + off: u32, + max_len: u32, + ) !*ReadFile { + var read_file = try allocator.create(ReadFile); + read_file.* = ReadFile{ + .file_store = store.data.file, + .offset = off, + .max_length = max_len, + .store = store, + .onReadFileCompleteCtx = onReadFileContext, + .onReadFileComplete = onReadFileComplete, + }; + store.ref(); + return read_file; + } + + pub fn create( + allocator: std.mem.Allocator, + store: *Store, + off: u32, + max_len: u32, + comptime Context: type, + context: Context, + comptime callback: fn (ctx: Context, bytes: anyerror![]u8) void, + ) !*ReadFile { + const Handler = struct { + pub fn run(ptr: *anyopaque, bytes: anyerror![]u8) void { + callback(bun.cast(Context, ptr), bytes); + } + }; + + return try ReadFile.createWithCtx(allocator, store, @ptrCast(*anyopaque, context), Handler.run, off, max_len); + } + + pub fn getFdMac(this: *ReadFile) AsyncIO.OpenError!JSC.Node.FileDescriptor { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + this.opened_fd = AsyncIO.openSync( + this.file_store.pathlike.path.sliceZ(&buf), + std.os.O.RDONLY, + ) catch |err| { + this.errno = err; + return err; + }; + return this.opened_fd; + } + + pub fn getFd(this: *ReadFile) AsyncIO.OpenError!JSC.Node.FileDescriptor { + if (this.file_store.pathlike == .fd) { + return this.file_store.pathlike.fd; + } + + if (comptime Environment.isMac) { + return try this.getFdMac(); + } else { + return try this.getFdLinux(); + } + } + + pub fn getFdLinux(this: *ReadFile) AsyncIO.OpenError!JSC.Node.FileDescriptor { + var aio = &AsyncIO.global; + + aio.open( + *ReadFile, + this, + onOpen, + &this.open_completion, + this.file_store.pathlike.path.sliceZ(), + std.os.O.RDONLY, + 0, + ); + + suspend { + this.open_frame = @frame().*; + } + + if (this.errno) |errno| { + return @errSetCast(AsyncIO.OpenError, errno); + } + + return this.opened_fd; + } + + pub fn doRead(this: *ReadFile) AsyncIO.ReadError!u32 { + var aio = &AsyncIO.global; + + var remaining = this.buffer[this.read_off..]; + this.read_len = 0; + aio.read( + *ReadFile, + this, + onRead, + &this.read_completion, + this.opened_fd, + remaining[0..@minimum(remaining.len, this.max_length - this.read_off)], + this.offset + this.read_off, + ); + + suspend { + this.read_frame = @frame().*; + } + + if (this.errno) |errno| { + return @errSetCast(AsyncIO.ReadError, errno); + } + + return this.read_len; + } + + pub fn doClose(this: *ReadFile) AsyncIO.CloseError!void { + var aio = &AsyncIO.global; + + aio.close( + *ReadFile, + this, + onClose, + &this.close_completion, + this.opened_fd, + ); + this.opened_fd = 0; + + suspend { + this.close_frame = @frame().*; + } + + if (this.errno) |errno| { + return @errSetCast(AsyncIO.CloseError, errno); + } + } + + pub const ReadFileTask = JSC.IOTask(@This()); + + pub fn then(this: *ReadFile, _: *JSC.JSGlobalObject) void { + var cb = this.onReadFileComplete; + var cb_ctx = this.onReadFileCompleteCtx; + + var store = this.store orelse { + var _err = this.errno orelse error.MissingData; + this.byte_store.deinit(); + bun.default_allocator.destroy(this); + cb(cb_ctx, _err); + return; + }; + + defer store.deref(); + if (this.file_store.pathlike == .path) { + VirtualMachine.vm.removeFileBlob(this.file_store.pathlike); + } + if (this.errno) |err| { + bun.default_allocator.destroy(this); + cb(cb_ctx, err); + return; + } + + var bytes = this.buffer; + if (store.data == .bytes) { + bun.default_allocator.free(this.buffer); + bytes = store.data.bytes.slice(); + } else if (store.data == .file) { + if (this.file_store.pathlike == .path) { + if (this.file_store.pathlike.path == .string) { + bun.default_allocator.free(this.file_store.pathlike.path.slice()); + } + } + store.data = .{ .bytes = ByteStore.init(bytes, bun.default_allocator) }; + } + + bun.default_allocator.destroy(this); + cb(cb_ctx, bytes); + } + pub fn run(this: *ReadFile, task: *ReadFileTask) void { + this.runAsyncFrame = async this.runAsync(task); + } + + pub fn onOpen(this: *ReadFile, _: *HTTPClient.NetworkThread.Completion, result: AsyncIO.OpenError!JSC.Node.FileDescriptor) void { + this.opened_fd = result catch |err| { + this.errno = err; + if (comptime Environment.isLinux) resume this.open_frame; + return; + }; + + if (comptime Environment.isLinux) resume this.open_frame; + } + + pub fn onRead(this: *ReadFile, _: *HTTPClient.NetworkThread.Completion, result: AsyncIO.ReadError!usize) void { + this.read_len = @truncate(u32, result catch |err| { + this.errno = err; + this.read_len = 0; + resume this.read_frame; + return; + }); + + resume this.read_frame; + } + + pub fn onClose(this: *ReadFile, _: *HTTPClient.NetworkThread.Completion, result: AsyncIO.CloseError!void) void { + result catch |err| { + this.errno = err; + resume this.close_frame; + return; + }; + + resume this.close_frame; + } + + pub fn runAsync(this: *ReadFile, task: *ReadFileTask) void { + defer task.onFinish(); + + const fd = this.getFd() catch return; + const needs_close = this.file_store.pathlike == .path; + const stat: std.os.Stat = switch (JSC.Node.Syscall.fstat(fd)) { + .result => |result| result, + .err => |err| { + this.errno = AsyncIO.asError(err.errno); + return; + }, + }; + if (!std.os.S.ISREG(stat.mode)) { + this.errno = error.ENOTSUP; + return; + } + + this.size = @minimum( + @truncate(u32, @intCast(u64, @maximum(@intCast(i64, stat.size), 0))), + this.max_length, + ); + if (this.size == 0) { + this.buffer = &[_]u8{}; + this.byte_store = ByteStore.init(this.buffer, bun.default_allocator); + + if (needs_close) { + this.doClose() catch {}; + } + return; + } + var bytes = bun.default_allocator.alloc(u8, this.size) catch |err| { + this.errno = err; + if (needs_close) { + this.doClose() catch {}; + } + return; + }; + this.buffer = bytes; + + var remain = bytes; + while (remain.len > 0) { + var read_len = this.doRead() catch { + if (needs_close) { + this.doClose() catch {}; + } + return; + }; + this.read_off += read_len; + if (read_len == 0) break; + remain = remain[read_len..]; + } + + _ = bun.default_allocator.resize(bytes, this.read_off); + this.buffer = bytes[0..this.read_off]; + this.byte_store = ByteStore.init(this.buffer, bun.default_allocator); + } + }; + }; + + pub const FileStore = struct { + pathlike: JSC.Node.PathOrFileDescriptor, + mime_type: HTTPClient.MimeType = HTTPClient.MimeType.other, + + pub fn init(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?HTTPClient.MimeType) FileStore { + return .{ .pathlike = pathlike, .mime_type = mime_type orelse HTTPClient.MimeType.other }; + } + }; + + pub const ByteStore = struct { + ptr: [*]u8 = undefined, + len: u32 = 0, + cap: u32 = 0, + allocator: std.mem.Allocator, + + pub fn init(bytes: []u8, allocator: std.mem.Allocator) ByteStore { + return .{ + .ptr = bytes.ptr, + .len = @truncate(u32, bytes.len), + .cap = @truncate(u32, bytes.len), + .allocator = allocator, + }; + } + + pub fn fromArrayList(list: std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !*ByteStore { + return ByteStore.init(list.items, allocator); + } + + pub fn slice(this: ByteStore) []u8 { + return this.ptr[0..this.len]; + } + + pub fn deinit(this: *ByteStore) void { + this.allocator.free(this.ptr[0..this.cap]); + } + + pub fn asArrayList(this: ByteStore) std.ArrayListUnmanaged(u8) { return this.asArrayListLeak(); } - pub fn asArrayListLeak(this: *const Store) std.ArrayListUnmanaged(u8) { + pub fn asArrayListLeak(this: ByteStore) std.ArrayListUnmanaged(u8) { return .{ .items = this.ptr[0..this.len], .capacity = this.cap, @@ -1772,6 +2200,10 @@ pub const Blob = struct { if (value.isError()) { return JSC.JSPromise.rejectedPromiseValue(global, value); } + + if (value.jsType() == .JSPromise) + return value; + return JSC.JSPromise.resolvedPromiseValue(global, value); } @@ -1940,9 +2372,31 @@ pub const Blob = struct { _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { + if (this.size == std.math.maxInt(i32)) { + this.resolveSize(); + if (this.size == std.math.maxInt(i32) and this.store != null) { + return JSValue.jsNumber(@as(u32, 0)).asRef(); + } + } + return JSValue.jsNumber(@truncate(u32, this.size)).asRef(); } + pub fn resolveSize(this: *Blob) void { + if (this.store) |store| { + if (store.data == .bytes) { + const offset = this.offset; + const store_size = store.size(); + if (store_size != std.math.maxInt(i32)) { + this.offset = @minimum(store_size, offset); + this.size = store_size - offset; + } + } + } else { + this.size = 0; + } + } + pub fn constructor( ctx: js.JSContextRef, _: js.JSObjectRef, @@ -2025,6 +2479,16 @@ pub const Blob = struct { }; } + pub fn initWithStore(store: *Blob.Store, globalThis: *JSGlobalObject) Blob { + return Blob{ + .size = store.size(), + .store = store, + .allocator = null, + .content_type = "", + .globalThis = globalThis, + }; + } + pub fn initEmpty(globalThis: *JSGlobalObject) Blob { return Blob{ .size = 0, @@ -2042,20 +2506,15 @@ pub const Blob = struct { } pub fn detach(this: *Blob) void { - if (this.store) |store| { - store.deref(); - this.store = null; - } + if (this.store != null) this.store.?.deref(); + this.store = null; } /// This does not duplicate /// This creates a new view /// and increment the reference count pub fn dupe(this: *const Blob) Blob { - if (this.store) |store| { - store.ref(); - } - + if (this.store != null) this.store.?.ref(); return this.*; } @@ -2070,12 +2529,16 @@ pub const Blob = struct { pub fn sharedView(this: *const Blob) []const u8 { if (this.size == 0 or this.store == null) return ""; - return this.store.?.leakSlice()[this.offset..][0..this.size]; + var slice_ = this.store.?.sharedView(); + if (slice_.len == 0) return ""; + slice_ = slice_[this.offset..]; + + return slice_[0..@minimum(slice_.len, @as(usize, this.size))]; } pub fn view(this: *const Blob) []const u8 { if (this.size == 0 or this.store == null) return ""; - return this.store.?.slice()[this.offset..][0..this.size]; + return this.store.?.sharedView()[this.offset..][0..this.size]; } pub const Lifetime = enum { @@ -2091,12 +2554,100 @@ pub const Blob = struct { // we can update the store's is_all_ascii flag // and any other Blob that points to the same store // can skip checking the encoding - if (this.size > 0 and this.store != null and this.offset == 0) { + if (this.size > 0 and this.offset == 0) { this.store.?.is_all_ascii = is_all_ascii; } } + pub fn NewReadFileHandler(comptime Function: anytype, comptime lifetime: Lifetime) type { + return struct { + context: Blob, + promise: *JSPromise, + globalThis: *JSGlobalObject, + pub fn run(handler: *@This(), bytes_: anyerror![]u8) void { + var promise = handler.promise; + var blob = handler.context; + blob.allocator = null; + var globalThis = handler.globalThis; + bun.default_allocator.destroy(handler); + var bytes = bytes_ catch |err| { + var error_string = ZigString.init( + std.fmt.allocPrint(bun.default_allocator, "Failed to read file: {s}", .{std.mem.span(@errorName(err))}) catch unreachable, + ); + error_string.mark(); + blob.detach(); + + promise.reject(globalThis, error_string.toErrorInstance(globalThis)); + return; + }; + + if (blob.size > 0) + blob.size = @minimum(@truncate(u32, bytes.len), blob.size); + + promise.resolve(globalThis, Function(&blob, globalThis, comptime lifetime)); + } + }; + } + + pub fn NewInternalReadFileHandler(comptime Context: type, comptime Function: anytype) type { + return struct { + context: Context, + + pub fn run(handler: *anyopaque, bytes_: anyerror![]u8) void { + Function(bun.cast(Context, handler.context), bytes_); + } + }; + } + + pub fn doReadFileInternal(this: *Blob, comptime Handler: type, ctx: Handler, comptime Function: anytype, global: *JSGlobalObject) void { + var file_read = Store.ReadFile.createWithCtx( + bun.default_allocator, + this.store.?, + this.offset, + this.size, + Handler, + ctx, + Function, + ) catch unreachable; + var read_file_task = Store.ReadFile.ReadFileTask.createOnJSThread(bun.default_allocator, global, file_read) catch unreachable; + read_file_task.schedule(); + } + + pub fn doReadFile(this: *Blob, comptime Function: anytype, comptime lifetime: Lifetime, global: *JSGlobalObject) JSValue { + const Handler = NewReadFileHandler(Function, lifetime); + var promise = JSPromise.create(global); + + var handler = Handler{ + .context = this.*, + .promise = promise, + .globalThis = global, + }; + + var ptr = bun.default_allocator.create(Handler) catch unreachable; + ptr.* = handler; + var file_read = Store.ReadFile.create( + bun.default_allocator, + this.store.?, + this.offset, + this.size, + *Handler, + ptr, + Handler.run, + ) catch unreachable; + var read_file_task = Store.ReadFile.ReadFileTask.createOnJSThread(bun.default_allocator, global, file_read) catch unreachable; + read_file_task.schedule(); + return promise.asValue(global); + } + + pub fn needsToReadFile(this: *const Blob) bool { + return this.store != null and this.store.?.data == .file; + } + pub fn toString(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue { + if (this.needsToReadFile()) { + return this.doReadFile(toString, lifetime, global); + } + var view_: []const u8 = this.sharedView(); @@ -2112,10 +2663,11 @@ pub const Blob = struct { // if toUTF16Alloc returns null, it means there are no non-ASCII characters // instead of erroring, invalid characters will become a U+FFFD replacement character if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch unreachable) |external| { + this.setIsASCIIFlag(false); + if (lifetime == .transfer) { this.detach(); } - this.setIsASCIIFlag(false); return ZigString.toExternalU16(external.ptr, external.len, global); } @@ -2143,7 +2695,15 @@ pub const Blob = struct { } } + pub fn toJSONShare(this: *Blob, global: *JSGlobalObject, comptime _: Lifetime) JSValue { + return toJSON(this, global); + } + pub fn toJSON(this: *Blob, global: *JSGlobalObject) JSValue { + if (this.needsToReadFile()) { + return this.doReadFile(toJSONShare, .share, global); + } + var view_ = this.sharedView(); if (view_.len == 0) @@ -2171,6 +2731,10 @@ pub const Blob = struct { ).parseJSON(global); } pub fn toArrayBuffer(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue { + if (this.needsToReadFile()) { + return this.doReadFile(toArrayBuffer, lifetime, global); + } + var view_ = this.sharedView(); if (view_.len == 0) @@ -2603,6 +3167,7 @@ pub const Body = struct { promise: ?JSValue = null, global: *JSGlobalObject, task: ?*anyopaque = null, + callback: ?fn (ctx: *anyopaque, value: *Value) void = null, deinit: bool = false, }; @@ -2611,12 +3176,14 @@ pub const Body = struct { Locked: PendingValue, Used: void, Empty: void, + Error: JSValue, pub const Tag = enum { Blob, Locked, Used, Empty, + Error, }; pub const empty = Value{ .Empty = .{} }; @@ -2640,6 +3207,41 @@ pub const Body = struct { } } + pub fn toErrorInstance(this: *Value, error_instance: JSC.JSValue, global: *JSGlobalObject) void { + if (this.value == .Locked) { + var locked = this.Locked; + locked.deinit = true; + if (locked.promise) |promise| { + if (promise.asInternalPromise()) |internal| { + internal.reject(global, error_instance); + } + + JSC.C.JSValueUnprotect(global.ref(), promise.asObjectRef()); + locked.promise = null; + } + + this.* = .{ .Error = error_instance }; + if (locked.callback) |callback| { + locked.callback = null; + callback(locked.task.?, this); + } + return; + } + + this.* = .{ .Error = error_instance }; + } + + pub fn toError(this: *Value, err: anyerror, global: *JSGlobalObject) void { + var error_str = ZigString.init(std.fmt.allocPrint( + bun.default_allocator, + "Error reading file {s}", + .{@errorName(err)}, + )); + error_str.mark(); + var error_instance = error_str.toErrorInstance(global); + return this.toErrorInstance(error_instance, global); + } + pub fn deinit(this: *Value) void { const tag = @as(Tag, this.*); if (tag == .Locked) { @@ -2651,6 +3253,10 @@ pub const Body = struct { this.Blob.deinit(); this.* = Value.empty; } + + if (tag == .Error) { + JSC.C.JSValueUnprotect(VirtualMachine.vm.global.vm(), this.Error.asObjectRef()); + } } pub fn clone(this: Value, _: std.mem.Allocator) Value { |