aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2022-03-21 02:21:51 -0700
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2022-03-21 02:21:51 -0700
commitfa343fa8adb25a7e307e91a3cd3c2c3f24e0152b (patch)
tree63a7ed4a7d787dd2b61330f5c1e69b2af0737e39
parent1f93de264f55e7a392bd34dbb9bda0b2365d7c88 (diff)
downloadbun-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.js49
-rw-r--r--src/bundler/entry_points.zig3
-rw-r--r--src/io/io_darwin.zig127
-rw-r--r--src/io/io_linux.zig134
-rw-r--r--src/javascript/jsc/api/html_rewriter.zig107
-rw-r--r--src/javascript/jsc/bindings/bindings.cpp17
-rw-r--r--src/javascript/jsc/bindings/bindings.zig10
-rw-r--r--src/javascript/jsc/bindings/headers-cpp.h2
-rw-r--r--src/javascript/jsc/bindings/headers.h6
-rw-r--r--src/javascript/jsc/bindings/headers.zig2
-rw-r--r--src/javascript/jsc/javascript.zig24
-rw-r--r--src/javascript/jsc/node/types.zig7
-rw-r--r--src/javascript/jsc/webcore/response.zig718
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 {