aboutsummaryrefslogtreecommitdiff
path: root/src/bun.js
diff options
context:
space:
mode:
authorGravatar Jarred SUmner <jarred@jarredsumner.com> 2023-01-13 11:27:16 -0800
committerGravatar Jarred SUmner <jarred@jarredsumner.com> 2023-01-13 11:27:16 -0800
commit996ef44c021a692403082c70e0eedc2ce1696eff (patch)
treea2d238991ca017a30ffa01e2cc005f802cbac15f /src/bun.js
parent734b5b89da07fa074ea1c2a1013f32a56fc58637 (diff)
downloadbun-996ef44c021a692403082c70e0eedc2ce1696eff.tar.gz
bun-996ef44c021a692403082c70e0eedc2ce1696eff.tar.zst
bun-996ef44c021a692403082c70e0eedc2ce1696eff.zip
Split some things into more files and use bun namespace instead of import more
Diffstat (limited to 'src/bun.js')
-rw-r--r--src/bun.js/api/bun.zig10
-rw-r--r--src/bun.js/api/ffi.zig10
-rw-r--r--src/bun.js/api/filesystem_router.zig4
-rw-r--r--src/bun.js/api/html_rewriter.zig2
-rw-r--r--src/bun.js/api/server.zig10
-rw-r--r--src/bun.js/api/transpiler.zig2
-rw-r--r--src/bun.js/config.zig2
-rw-r--r--src/bun.js/javascript.zig10
-rw-r--r--src/bun.js/module_loader.zig10
-rw-r--r--src/bun.js/rare_data.zig2
-rw-r--r--src/bun.js/test/jest.zig2
-rw-r--r--src/bun.js/webcore.zig3
-rw-r--r--src/bun.js/webcore/blob.zig3391
-rw-r--r--src/bun.js/webcore/body.zig1029
-rw-r--r--src/bun.js/webcore/encoding.zig2
-rw-r--r--src/bun.js/webcore/request.zig454
-rw-r--r--src/bun.js/webcore/response.zig4730
-rw-r--r--src/bun.js/webcore/streams.zig2
18 files changed, 4920 insertions, 4755 deletions
diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig
index 32bd0a9ec..5d296927a 100644
--- a/src/bun.js/api/bun.zig
+++ b/src/bun.js/api/bun.zig
@@ -15,12 +15,12 @@ const Fs = @import("../../fs.zig");
const Resolver = @import("../../resolver/resolver.zig");
const ast = @import("../../import_record.zig");
const NodeModuleBundle = @import("../../node_module_bundle.zig").NodeModuleBundle;
-const MacroEntryPoint = @import("../../bundler.zig").MacroEntryPoint;
+const MacroEntryPoint = bun.bundler.MacroEntryPoint;
const logger = @import("bun").logger;
const Api = @import("../../api/schema.zig").Api;
const options = @import("../../options.zig");
-const Bundler = @import("../../bundler.zig").Bundler;
-const ServerEntryPoint = @import("../../bundler.zig").ServerEntryPoint;
+const Bundler = bun.Bundler;
+const ServerEntryPoint = bun.bundler.ServerEntryPoint;
const js_printer = @import("../../js_printer.zig");
const js_parser = @import("../../js_parser.zig");
const js_ast = @import("../../js_ast.zig");
@@ -33,7 +33,7 @@ const Runtime = @import("../../runtime.zig");
const Router = @import("./filesystem_router.zig");
const ImportRecord = ast.ImportRecord;
const DotEnv = @import("../../env_loader.zig");
-const ParseResult = @import("../../bundler.zig").ParseResult;
+const ParseResult = bun.bundler.ParseResult;
const PackageJSON = @import("../../resolver/package_json.zig").PackageJSON;
const MacroRemap = @import("../../resolver/package_json.zig").MacroMap;
const WebCore = @import("bun").JSC.WebCore;
@@ -72,7 +72,7 @@ const JSFunction = @import("bun").JSC.JSFunction;
const Config = @import("../config.zig");
const URL = @import("../../url.zig").URL;
const Transpiler = @import("./transpiler.zig");
-const VirtualMachine = @import("../javascript.zig").VirtualMachine;
+const VirtualMachine = JSC.VirtualMachine;
const IOTask = JSC.IOTask;
const zlib = @import("../../zlib.zig");
const Which = @import("../../which.zig");
diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig
index 8ff86f04a..896ebf077 100644
--- a/src/bun.js/api/ffi.zig
+++ b/src/bun.js/api/ffi.zig
@@ -16,12 +16,12 @@ const Fs = @import("../../fs.zig");
const Resolver = @import("../../resolver/resolver.zig");
const ast = @import("../../import_record.zig");
const NodeModuleBundle = @import("../../node_module_bundle.zig").NodeModuleBundle;
-const MacroEntryPoint = @import("../../bundler.zig").MacroEntryPoint;
+const MacroEntryPoint = bun.bundler.MacroEntryPoint;
const logger = @import("bun").logger;
const Api = @import("../../api/schema.zig").Api;
const options = @import("../../options.zig");
-const Bundler = @import("../../bundler.zig").Bundler;
-const ServerEntryPoint = @import("../../bundler.zig").ServerEntryPoint;
+const Bundler = bun.Bundler;
+const ServerEntryPoint = bun.bundler.ServerEntryPoint;
const js_printer = @import("../../js_printer.zig");
const js_parser = @import("../../js_parser.zig");
const js_ast = @import("../../js_ast.zig");
@@ -33,7 +33,7 @@ const ZigString = @import("bun").JSC.ZigString;
const Runtime = @import("../../runtime.zig");
const ImportRecord = ast.ImportRecord;
const DotEnv = @import("../../env_loader.zig");
-const ParseResult = @import("../../bundler.zig").ParseResult;
+const ParseResult = bun.bundler.ParseResult;
const PackageJSON = @import("../../resolver/package_json.zig").PackageJSON;
const MacroRemap = @import("../../resolver/package_json.zig").MacroMap;
const WebCore = @import("bun").JSC.WebCore;
@@ -72,7 +72,7 @@ const JSFunction = @import("bun").JSC.JSFunction;
const Config = @import("../config.zig");
const URL = @import("../../url.zig").URL;
const Transpiler = @import("./transpiler.zig");
-const VirtualMachine = @import("../javascript.zig").VirtualMachine;
+const VirtualMachine = JSC.VirtualMachine;
const IOTask = JSC.IOTask;
const ComptimeStringMap = @import("../../comptime_string_map.zig").ComptimeStringMap;
diff --git a/src/bun.js/api/filesystem_router.zig b/src/bun.js/api/filesystem_router.zig
index 3271e3cc7..7141b5f5e 100644
--- a/src/bun.js/api/filesystem_router.zig
+++ b/src/bun.js/api/filesystem_router.zig
@@ -8,8 +8,8 @@ const bun = @import("bun");
const string = bun.string;
const JSC = @import("bun").JSC;
const js = JSC.C;
-const WebCore = @import("../webcore/response.zig");
-const Bundler = @import("../../bundler.zig");
+const WebCore = JSC.WebCore;
+const Bundler = bun.bundler;
const VirtualMachine = JavaScript.VirtualMachine;
const ScriptSrcStream = std.io.FixedBufferStream([]u8);
const ZigString = JSC.ZigString;
diff --git a/src/bun.js/api/html_rewriter.zig b/src/bun.js/api/html_rewriter.zig
index f5abdc734..1c73223a4 100644
--- a/src/bun.js/api/html_rewriter.zig
+++ b/src/bun.js/api/html_rewriter.zig
@@ -10,7 +10,7 @@ const JSC = @import("bun").JSC;
const js = JSC.C;
const WebCore = @import("../webcore/response.zig");
const Router = @This();
-const Bundler = @import("../../bundler.zig");
+const Bundler = bun.bundler;
const VirtualMachine = JavaScript.VirtualMachine;
const ScriptSrcStream = std.io.FixedBufferStream([]u8);
const ZigString = JSC.ZigString;
diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig
index 0060924d7..7821f795c 100644
--- a/src/bun.js/api/server.zig
+++ b/src/bun.js/api/server.zig
@@ -15,12 +15,12 @@ const Fs = @import("../../fs.zig");
const Resolver = @import("../../resolver/resolver.zig");
const ast = @import("../../import_record.zig");
const NodeModuleBundle = @import("../../node_module_bundle.zig").NodeModuleBundle;
-const MacroEntryPoint = @import("../../bundler.zig").MacroEntryPoint;
+const MacroEntryPoint = bun.bundler.MacroEntryPoint;
const logger = @import("bun").logger;
const Api = @import("../../api/schema.zig").Api;
const options = @import("../../options.zig");
-const Bundler = @import("../../bundler.zig").Bundler;
-const ServerEntryPoint = @import("../../bundler.zig").ServerEntryPoint;
+const Bundler = bun.Bundler;
+const ServerEntryPoint = bun.bundler.ServerEntryPoint;
const js_printer = @import("../../js_printer.zig");
const js_parser = @import("../../js_parser.zig");
const js_ast = @import("../../js_ast.zig");
@@ -32,7 +32,7 @@ const ZigString = @import("bun").JSC.ZigString;
const Runtime = @import("../../runtime.zig");
const ImportRecord = ast.ImportRecord;
const DotEnv = @import("../../env_loader.zig");
-const ParseResult = @import("../../bundler.zig").ParseResult;
+const ParseResult = bun.bundler.ParseResult;
const PackageJSON = @import("../../resolver/package_json.zig").PackageJSON;
const MacroRemap = @import("../../resolver/package_json.zig").MacroMap;
const WebCore = @import("bun").JSC.WebCore;
@@ -71,7 +71,7 @@ const JSFunction = @import("bun").JSC.JSFunction;
const Config = @import("../config.zig");
const URL = @import("../../url.zig").URL;
const Transpiler = @import("./transpiler.zig");
-const VirtualMachine = @import("../javascript.zig").VirtualMachine;
+const VirtualMachine = JSC.VirtualMachine;
const IOTask = JSC.IOTask;
const is_bindgen = JSC.is_bindgen;
const uws = @import("bun").uws;
diff --git a/src/bun.js/api/transpiler.zig b/src/bun.js/api/transpiler.zig
index abcef8248..7772b16db 100644
--- a/src/bun.js/api/transpiler.zig
+++ b/src/bun.js/api/transpiler.zig
@@ -9,7 +9,7 @@ const string = bun.string;
const JSC = @import("bun").JSC;
const js = JSC.C;
const WebCore = @import("../webcore/response.zig");
-const Bundler = @import("../../bundler.zig");
+const Bundler = bun.bundler;
const options = @import("../../options.zig");
const VirtualMachine = JavaScript.VirtualMachine;
const ScriptSrcStream = std.io.FixedBufferStream([]u8);
diff --git a/src/bun.js/config.zig b/src/bun.js/config.zig
index ee3e30412..e237064a6 100644
--- a/src/bun.js/config.zig
+++ b/src/bun.js/config.zig
@@ -17,7 +17,7 @@ const NodeModuleBundle = @import("../node_module_bundle.zig").NodeModuleBundle;
const logger = @import("bun").logger;
const Api = @import("../api/schema.zig").Api;
const options = @import("../options.zig");
-const Bundler = @import("../bundler.zig").ServeBundler;
+const Bundler = bun.bundler.ServeBundler;
const js_printer = @import("../js_printer.zig");
const http = @import("../http.zig");
diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig
index c2c43cf81..a6474799b 100644
--- a/src/bun.js/javascript.zig
+++ b/src/bun.js/javascript.zig
@@ -22,14 +22,14 @@ const Fs = @import("../fs.zig");
const Resolver = @import("../resolver/resolver.zig");
const ast = @import("../import_record.zig");
const NodeModuleBundle = @import("../node_module_bundle.zig").NodeModuleBundle;
-const MacroEntryPoint = @import("../bundler.zig").MacroEntryPoint;
-const ParseResult = @import("../bundler.zig").ParseResult;
+const MacroEntryPoint = bun.bundler.MacroEntryPoint;
+const ParseResult = bun.bundler.ParseResult;
const logger = @import("bun").logger;
const Api = @import("../api/schema.zig").Api;
const options = @import("../options.zig");
-const Bundler = @import("../bundler.zig").Bundler;
-const PluginRunner = @import("../bundler.zig").PluginRunner;
-const ServerEntryPoint = @import("../bundler.zig").ServerEntryPoint;
+const Bundler = bun.Bundler;
+const PluginRunner = bun.bundler.PluginRunner;
+const ServerEntryPoint = bun.bundler.ServerEntryPoint;
const js_printer = @import("../js_printer.zig");
const js_parser = @import("../js_parser.zig");
const js_ast = @import("../js_ast.zig");
diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig
index e4ee611cf..dc4b156f7 100644
--- a/src/bun.js/module_loader.zig
+++ b/src/bun.js/module_loader.zig
@@ -22,14 +22,14 @@ const Fs = @import("../fs.zig");
const Resolver = @import("../resolver/resolver.zig");
const ast = @import("../import_record.zig");
const NodeModuleBundle = @import("../node_module_bundle.zig").NodeModuleBundle;
-const MacroEntryPoint = @import("../bundler.zig").MacroEntryPoint;
-const ParseResult = @import("../bundler.zig").ParseResult;
+const MacroEntryPoint = bun.bundler.MacroEntryPoint;
+const ParseResult = bun.bundler.ParseResult;
const logger = @import("bun").logger;
const Api = @import("../api/schema.zig").Api;
const options = @import("../options.zig");
-const Bundler = @import("../bundler.zig").Bundler;
-const PluginRunner = @import("../bundler.zig").PluginRunner;
-const ServerEntryPoint = @import("../bundler.zig").ServerEntryPoint;
+const Bundler = bun.Bundler;
+const PluginRunner = bun.bundler.PluginRunner;
+const ServerEntryPoint = bun.bundler.ServerEntryPoint;
const js_printer = @import("../js_printer.zig");
const js_parser = @import("../js_parser.zig");
const js_ast = @import("../js_ast.zig");
diff --git a/src/bun.js/rare_data.zig b/src/bun.js/rare_data.zig
index a14b3282a..253eb2cf2 100644
--- a/src/bun.js/rare_data.zig
+++ b/src/bun.js/rare_data.zig
@@ -1,5 +1,5 @@
const EditorContext = @import("../open.zig").EditorContext;
-const Blob = @import("./webcore/response.zig").Blob;
+const Blob = JSC.WebCore.Blob;
const default_allocator = @import("bun").default_allocator;
const Output = @import("bun").Output;
const RareData = @This();
diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig
index 4b31f7309..f43c97a82 100644
--- a/src/bun.js/test/jest.zig
+++ b/src/bun.js/test/jest.zig
@@ -39,7 +39,7 @@ const JSError = JSC.JSError;
const JSGlobalObject = JSC.JSGlobalObject;
const JSObject = JSC.JSObject;
-const VirtualMachine = @import("../javascript.zig").VirtualMachine;
+const VirtualMachine = JSC.VirtualMachine;
const Task = @import("../javascript.zig").Task;
const Fs = @import("../../fs.zig");
diff --git a/src/bun.js/webcore.zig b/src/bun.js/webcore.zig
index a2aa39382..3a7a978f0 100644
--- a/src/bun.js/webcore.zig
+++ b/src/bun.js/webcore.zig
@@ -1,6 +1,9 @@
pub usingnamespace @import("./webcore/response.zig");
pub usingnamespace @import("./webcore/encoding.zig");
pub usingnamespace @import("./webcore/streams.zig");
+pub usingnamespace @import("./webcore/blob.zig");
+pub usingnamespace @import("./webcore/request.zig");
+pub usingnamespace @import("./webcore/body.zig");
const JSC = @import("bun").JSC;
const std = @import("std");
diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig
new file mode 100644
index 000000000..1bd338c06
--- /dev/null
+++ b/src/bun.js/webcore/blob.zig
@@ -0,0 +1,3391 @@
+const std = @import("std");
+const Api = @import("../../api/schema.zig").Api;
+const bun = @import("bun");
+const RequestContext = @import("../../http.zig").RequestContext;
+const MimeType = @import("../../http.zig").MimeType;
+const ZigURL = @import("../../url.zig").URL;
+const HTTPClient = @import("bun").HTTP;
+const NetworkThread = HTTPClient.NetworkThread;
+const AsyncIO = NetworkThread.AsyncIO;
+const JSC = @import("bun").JSC;
+const js = JSC.C;
+
+const Method = @import("../../http/method.zig").Method;
+const FetchHeaders = JSC.FetchHeaders;
+const ObjectPool = @import("../../pool.zig").ObjectPool;
+const SystemError = JSC.SystemError;
+const Output = @import("bun").Output;
+const MutableString = @import("bun").MutableString;
+const strings = @import("bun").strings;
+const string = @import("bun").string;
+const default_allocator = @import("bun").default_allocator;
+const FeatureFlags = @import("bun").FeatureFlags;
+const ArrayBuffer = @import("../base.zig").ArrayBuffer;
+const Properties = @import("../base.zig").Properties;
+const NewClass = @import("../base.zig").NewClass;
+const d = @import("../base.zig").d;
+const castObj = @import("../base.zig").castObj;
+const getAllocator = @import("../base.zig").getAllocator;
+const JSPrivateDataPtr = @import("../base.zig").JSPrivateDataPtr;
+const GetJSPrivateData = @import("../base.zig").GetJSPrivateData;
+const Environment = @import("../../env.zig");
+const ZigString = JSC.ZigString;
+const IdentityContext = @import("../../identity_context.zig").IdentityContext;
+const JSPromise = JSC.JSPromise;
+const JSValue = JSC.JSValue;
+const JSError = JSC.JSError;
+const JSGlobalObject = JSC.JSGlobalObject;
+const NullableAllocator = @import("../../nullable_allocator.zig").NullableAllocator;
+
+const VirtualMachine = JSC.VirtualMachine;
+const Task = JSC.Task;
+const JSPrinter = @import("../../js_printer.zig");
+const picohttp = @import("bun").picohttp;
+const StringJoiner = @import("../../string_joiner.zig");
+const uws = @import("bun").uws;
+
+const null_fd = bun.invalid_fd;
+const Response = JSC.WebCore.Response;
+const Body = JSC.WebCore.Body;
+const Request = JSC.WebCore.Request;
+
+const PathOrBlob = union(enum) {
+ path: JSC.Node.PathOrFileDescriptor,
+ blob: Blob,
+
+ pub fn fromJSNoCopy(ctx: js.JSContextRef, args: *JSC.Node.ArgumentsSlice, exception: js.ExceptionRef) ?PathOrBlob {
+ if (JSC.Node.PathOrFileDescriptor.fromJS(ctx, args, args.arena.allocator(), exception)) |path| {
+ return PathOrBlob{
+ .path = path,
+ };
+ }
+
+ const arg = args.nextEat() orelse return null;
+
+ if (arg.as(Blob)) |blob| {
+ return PathOrBlob{
+ .blob = blob.*,
+ };
+ }
+
+ return null;
+ }
+};
+
+pub const Blob = struct {
+ pub usingnamespace JSC.Codegen.JSBlob;
+
+ size: SizeType = 0,
+ offset: SizeType = 0,
+ /// When set, the blob will be freed on finalization callbacks
+ /// If the blob is contained in Response or Request, this must be null
+ allocator: ?std.mem.Allocator = null,
+ store: ?*Store = null,
+ content_type: string = "",
+ content_type_allocated: bool = false,
+
+ /// JavaScriptCore strings are either latin1 or UTF-16
+ /// When UTF-16, they're nearly always due to non-ascii characters
+ is_all_ascii: ?bool = null,
+
+ globalThis: *JSGlobalObject = undefined,
+
+ /// Max int of double precision
+ /// 9 petabytes is probably enough for awhile
+ /// We want to avoid coercing to a BigInt because that's a heap allocation
+ /// and it's generally just harder to use
+ pub const SizeType = u52;
+ pub const max_size = std.math.maxInt(SizeType);
+
+ pub fn contentType(this: *const Blob) string {
+ return this.content_type;
+ }
+
+ pub fn isDetached(this: *const Blob) bool {
+ return this.store == null;
+ }
+
+ pub fn writeFormatForSize(size: usize, writer: anytype, comptime enable_ansi_colors: bool) !void {
+ try writer.writeAll(comptime Output.prettyFmt("<r>Blob<r>", enable_ansi_colors));
+ try writer.print(
+ comptime Output.prettyFmt(" (<yellow>{any}<r>)", enable_ansi_colors),
+ .{
+ bun.fmt.size(size),
+ },
+ );
+ }
+ pub fn writeFormat(this: *const Blob, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void {
+ const Writer = @TypeOf(writer);
+
+ if (this.isDetached()) {
+ try writer.writeAll(comptime Output.prettyFmt("<d>[<r>Blob<r> detached<d>]<r>", enable_ansi_colors));
+ return;
+ }
+
+ {
+ var store = this.store.?;
+ switch (store.data) {
+ .file => |file| {
+ try writer.writeAll(comptime Output.prettyFmt("<r>FileRef<r>", enable_ansi_colors));
+ switch (file.pathlike) {
+ .path => |path| {
+ try writer.print(
+ comptime Output.prettyFmt(" (<green>\"{s}\"<r>)<r>", enable_ansi_colors),
+ .{
+ path.slice(),
+ },
+ );
+ },
+ .fd => |fd| {
+ try writer.print(
+ comptime Output.prettyFmt(" (<r>fd: <yellow>{d}<r>)<r>", enable_ansi_colors),
+ .{
+ fd,
+ },
+ );
+ },
+ }
+ },
+ .bytes => {
+ try writeFormatForSize(this.size, writer, enable_ansi_colors);
+ },
+ }
+ }
+
+ if (this.content_type.len > 0 or this.offset > 0) {
+ try writer.writeAll(" {\n");
+ {
+ formatter.indent += 1;
+ defer formatter.indent -= 1;
+
+ if (this.content_type.len > 0) {
+ try formatter.writeIndent(Writer, writer);
+ try writer.print(
+ comptime Output.prettyFmt("type: <green>\"{s}\"<r>", enable_ansi_colors),
+ .{
+ this.content_type,
+ },
+ );
+
+ if (this.offset > 0) {
+ formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
+ }
+
+ try writer.writeAll("\n");
+ }
+
+ if (this.offset > 0) {
+ try formatter.writeIndent(Writer, writer);
+
+ try writer.print(
+ comptime Output.prettyFmt("offset: <yellow>{d}<r>\n", enable_ansi_colors),
+ .{
+ this.offset,
+ },
+ );
+ }
+ }
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("}");
+ }
+ }
+
+ const CopyFilePromiseHandler = struct {
+ promise: *JSPromise,
+ globalThis: *JSGlobalObject,
+ pub fn run(handler: *@This(), blob_: Store.CopyFile.ResultType) void {
+ var promise = handler.promise;
+ var globalThis = handler.globalThis;
+ bun.default_allocator.destroy(handler);
+ var blob = blob_ catch |err| {
+ var error_string = ZigString.init(
+ std.fmt.allocPrint(bun.default_allocator, "Failed to write file \"{s}\"", .{std.mem.span(@errorName(err))}) catch unreachable,
+ );
+ error_string.mark();
+
+ promise.reject(globalThis, error_string.toErrorInstance(globalThis));
+ return;
+ };
+ var _blob = bun.default_allocator.create(Blob) catch unreachable;
+ _blob.* = blob;
+ _blob.allocator = bun.default_allocator;
+ promise.resolve(
+ globalThis,
+ );
+ }
+ };
+
+ const WriteFileWaitFromLockedValueTask = struct {
+ file_blob: Blob,
+ globalThis: *JSGlobalObject,
+ promise: *JSPromise,
+
+ pub fn thenWrap(this: *anyopaque, value: *Body.Value) void {
+ then(bun.cast(*WriteFileWaitFromLockedValueTask, this), value);
+ }
+
+ pub fn then(this: *WriteFileWaitFromLockedValueTask, value: *Body.Value) void {
+ var promise = this.promise;
+ var globalThis = this.globalThis;
+ var file_blob = this.file_blob;
+ switch (value.*) {
+ .Error => |err| {
+ file_blob.detach();
+ _ = value.use();
+ bun.default_allocator.destroy(this);
+ promise.reject(globalThis, err);
+ },
+ .Used => {
+ file_blob.detach();
+ _ = value.use();
+ bun.default_allocator.destroy(this);
+ promise.reject(globalThis, ZigString.init("Body was used after it was consumed").toErrorInstance(globalThis));
+ },
+ // .InlineBlob,
+ .InternalBlob,
+ .Empty,
+ .Blob,
+ => {
+ var blob = value.use();
+ // TODO: this should be one promise not two!
+ const new_promise = writeFileWithSourceDestination(globalThis, &blob, &file_blob);
+ if (JSC.JSValue.fromRef(new_promise.?).asAnyPromise()) |_promise| {
+ switch (_promise.status(globalThis.vm())) {
+ .Pending => {
+ promise.resolve(
+ globalThis,
+ JSC.JSValue.fromRef(new_promise.?),
+ );
+ },
+ .Rejected => {
+ promise.reject(globalThis, _promise.result(globalThis.vm()));
+ },
+ else => {
+ promise.resolve(globalThis, _promise.result(globalThis.vm()));
+ },
+ }
+ }
+
+ file_blob.detach();
+ bun.default_allocator.destroy(this);
+ },
+ .Locked => {
+ value.Locked.onReceiveValue = thenWrap;
+ value.Locked.task = this;
+ },
+ }
+ }
+ };
+
+ pub fn writeFileWithSourceDestination(
+ ctx: JSC.C.JSContextRef,
+ source_blob: *Blob,
+ destination_blob: *Blob,
+ ) js.JSObjectRef {
+ const destination_type = std.meta.activeTag(destination_blob.store.?.data);
+
+ // Writing an empty string to a file is a no-op
+ if (source_blob.store == null) {
+ destination_blob.detach();
+ return JSC.JSPromise.resolvedPromiseValue(ctx.ptr(), JSC.JSValue.jsNumber(0)).asObjectRef();
+ }
+
+ const source_type = std.meta.activeTag(source_blob.store.?.data);
+
+ if (destination_type == .file and source_type == .bytes) {
+ var write_file_promise = bun.default_allocator.create(WriteFilePromise) catch unreachable;
+ var promise = JSC.JSPromise.create(ctx.ptr());
+ const promise_value = promise.asValue(ctx);
+ write_file_promise.* = .{
+ .globalThis = ctx.ptr(),
+ };
+ write_file_promise.promise.strong.set(ctx, promise_value);
+ promise_value.ensureStillAlive();
+
+ var file_copier = Store.WriteFile.create(
+ bun.default_allocator,
+ destination_blob.*,
+ source_blob.*,
+ *WriteFilePromise,
+ write_file_promise,
+ WriteFilePromise.run,
+ ) catch unreachable;
+ var task = Store.WriteFile.WriteFileTask.createOnJSThread(bun.default_allocator, ctx.ptr(), file_copier) catch unreachable;
+ task.schedule();
+ return promise_value.asObjectRef();
+ }
+ // If this is file <> file, we can just copy the file
+ else if (destination_type == .file and source_type == .file) {
+ var file_copier = Store.CopyFile.create(
+ bun.default_allocator,
+ destination_blob.store.?,
+ source_blob.store.?,
+
+ destination_blob.offset,
+ destination_blob.size,
+ ctx.ptr(),
+ ) catch unreachable;
+ file_copier.schedule();
+ return file_copier.promise.value().asObjectRef();
+ } else if (destination_type == .bytes and source_type == .bytes) {
+ // If this is bytes <> bytes, we can just duplicate it
+ // this is an edgecase
+ // it will happen if someone did Bun.write(new Blob([123]), new Blob([456]))
+ // eventually, this could be like Buffer.concat
+ var clone = source_blob.dupe();
+ clone.allocator = bun.default_allocator;
+ var cloned = bun.default_allocator.create(Blob) catch unreachable;
+ cloned.* = clone;
+ return JSPromise.resolvedPromiseValue(ctx.ptr(), cloned.toJS(ctx)).asObjectRef();
+ } else if (destination_type == .bytes and source_type == .file) {
+ var fake_call_frame: [8]JSC.JSValue = undefined;
+ @memset(@ptrCast([*]u8, &fake_call_frame), 0, @sizeOf(@TypeOf(fake_call_frame)));
+ const blob_value =
+ source_blob.getSlice(ctx, @ptrCast(*JSC.CallFrame, &fake_call_frame));
+
+ return JSPromise.resolvedPromiseValue(
+ ctx.ptr(),
+ blob_value,
+ ).asObjectRef();
+ }
+
+ unreachable;
+ }
+ pub fn writeFile(
+ _: void,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ arguments: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) js.JSObjectRef {
+ var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments);
+ defer args.deinit();
+ // accept a path or a blob
+ var path_or_blob = PathOrBlob.fromJSNoCopy(ctx, &args, exception) orelse {
+ exception.* = JSC.toInvalidArguments("Bun.write expects a path, file descriptor or a blob", .{}, ctx).asObjectRef();
+ return null;
+ };
+
+ var data = args.nextEat() orelse {
+ exception.* = JSC.toInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}, ctx).asObjectRef();
+ return null;
+ };
+
+ if (data.isEmptyOrUndefinedOrNull()) {
+ exception.* = JSC.toInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}, ctx).asObjectRef();
+ return null;
+ }
+
+ if (path_or_blob == .blob and path_or_blob.blob.store == null) {
+ exception.* = JSC.toInvalidArguments("Blob is detached", .{}, ctx).asObjectRef();
+ return null;
+ }
+
+ var needs_async = false;
+ if (data.isString()) {
+ const len = data.getLengthOfArray(ctx);
+
+ if (len < 256 * 1024 or bun.isMissingIOUring()) {
+ const str = data.getZigString(ctx);
+
+ const pathlike: JSC.Node.PathOrFileDescriptor = if (path_or_blob == .path)
+ path_or_blob.path
+ else
+ path_or_blob.blob.store.?.data.file.pathlike;
+
+ if (pathlike == .path) {
+ const result = writeStringToFileFast(
+ ctx,
+ pathlike,
+ str,
+ &needs_async,
+ true,
+ );
+ if (!needs_async) {
+ return result.asObjectRef();
+ }
+ } else {
+ const result = writeStringToFileFast(
+ ctx,
+ pathlike,
+ str,
+ &needs_async,
+ false,
+ );
+ if (!needs_async) {
+ return result.asObjectRef();
+ }
+ }
+ }
+ } else if (data.asArrayBuffer(ctx)) |buffer_view| {
+ if (buffer_view.byte_len < 256 * 1024 or bun.isMissingIOUring()) {
+ const pathlike: JSC.Node.PathOrFileDescriptor = if (path_or_blob == .path)
+ path_or_blob.path
+ else
+ path_or_blob.blob.store.?.data.file.pathlike;
+
+ if (pathlike == .path) {
+ const result = writeBytesToFileFast(
+ ctx,
+ pathlike,
+ buffer_view.byteSlice(),
+ &needs_async,
+ true,
+ );
+
+ if (!needs_async) {
+ return result.asObjectRef();
+ }
+ } else {
+ const result = writeBytesToFileFast(
+ ctx,
+ pathlike,
+ buffer_view.byteSlice(),
+ &needs_async,
+ false,
+ );
+
+ if (!needs_async) {
+ return result.asObjectRef();
+ }
+ }
+ }
+ }
+
+ // if path_or_blob is a path, convert it into a file blob
+ var destination_blob: Blob = if (path_or_blob == .path)
+ Blob.findOrCreateFileFromPath(path_or_blob.path, ctx.ptr())
+ else
+ path_or_blob.blob.dupe();
+
+ if (destination_blob.store == null) {
+ exception.* = JSC.toInvalidArguments("Writing to an empty blob is not implemented yet", .{}, ctx).asObjectRef();
+ return null;
+ }
+
+ // TODO: implement a writeev() fast path
+ var source_blob: Blob = brk: {
+ if (data.as(Response)) |response| {
+ switch (response.body.value) {
+ // .InlineBlob,
+ .InternalBlob,
+ .Used,
+ .Empty,
+ .Blob,
+ => {
+ break :brk response.body.use();
+ },
+ .Error => {
+ destination_blob.detach();
+ const err = response.body.value.Error;
+ JSC.C.JSValueUnprotect(ctx, err.asObjectRef());
+ _ = response.body.value.use();
+ return JSC.JSPromise.rejectedPromiseValue(ctx.ptr(), err).asObjectRef();
+ },
+ .Locked => {
+ var task = bun.default_allocator.create(WriteFileWaitFromLockedValueTask) catch unreachable;
+ var promise = JSC.JSPromise.create(ctx.ptr());
+ task.* = WriteFileWaitFromLockedValueTask{
+ .globalThis = ctx.ptr(),
+ .file_blob = destination_blob,
+ .promise = promise,
+ };
+
+ response.body.value.Locked.task = task;
+ response.body.value.Locked.onReceiveValue = WriteFileWaitFromLockedValueTask.thenWrap;
+
+ return promise.asValue(ctx.ptr()).asObjectRef();
+ },
+ }
+ }
+
+ if (data.as(Request)) |request| {
+ switch (request.body) {
+ // .InlineBlob,
+ .InternalBlob,
+ .Used,
+ .Empty,
+ .Blob,
+ => {
+ break :brk request.body.use();
+ },
+ .Error => {
+ destination_blob.detach();
+ const err = request.body.Error;
+ JSC.C.JSValueUnprotect(ctx, err.asObjectRef());
+ _ = request.body.use();
+ return JSC.JSPromise.rejectedPromiseValue(ctx.ptr(), err).asObjectRef();
+ },
+ .Locked => {
+ var task = bun.default_allocator.create(WriteFileWaitFromLockedValueTask) catch unreachable;
+ var promise = JSC.JSPromise.create(ctx.ptr());
+ task.* = WriteFileWaitFromLockedValueTask{
+ .globalThis = ctx.ptr(),
+ .file_blob = destination_blob,
+ .promise = promise,
+ };
+
+ request.body.Locked.task = task;
+ request.body.Locked.onReceiveValue = WriteFileWaitFromLockedValueTask.thenWrap;
+
+ return promise.asValue(ctx.ptr()).asObjectRef();
+ },
+ }
+ }
+
+ break :brk Blob.get(
+ ctx.ptr(),
+ data,
+ false,
+ false,
+ ) catch |err| {
+ if (err == error.InvalidArguments) {
+ exception.* = JSC.toInvalidArguments(
+ "Expected an Array",
+ .{},
+ ctx,
+ ).asObjectRef();
+ return null;
+ }
+
+ exception.* = JSC.toInvalidArguments(
+ "Out of memory",
+ .{},
+ ctx,
+ ).asObjectRef();
+ return null;
+ };
+ };
+
+ return writeFileWithSourceDestination(ctx, &source_blob, &destination_blob);
+ }
+
+ const write_permissions = 0o664;
+
+ fn writeStringToFileFast(
+ globalThis: *JSC.JSGlobalObject,
+ pathlike: JSC.Node.PathOrFileDescriptor,
+ str: ZigString,
+ needs_async: *bool,
+ comptime needs_open: bool,
+ ) JSC.JSValue {
+ const fd: bun.FileDescriptor = if (comptime !needs_open) pathlike.fd else brk: {
+ var file_path: [bun.MAX_PATH_BYTES]u8 = undefined;
+ switch (JSC.Node.Syscall.open(
+ pathlike.path.sliceZ(&file_path),
+ // we deliberately don't use O_TRUNC here
+ // it's a perf optimization
+ std.os.O.WRONLY | std.os.O.CREAT | std.os.O.NONBLOCK,
+ write_permissions,
+ )) {
+ .result => |result| {
+ break :brk result;
+ },
+ .err => |err| {
+ return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
+ },
+ }
+ unreachable;
+ };
+
+ var truncate = needs_open or str.len == 0;
+ var jsc_vm = globalThis.bunVM();
+ var written: usize = 0;
+
+ defer {
+ // we only truncate if it's a path
+ // if it's a file descriptor, we assume they want manual control over that behavior
+ if (truncate) {
+ _ = JSC.Node.Syscall.system.ftruncate(fd, @intCast(i64, written));
+ }
+
+ if (needs_open) {
+ _ = JSC.Node.Syscall.close(fd);
+ }
+ }
+ if (str.len == 0) {} else if (str.is16Bit()) {
+ var decoded = str.toSlice(jsc_vm.allocator);
+ defer decoded.deinit();
+
+ var remain = decoded.slice();
+ const end = remain.ptr + remain.len;
+
+ while (remain.ptr != end) {
+ const result = JSC.Node.Syscall.write(fd, remain);
+ switch (result) {
+ .result => |res| {
+ written += res;
+ remain = remain[res..];
+ if (res == 0) break;
+ },
+ .err => |err| {
+ truncate = false;
+ if (err.getErrno() == .AGAIN) {
+ needs_async.* = true;
+ return .zero;
+ }
+ return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
+ },
+ }
+ }
+ } else if (str.isUTF8() or strings.isAllASCII(str.slice())) {
+ var remain = str.slice();
+ const end = remain.ptr + remain.len;
+
+ while (remain.ptr != end) {
+ const result = JSC.Node.Syscall.write(fd, remain);
+ switch (result) {
+ .result => |res| {
+ written += res;
+ remain = remain[res..];
+ if (res == 0) break;
+ },
+ .err => |err| {
+ truncate = false;
+ if (err.getErrno() == .AGAIN) {
+ needs_async.* = true;
+ return .zero;
+ }
+
+ return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
+ },
+ }
+ }
+ } else {
+ var decoded = str.toOwnedSlice(jsc_vm.allocator) catch {
+ return JSC.JSPromise.rejectedPromiseValue(globalThis, ZigString.static("Out of memory").toErrorInstance(globalThis));
+ };
+ defer jsc_vm.allocator.free(decoded);
+ var remain = decoded;
+ const end = remain.ptr + remain.len;
+ while (remain.ptr != end) {
+ const result = JSC.Node.Syscall.write(fd, remain);
+ switch (result) {
+ .result => |res| {
+ written += res;
+ remain = remain[res..];
+ if (res == 0) break;
+ },
+ .err => |err| {
+ truncate = false;
+ if (err.getErrno() == .AGAIN) {
+ needs_async.* = true;
+ return .zero;
+ }
+
+ return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
+ },
+ }
+ }
+ }
+
+ return JSC.JSPromise.resolvedPromiseValue(globalThis, JSC.JSValue.jsNumber(written));
+ }
+
+ fn writeBytesToFileFast(
+ globalThis: *JSC.JSGlobalObject,
+ pathlike: JSC.Node.PathOrFileDescriptor,
+ bytes: []const u8,
+ needs_async: *bool,
+ comptime needs_open: bool,
+ ) JSC.JSValue {
+ const fd: bun.FileDescriptor = if (comptime !needs_open) pathlike.fd else brk: {
+ var file_path: [bun.MAX_PATH_BYTES]u8 = undefined;
+ switch (JSC.Node.Syscall.open(
+ pathlike.path.sliceZ(&file_path),
+ // we deliberately don't use O_TRUNC here
+ // it's a perf optimization
+ std.os.O.WRONLY | std.os.O.CREAT | std.os.O.NONBLOCK,
+ write_permissions,
+ )) {
+ .result => |result| {
+ break :brk result;
+ },
+ .err => |err| {
+ return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
+ },
+ }
+ unreachable;
+ };
+
+ var truncate = needs_open or bytes.len == 0;
+ var written: usize = 0;
+ defer {
+ if (truncate) {
+ _ = JSC.Node.Syscall.system.ftruncate(fd, @intCast(i64, written));
+ }
+
+ if (needs_open) {
+ _ = JSC.Node.Syscall.close(fd);
+ }
+ }
+
+ var remain = bytes;
+ const end = remain.ptr + remain.len;
+
+ while (remain.ptr != end) {
+ const result = JSC.Node.Syscall.write(fd, remain);
+ switch (result) {
+ .result => |res| {
+ written += res;
+ remain = remain[res..];
+ if (res == 0) break;
+ },
+ .err => |err| {
+ truncate = false;
+ if (err.getErrno() == .AGAIN) {
+ needs_async.* = true;
+ return .zero;
+ }
+ return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
+ },
+ }
+ }
+
+ return JSC.JSPromise.resolvedPromiseValue(globalThis, JSC.JSValue.jsNumber(written));
+ }
+
+ pub fn constructFile(
+ _: void,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ arguments: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) js.JSObjectRef {
+ var vm = ctx.bunVM();
+ var args = JSC.Node.ArgumentsSlice.from(vm, arguments);
+ defer args.deinit();
+
+ const path = JSC.Node.PathOrFileDescriptor.fromJS(ctx, &args, args.arena.allocator(), exception) orelse {
+ exception.* = JSC.toInvalidArguments("Expected file path string or file descriptor", .{}, ctx).asObjectRef();
+ return js.JSValueMakeUndefined(ctx);
+ };
+
+ const blob = Blob.findOrCreateFileFromPath(path, ctx.ptr());
+
+ var ptr = vm.allocator.create(Blob) catch unreachable;
+ ptr.* = blob;
+ ptr.allocator = vm.allocator;
+ return ptr.toJS(ctx).asObjectRef();
+ }
+
+ pub fn findOrCreateFileFromPath(path_: JSC.Node.PathOrFileDescriptor, globalThis: *JSGlobalObject) Blob {
+ var vm = globalThis.bunVM();
+ const allocator = vm.allocator;
+
+ const path: JSC.Node.PathOrFileDescriptor = brk: {
+ switch (path_) {
+ .path => {
+ const slice = path_.path.slice();
+ var cloned = (allocator.dupeZ(u8, slice) catch unreachable)[0..slice.len];
+
+ break :brk .{
+ .path = .{
+ .string = bun.PathString.init(cloned),
+ },
+ };
+ },
+ .fd => {
+ switch (path_.fd) {
+ std.os.STDIN_FILENO => return Blob.initWithStore(
+ vm.rareData().stdin(),
+ globalThis,
+ ),
+ std.os.STDERR_FILENO => return Blob.initWithStore(
+ vm.rareData().stderr(),
+ globalThis,
+ ),
+ std.os.STDOUT_FILENO => return Blob.initWithStore(
+ vm.rareData().stdout(),
+ globalThis,
+ ),
+ else => {},
+ }
+ break :brk path_;
+ },
+ }
+ };
+
+ return Blob.initWithStore(Blob.Store.initFile(path, null, allocator) catch unreachable, globalThis);
+ }
+
+ pub const Store = struct {
+ data: Data,
+
+ mime_type: MimeType = MimeType.other,
+ ref_count: u32 = 0,
+ is_all_ascii: ?bool = null,
+ allocator: std.mem.Allocator,
+
+ pub fn size(this: *const Store) SizeType {
+ return switch (this.data) {
+ .bytes => this.data.bytes.len,
+ .file => Blob.max_size,
+ };
+ }
+
+ pub const Map = std.HashMap(u64, *JSC.WebCore.Blob.Store, IdentityContext(u64), 80);
+
+ pub const Data = union(enum) {
+ bytes: ByteStore,
+ file: FileStore,
+ };
+
+ pub fn ref(this: *Store) void {
+ std.debug.assert(this.ref_count > 0);
+ this.ref_count += 1;
+ }
+
+ pub fn external(ptr: ?*anyopaque, _: ?*anyopaque, _: usize) callconv(.C) void {
+ if (ptr == null) return;
+ var this = bun.cast(*Store, ptr);
+ this.deref();
+ }
+
+ pub fn initFile(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?HTTPClient.MimeType, allocator: std.mem.Allocator) !*Store {
+ var store = try allocator.create(Blob.Store);
+ store.* = .{
+ .data = .{
+ .file = FileStore.init(
+ pathlike,
+ mime_type orelse brk: {
+ if (pathlike == .path) {
+ const sliced = pathlike.path.slice();
+ if (sliced.len > 0) {
+ var extname = std.fs.path.extension(sliced);
+ extname = std.mem.trim(u8, extname, ".");
+ if (HTTPClient.MimeType.byExtensionNoDefault(extname)) |mime| {
+ break :brk mime;
+ }
+ }
+ }
+
+ break :brk null;
+ },
+ ),
+ },
+ .allocator = allocator,
+ .ref_count = 1,
+ };
+ return store;
+ }
+
+ pub fn init(bytes: []u8, allocator: std.mem.Allocator) !*Store {
+ var store = try allocator.create(Blob.Store);
+ store.* = .{
+ .data = .{ .bytes = ByteStore.init(bytes, allocator) },
+ .allocator = allocator,
+ .ref_count = 1,
+ };
+ return store;
+ }
+
+ pub fn sharedView(this: Store) []u8 {
+ if (this.data == .bytes)
+ return this.data.bytes.slice();
+
+ return &[_]u8{};
+ }
+
+ pub fn deref(this: *Blob.Store) void {
+ std.debug.assert(this.ref_count >= 1);
+ this.ref_count -= 1;
+ if (this.ref_count == 0) {
+ this.deinit();
+ }
+ }
+
+ pub fn deinit(this: *Blob.Store) void {
+ const allocator = this.allocator;
+
+ switch (this.data) {
+ .bytes => |*bytes| {
+ bytes.deinit();
+ },
+ .file => |file| {
+ if (file.pathlike == .path) {
+ allocator.free(bun.constStrToU8(file.pathlike.path.slice()));
+ }
+ },
+ }
+
+ allocator.destroy(this);
+ }
+
+ pub fn fromArrayList(list: std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !*Blob.Store {
+ return try Blob.Store.init(list.items, allocator);
+ }
+
+ pub fn FileOpenerMixin(comptime This: type) type {
+ return struct {
+ open_completion: AsyncIO.Completion = undefined,
+ context: *This,
+
+ const State = @This();
+
+ const __opener_flags = std.os.O.NONBLOCK | std.os.O.CLOEXEC;
+ const open_flags_ = if (@hasDecl(This, "open_flags"))
+ This.open_flags | __opener_flags
+ else
+ std.os.O.RDONLY | __opener_flags;
+
+ pub fn getFdMac(this: *This) bun.FileDescriptor {
+ var buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var path_string = if (@hasField(This, "file_store"))
+ this.file_store.pathlike.path
+ else
+ this.file_blob.store.?.data.file.pathlike.path;
+
+ var path = path_string.sliceZ(&buf);
+
+ this.opened_fd = switch (JSC.Node.Syscall.open(path, open_flags_, JSC.Node.default_permission)) {
+ .result => |fd| fd,
+ .err => |err| {
+ this.errno = AsyncIO.asError(err.errno);
+ this.system_error = err.withPath(path_string.slice()).toSystemError();
+ this.opened_fd = null_fd;
+ return null_fd;
+ },
+ };
+
+ return this.opened_fd;
+ }
+
+ pub const OpenCallback = *const fn (*This, bun.FileDescriptor) void;
+
+ pub fn getFd(this: *This, comptime Callback: OpenCallback) void {
+ if (this.opened_fd != null_fd) {
+ Callback(this, this.opened_fd);
+ return;
+ }
+
+ if (comptime Environment.isMac) {
+ Callback(this, this.getFdMac());
+ } else {
+ this.getFdLinux(Callback);
+ }
+ }
+
+ const WrappedOpenCallback = *const fn (*State, *HTTPClient.NetworkThread.Completion, AsyncIO.OpenError!bun.FileDescriptor) void;
+ fn OpenCallbackWrapper(comptime Callback: OpenCallback) WrappedOpenCallback {
+ return struct {
+ const callback = Callback;
+ const StateHolder = State;
+ pub fn onOpen(state: *State, completion: *HTTPClient.NetworkThread.Completion, result: AsyncIO.OpenError!bun.FileDescriptor) void {
+ var this = state.context;
+ var path_buffer = completion.operation.open.path;
+ defer bun.default_allocator.free(bun.span(path_buffer));
+ defer bun.default_allocator.destroy(state);
+ this.opened_fd = result catch {
+ this.errno = AsyncIO.asError(-completion.result);
+ // do not use path_buffer here because it is a temporary
+ var path_string = if (@hasField(This, "file_store"))
+ this.file_store.pathlike.path
+ else
+ this.file_blob.store.?.data.file.pathlike.path;
+
+ this.system_error = .{
+ .syscall = ZigString.init("open"),
+ .code = ZigString.init(std.mem.span(@errorName(this.errno.?))),
+ .path = ZigString.init(path_string.slice()),
+ };
+
+ // assert we never end up reusing the memory
+ std.debug.assert(@ptrToInt(this.system_error.?.path.slice().ptr) != @ptrToInt(path_buffer));
+
+ callback(this, null_fd);
+ return;
+ };
+
+ callback(this, this.opened_fd);
+ }
+ }.onOpen;
+ }
+
+ pub fn getFdLinux(this: *This, comptime callback: OpenCallback) void {
+ var aio = &AsyncIO.global;
+
+ var path_string = if (@hasField(This, "file_store"))
+ this.file_store.pathlike.path
+ else
+ this.file_blob.store.?.data.file.pathlike.path;
+
+ var holder = bun.default_allocator.create(State) catch unreachable;
+ holder.* = .{
+ .context = this,
+ };
+ var path_buffer = bun.default_allocator.dupeZ(u8, path_string.slice()) catch unreachable;
+ aio.open(
+ *State,
+ holder,
+ comptime OpenCallbackWrapper(callback),
+ &holder.open_completion,
+ path_buffer,
+ open_flags_,
+ JSC.Node.default_permission,
+ );
+ }
+ };
+ }
+
+ pub fn FileCloserMixin(comptime This: type) type {
+ return struct {
+ const Closer = @This();
+ close_completion: AsyncIO.Completion = undefined,
+
+ pub fn doClose(this: *This) void {
+ const fd = this.opened_fd;
+ std.debug.assert(fd != null_fd);
+ var aio = &AsyncIO.global;
+
+ var closer = bun.default_allocator.create(Closer) catch unreachable;
+
+ aio.close(
+ *Closer,
+ closer,
+ onClose,
+ &closer.close_completion,
+ fd,
+ );
+ this.opened_fd = null_fd;
+ }
+
+ pub fn onClose(closer: *Closer, _: *HTTPClient.NetworkThread.Completion, _: AsyncIO.CloseError!void) void {
+ bun.default_allocator.destroy(closer);
+ }
+ };
+ }
+
+ pub const ReadFile = struct {
+ file_store: FileStore,
+ byte_store: ByteStore = ByteStore{ .allocator = bun.default_allocator },
+ store: ?*Store = null,
+ offset: SizeType = 0,
+ max_length: SizeType = Blob.max_size,
+ opened_fd: bun.FileDescriptor = null_fd,
+ read_completion: HTTPClient.NetworkThread.Completion = undefined,
+ read_len: SizeType = 0,
+ read_off: SizeType = 0,
+ size: SizeType = 0,
+ buffer: []u8 = undefined,
+ task: HTTPClient.NetworkThread.Task = undefined,
+ system_error: ?JSC.SystemError = null,
+ errno: ?anyerror = null,
+ onCompleteCtx: *anyopaque = undefined,
+ onCompleteCallback: OnReadFileCallback = undefined,
+ io_task: ?*ReadFileTask = null,
+
+ pub const Read = struct {
+ buf: []u8,
+ is_temporary: bool = false,
+ total_size: SizeType = 0,
+ };
+ pub const ResultType = SystemError.Maybe(Read);
+
+ pub const OnReadFileCallback = *const fn (ctx: *anyopaque, bytes: ResultType) void;
+
+ pub usingnamespace FileOpenerMixin(ReadFile);
+ pub usingnamespace FileCloserMixin(ReadFile);
+
+ pub fn createWithCtx(
+ allocator: std.mem.Allocator,
+ store: *Store,
+ onReadFileContext: *anyopaque,
+ onCompleteCallback: OnReadFileCallback,
+ off: SizeType,
+ max_len: SizeType,
+ ) !*ReadFile {
+ var read_file = try allocator.create(ReadFile);
+ read_file.* = ReadFile{
+ .file_store = store.data.file,
+ .offset = off,
+ .max_length = max_len,
+ .store = store,
+ .onCompleteCtx = onReadFileContext,
+ .onCompleteCallback = onCompleteCallback,
+ };
+ store.ref();
+ return read_file;
+ }
+
+ pub fn create(
+ allocator: std.mem.Allocator,
+ store: *Store,
+ off: SizeType,
+ max_len: SizeType,
+ comptime Context: type,
+ context: Context,
+ comptime callback: fn (ctx: Context, bytes: ResultType) void,
+ ) !*ReadFile {
+ const Handler = struct {
+ pub fn run(ptr: *anyopaque, bytes: ResultType) void {
+ callback(bun.cast(Context, ptr), bytes);
+ }
+ };
+
+ return try ReadFile.createWithCtx(allocator, store, @ptrCast(*anyopaque, context), Handler.run, off, max_len);
+ }
+
+ pub fn doRead(this: *ReadFile) void {
+ 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..@min(remaining.len, this.max_length - this.read_off)],
+ this.offset + this.read_off,
+ );
+ }
+
+ pub const ReadFileTask = JSC.IOTask(@This());
+
+ pub fn then(this: *ReadFile, _: *JSC.JSGlobalObject) void {
+ var cb = this.onCompleteCallback;
+ var cb_ctx = this.onCompleteCtx;
+
+ if (this.store == null and this.system_error != null) {
+ var system_error = this.system_error.?;
+ bun.default_allocator.destroy(this);
+ cb(cb_ctx, ResultType{ .err = system_error });
+ return;
+ } else if (this.store == null) {
+ bun.default_allocator.destroy(this);
+ cb(cb_ctx, ResultType{ .err = SystemError{
+ .code = ZigString.init("INTERNAL_ERROR"),
+ .path = ZigString.Empty,
+ .message = ZigString.init("assertion failure - store should not be null"),
+ .syscall = ZigString.init("read"),
+ } });
+ return;
+ }
+
+ var store = this.store.?;
+ var buf = this.buffer;
+
+ defer store.deref();
+ defer bun.default_allocator.destroy(this);
+ if (this.system_error) |err| {
+ cb(cb_ctx, ResultType{ .err = err });
+ return;
+ }
+
+ cb(cb_ctx, .{ .result = .{ .buf = buf, .total_size = this.size, .is_temporary = true } });
+ }
+ pub fn run(this: *ReadFile, task: *ReadFileTask) void {
+ this.runAsync(task);
+ }
+
+ pub fn onRead(this: *ReadFile, completion: *HTTPClient.NetworkThread.Completion, result: AsyncIO.ReadError!usize) void {
+ defer this.doReadLoop();
+
+ this.read_len = @truncate(SizeType, result catch |err| {
+ if (@hasField(HTTPClient.NetworkThread.Completion, "result")) {
+ this.errno = AsyncIO.asError(-completion.result);
+ this.system_error = (JSC.Node.Syscall.Error{
+ .errno = @intCast(JSC.Node.Syscall.Error.Int, -completion.result),
+ .syscall = .read,
+ }).toSystemError();
+ } else {
+ this.system_error = JSC.SystemError{
+ .code = ZigString.init(std.mem.span(@errorName(err))),
+ .path = if (this.file_store.pathlike == .path)
+ ZigString.init(this.file_store.pathlike.path.slice())
+ else
+ ZigString.Empty,
+ .syscall = ZigString.init("read"),
+ };
+
+ this.errno = err;
+ }
+
+ this.read_len = 0;
+ return;
+ });
+ }
+
+ fn runAsync(this: *ReadFile, task: *ReadFileTask) void {
+ this.io_task = task;
+
+ if (this.file_store.pathlike == .fd) {
+ this.opened_fd = this.file_store.pathlike.fd;
+ }
+
+ this.getFd(runAsyncWithFD);
+ }
+
+ fn onFinish(this: *ReadFile) void {
+ const fd = this.opened_fd;
+ const file = &this.file_store;
+ const needs_close = fd != null_fd and file.pathlike == .path and fd > 2;
+
+ this.size = @max(this.read_len, this.size);
+
+ if (needs_close) {
+ this.doClose();
+ }
+
+ var io_task = this.io_task.?;
+ this.io_task = null;
+ io_task.onFinish();
+ }
+
+ fn resolveSize(this: *ReadFile, fd: bun.FileDescriptor) void {
+ const stat: std.os.Stat = switch (JSC.Node.Syscall.fstat(fd)) {
+ .result => |result| result,
+ .err => |err| {
+ this.errno = AsyncIO.asError(err.errno);
+ this.system_error = err.toSystemError();
+ return;
+ },
+ };
+ if (std.os.S.ISDIR(stat.mode)) {
+ this.errno = error.EISDIR;
+ this.system_error = JSC.SystemError{
+ .code = ZigString.init("EISDIR"),
+ .path = if (this.file_store.pathlike == .path)
+ ZigString.init(this.file_store.pathlike.path.slice())
+ else
+ ZigString.Empty,
+ .message = ZigString.init("Directories cannot be read like files"),
+ .syscall = ZigString.init("read"),
+ };
+ return;
+ }
+
+ if (stat.size > 0 and std.os.S.ISREG(stat.mode)) {
+ this.size = @min(
+ @truncate(SizeType, @intCast(SizeType, @max(@intCast(i64, stat.size), 0))),
+ this.max_length,
+ );
+ // read up to 4k at a time if
+ // they didn't explicitly set a size and we're reading from something that's not a regular file
+ } else if (stat.size == 0 and !std.os.S.ISREG(stat.mode)) {
+ this.size = if (this.max_length == Blob.max_size)
+ 4096
+ else
+ this.max_length;
+ }
+ }
+
+ fn runAsyncWithFD(this: *ReadFile, fd: bun.FileDescriptor) void {
+ if (this.errno != null) {
+ this.onFinish();
+ return;
+ }
+
+ this.resolveSize(fd);
+ if (this.errno != null)
+ return this.onFinish();
+
+ if (this.size == 0) {
+ this.buffer = &[_]u8{};
+ this.byte_store = ByteStore.init(this.buffer, bun.default_allocator);
+
+ this.onFinish();
+ }
+
+ this.buffer = bun.default_allocator.alloc(u8, this.size) catch |err| {
+ this.errno = err;
+ this.onFinish();
+ return;
+ };
+ this.read_len = 0;
+ this.doReadLoop();
+ }
+
+ fn doReadLoop(this: *ReadFile) void {
+ this.read_off += this.read_len;
+ var remain = this.buffer[@min(this.read_off, @truncate(Blob.SizeType, this.buffer.len))..];
+
+ if (remain.len > 0 and this.errno == null) {
+ this.doRead();
+ return;
+ }
+
+ _ = bun.default_allocator.resize(this.buffer, this.read_off);
+ this.buffer = this.buffer[0..this.read_off];
+ this.byte_store = ByteStore.init(this.buffer, bun.default_allocator);
+ this.onFinish();
+ }
+ };
+
+ pub const WriteFile = struct {
+ file_blob: Blob,
+ bytes_blob: Blob,
+
+ opened_fd: bun.FileDescriptor = null_fd,
+ system_error: ?JSC.SystemError = null,
+ errno: ?anyerror = null,
+ write_completion: HTTPClient.NetworkThread.Completion = undefined,
+ task: HTTPClient.NetworkThread.Task = undefined,
+ io_task: ?*WriteFileTask = null,
+
+ onCompleteCtx: *anyopaque = undefined,
+ onCompleteCallback: OnWriteFileCallback = undefined,
+ wrote: usize = 0,
+
+ pub const ResultType = SystemError.Maybe(SizeType);
+ pub const OnWriteFileCallback = *const fn (ctx: *anyopaque, count: ResultType) void;
+
+ pub usingnamespace FileOpenerMixin(WriteFile);
+ pub usingnamespace FileCloserMixin(WriteFile);
+
+ // Do not open with APPEND because we may use pwrite()
+ pub const open_flags = std.os.O.WRONLY | std.os.O.CREAT | std.os.O.TRUNC;
+
+ pub fn createWithCtx(
+ allocator: std.mem.Allocator,
+ file_blob: Blob,
+ bytes_blob: Blob,
+ onWriteFileContext: *anyopaque,
+ onCompleteCallback: OnWriteFileCallback,
+ ) !*WriteFile {
+ var read_file = try allocator.create(WriteFile);
+ read_file.* = WriteFile{
+ .file_blob = file_blob,
+ .bytes_blob = bytes_blob,
+ .onCompleteCtx = onWriteFileContext,
+ .onCompleteCallback = onCompleteCallback,
+ };
+ file_blob.store.?.ref();
+ bytes_blob.store.?.ref();
+ return read_file;
+ }
+
+ pub fn create(
+ allocator: std.mem.Allocator,
+ file_blob: Blob,
+ bytes_blob: Blob,
+ comptime Context: type,
+ context: Context,
+ comptime callback: fn (ctx: Context, bytes: ResultType) void,
+ ) !*WriteFile {
+ const Handler = struct {
+ pub fn run(ptr: *anyopaque, bytes: ResultType) void {
+ callback(bun.cast(Context, ptr), bytes);
+ }
+ };
+
+ return try WriteFile.createWithCtx(
+ allocator,
+ file_blob,
+ bytes_blob,
+ @ptrCast(*anyopaque, context),
+ Handler.run,
+ );
+ }
+
+ pub fn doWrite(
+ this: *WriteFile,
+ buffer: []const u8,
+ file_offset: u64,
+ ) void {
+ var aio = &AsyncIO.global;
+ this.wrote = 0;
+ const fd = this.opened_fd;
+ std.debug.assert(fd != null_fd);
+ aio.write(
+ *WriteFile,
+ this,
+ onWrite,
+ &this.write_completion,
+ fd,
+ buffer,
+ if (fd > 2) file_offset else 0,
+ );
+ }
+
+ pub const WriteFileTask = JSC.IOTask(@This());
+
+ pub fn then(this: *WriteFile, _: *JSC.JSGlobalObject) void {
+ var cb = this.onCompleteCallback;
+ var cb_ctx = this.onCompleteCtx;
+
+ this.bytes_blob.store.?.deref();
+ this.file_blob.store.?.deref();
+
+ if (this.system_error) |err| {
+ bun.default_allocator.destroy(this);
+ cb(cb_ctx, .{
+ .err = err,
+ });
+ return;
+ }
+
+ const wrote = this.wrote;
+ bun.default_allocator.destroy(this);
+ cb(cb_ctx, .{ .result = @truncate(SizeType, wrote) });
+ }
+ pub fn run(this: *WriteFile, task: *WriteFileTask) void {
+ this.io_task = task;
+ this.runAsync();
+ }
+
+ pub fn onWrite(this: *WriteFile, _: *HTTPClient.NetworkThread.Completion, result: AsyncIO.WriteError!usize) void {
+ defer this.doWriteLoop();
+ this.wrote += @truncate(SizeType, result catch |errno| {
+ this.errno = errno;
+ this.system_error = this.system_error orelse JSC.SystemError{
+ .code = ZigString.init(std.mem.span(@errorName(errno))),
+ .syscall = ZigString.init("write"),
+ };
+
+ this.wrote = 0;
+ return;
+ });
+ }
+
+ fn runAsync(this: *WriteFile) void {
+ this.getFd(runWithFD);
+ }
+
+ fn onFinish(this: *WriteFile) void {
+ const fd = this.opened_fd;
+ const file = this.file_blob.store.?.data.file;
+ const needs_close = fd != null_fd and file.pathlike == .path and fd > 2;
+
+ if (needs_close) {
+ this.doClose();
+ }
+
+ var io_task = this.io_task.?;
+ this.io_task = null;
+ io_task.onFinish();
+ }
+
+ fn runWithFD(this: *WriteFile, fd: bun.FileDescriptor) void {
+ if (fd == null_fd or this.errno != null) {
+ this.onFinish();
+ return;
+ }
+
+ this.doWriteLoop();
+ }
+
+ fn doWriteLoop(this: *WriteFile) void {
+ var remain = this.bytes_blob.sharedView();
+ var file_offset = this.file_blob.offset;
+
+ const this_tick = file_offset + this.wrote;
+ remain = remain[@min(this.wrote, remain.len)..];
+
+ if (remain.len > 0 and this.errno == null) {
+ this.doWrite(remain, this_tick);
+ } else {
+ this.onFinish();
+ }
+ }
+ };
+
+ pub const IOWhich = enum {
+ source,
+ destination,
+ both,
+ };
+
+ const unsupported_directory_error = SystemError{
+ .errno = @intCast(c_int, @enumToInt(bun.C.SystemErrno.EISDIR)),
+ .message = ZigString.init("That doesn't work on folders"),
+ .syscall = ZigString.init("fstat"),
+ };
+ const unsupported_non_regular_file_error = SystemError{
+ .errno = @intCast(c_int, @enumToInt(bun.C.SystemErrno.ENOTSUP)),
+ .message = ZigString.init("Non-regular files aren't supported yet"),
+ .syscall = ZigString.init("fstat"),
+ };
+
+ // blocking, but off the main thread
+ pub const CopyFile = struct {
+ destination_file_store: FileStore,
+ source_file_store: FileStore,
+ store: ?*Store = null,
+ source_store: ?*Store = null,
+ offset: SizeType = 0,
+ size: SizeType = 0,
+ max_length: SizeType = Blob.max_size,
+ destination_fd: bun.FileDescriptor = null_fd,
+ source_fd: bun.FileDescriptor = null_fd,
+
+ system_error: ?SystemError = null,
+
+ read_len: SizeType = 0,
+ read_off: SizeType = 0,
+
+ globalThis: *JSGlobalObject,
+
+ pub const ResultType = anyerror!SizeType;
+
+ pub const Callback = *const fn (ctx: *anyopaque, len: ResultType) void;
+ pub const CopyFilePromiseTask = JSC.ConcurrentPromiseTask(CopyFile);
+ pub const CopyFilePromiseTaskEventLoopTask = CopyFilePromiseTask.EventLoopTask;
+
+ pub fn create(
+ allocator: std.mem.Allocator,
+ store: *Store,
+ source_store: *Store,
+ off: SizeType,
+ max_len: SizeType,
+ globalThis: *JSC.JSGlobalObject,
+ ) !*CopyFilePromiseTask {
+ var read_file = try allocator.create(CopyFile);
+ read_file.* = CopyFile{
+ .store = store,
+ .source_store = source_store,
+ .offset = off,
+ .max_length = max_len,
+ .globalThis = globalThis,
+ .destination_file_store = store.data.file,
+ .source_file_store = source_store.data.file,
+ };
+ store.ref();
+ source_store.ref();
+ return try CopyFilePromiseTask.createOnJSThread(allocator, globalThis, read_file);
+ }
+
+ const linux = std.os.linux;
+ const darwin = std.os.darwin;
+
+ pub fn deinit(this: *CopyFile) void {
+ if (this.source_file_store.pathlike == .path) {
+ if (this.source_file_store.pathlike.path == .string and this.system_error == null) {
+ bun.default_allocator.free(bun.constStrToU8(this.source_file_store.pathlike.path.slice()));
+ }
+ }
+ this.store.?.deref();
+
+ bun.default_allocator.destroy(this);
+ }
+
+ pub fn reject(this: *CopyFile, promise: *JSC.JSPromise) void {
+ var globalThis = this.globalThis;
+ var system_error: SystemError = this.system_error orelse SystemError{};
+ if (this.source_file_store.pathlike == .path and system_error.path.len == 0) {
+ system_error.path = ZigString.init(this.source_file_store.pathlike.path.slice());
+ system_error.path.mark();
+ }
+
+ if (system_error.message.len == 0) {
+ system_error.message = ZigString.init("Failed to copy file");
+ }
+
+ var instance = system_error.toErrorInstance(this.globalThis);
+ if (this.store) |store| {
+ store.deref();
+ }
+ promise.reject(globalThis, instance);
+ }
+
+ pub fn then(this: *CopyFile, promise: *JSC.JSPromise) void {
+ this.source_store.?.deref();
+
+ if (this.system_error != null) {
+ this.reject(promise);
+ return;
+ }
+
+ promise.resolve(this.globalThis, JSC.JSValue.jsNumberFromUint64(this.read_len));
+ }
+
+ pub fn run(this: *CopyFile) void {
+ this.runAsync();
+ }
+
+ pub fn doClose(this: *CopyFile) void {
+ const close_input = this.destination_file_store.pathlike != .fd and this.destination_fd != null_fd;
+ const close_output = this.source_file_store.pathlike != .fd and this.source_fd != null_fd;
+
+ if (close_input and close_output) {
+ this.doCloseFile(.both);
+ } else if (close_input) {
+ this.doCloseFile(.destination);
+ } else if (close_output) {
+ this.doCloseFile(.source);
+ }
+ }
+
+ const os = std.os;
+
+ pub fn doCloseFile(this: *CopyFile, comptime which: IOWhich) void {
+ switch (which) {
+ .both => {
+ _ = JSC.Node.Syscall.close(this.destination_fd);
+ _ = JSC.Node.Syscall.close(this.source_fd);
+ },
+ .destination => {
+ _ = JSC.Node.Syscall.close(this.destination_fd);
+ },
+ .source => {
+ _ = JSC.Node.Syscall.close(this.source_fd);
+ },
+ }
+ }
+
+ const O = if (Environment.isLinux) linux.O else std.os.O;
+ const open_destination_flags = O.CLOEXEC | O.CREAT | O.WRONLY | O.TRUNC;
+ const open_source_flags = O.CLOEXEC | O.RDONLY;
+
+ pub fn doOpenFile(this: *CopyFile, comptime which: IOWhich) !void {
+ // open source file first
+ // if it fails, we don't want the extra destination file hanging out
+ if (which == .both or which == .source) {
+ this.source_fd = switch (JSC.Node.Syscall.open(
+ this.source_file_store.pathlike.path.sliceZAssume(),
+ open_source_flags,
+ 0,
+ )) {
+ .result => |result| result,
+ .err => |errno| {
+ this.system_error = errno.toSystemError();
+ return AsyncIO.asError(errno.errno);
+ },
+ };
+ }
+
+ if (which == .both or which == .destination) {
+ this.destination_fd = switch (JSC.Node.Syscall.open(
+ this.destination_file_store.pathlike.path.sliceZAssume(),
+ open_destination_flags,
+ JSC.Node.default_permission,
+ )) {
+ .result => |result| result,
+ .err => |errno| {
+ if (which == .both) {
+ _ = JSC.Node.Syscall.close(this.source_fd);
+ this.source_fd = 0;
+ }
+
+ this.system_error = errno.toSystemError();
+ return AsyncIO.asError(errno.errno);
+ },
+ };
+ }
+ }
+
+ const TryWith = enum {
+ sendfile,
+ copy_file_range,
+ splice,
+
+ pub const tag = std.EnumMap(TryWith, JSC.Node.Syscall.Tag).init(.{
+ .sendfile = .sendfile,
+ .copy_file_range = .copy_file_range,
+ .splice = .splice,
+ });
+ };
+
+ pub fn doCopyFileRange(
+ this: *CopyFile,
+ comptime use: TryWith,
+ comptime clear_append_if_invalid: bool,
+ ) anyerror!void {
+ this.read_off += this.offset;
+
+ var remain = @as(usize, this.max_length);
+ if (remain == max_size or remain == 0) {
+ // sometimes stat lies
+ // let's give it 4096 and see how it goes
+ remain = 4096;
+ }
+
+ var total_written: usize = 0;
+ const src_fd = this.source_fd;
+ const dest_fd = this.destination_fd;
+
+ defer {
+ this.read_len = @truncate(SizeType, total_written);
+ }
+
+ var has_unset_append = false;
+
+ while (true) {
+ const written = switch (comptime use) {
+ .copy_file_range => linux.copy_file_range(src_fd, null, dest_fd, null, remain, 0),
+ .sendfile => linux.sendfile(dest_fd, src_fd, null, remain),
+ .splice => bun.C.splice(src_fd, null, dest_fd, null, remain, 0),
+ };
+
+ switch (linux.getErrno(written)) {
+ .SUCCESS => {},
+
+ .INVAL => {
+ if (comptime clear_append_if_invalid) {
+ if (!has_unset_append) {
+ // https://kylelaker.com/2018/08/31/stdout-oappend.html
+ // make() can set STDOUT / STDERR to O_APPEND
+ // this messes up sendfile()
+ has_unset_append = true;
+ const flags = linux.fcntl(dest_fd, linux.F.GETFL, 0);
+ if ((flags & O.APPEND) != 0) {
+ _ = linux.fcntl(dest_fd, linux.F.SETFL, flags ^ O.APPEND);
+ continue;
+ }
+ }
+ }
+
+ this.system_error = (JSC.Node.Syscall.Error{
+ .errno = @intCast(JSC.Node.Syscall.Error.Int, @enumToInt(linux.E.INVAL)),
+ .syscall = TryWith.tag.get(use).?,
+ }).toSystemError();
+ return AsyncIO.asError(linux.E.INVAL);
+ },
+ else => |errno| {
+ this.system_error = (JSC.Node.Syscall.Error{
+ .errno = @intCast(JSC.Node.Syscall.Error.Int, @enumToInt(errno)),
+ .syscall = TryWith.tag.get(use).?,
+ }).toSystemError();
+ return AsyncIO.asError(errno);
+ },
+ }
+
+ // wrote zero bytes means EOF
+ remain -|= written;
+ total_written += written;
+ if (written == 0 or remain == 0) break;
+ }
+ }
+
+ pub fn doFCopyFile(this: *CopyFile) anyerror!void {
+ switch (JSC.Node.Syscall.fcopyfile(this.source_fd, this.destination_fd, os.system.COPYFILE_DATA)) {
+ .err => |errno| {
+ this.system_error = errno.toSystemError();
+
+ return AsyncIO.asError(errno.errno);
+ },
+ .result => {},
+ }
+ }
+
+ pub fn doClonefile(this: *CopyFile) anyerror!void {
+ var source_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var dest_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+
+ switch (JSC.Node.Syscall.clonefile(
+ this.source_file_store.pathlike.path.sliceZ(&source_buf),
+ this.destination_file_store.pathlike.path.sliceZ(
+ &dest_buf,
+ ),
+ )) {
+ .err => |errno| {
+ this.system_error = errno.toSystemError();
+ return AsyncIO.asError(errno.errno);
+ },
+ .result => {},
+ }
+ }
+
+ pub fn runAsync(this: *CopyFile) void {
+ // defer task.onFinish();
+
+ var stat_: ?std.os.Stat = null;
+
+ if (this.destination_file_store.pathlike == .fd) {
+ this.destination_fd = this.destination_file_store.pathlike.fd;
+ }
+
+ if (this.source_file_store.pathlike == .fd) {
+ this.source_fd = this.source_file_store.pathlike.fd;
+ }
+
+ // Do we need to open both files?
+ if (this.destination_fd == null_fd and this.source_fd == null_fd) {
+
+ // First, we attempt to clonefile() on macOS
+ // This is the fastest way to copy a file.
+ if (comptime Environment.isMac) {
+ if (this.offset == 0 and this.source_file_store.pathlike == .path and this.destination_file_store.pathlike == .path) {
+ do_clonefile: {
+
+ // stat the output file, make sure it:
+ // 1. Exists
+ switch (JSC.Node.Syscall.stat(this.source_file_store.pathlike.path.sliceZAssume())) {
+ .result => |result| {
+ stat_ = result;
+
+ if (os.S.ISDIR(result.mode)) {
+ this.system_error = unsupported_directory_error;
+ return;
+ }
+
+ if (!os.S.ISREG(result.mode))
+ break :do_clonefile;
+ },
+ .err => |err| {
+ // If we can't stat it, we also can't copy it.
+ this.system_error = err.toSystemError();
+ return;
+ },
+ }
+
+ if (this.doClonefile()) {
+ if (this.max_length != Blob.max_size and this.max_length < @intCast(SizeType, stat_.?.size)) {
+ // If this fails...well, there's not much we can do about it.
+ _ = bun.C.truncate(
+ this.destination_file_store.pathlike.path.sliceZAssume(),
+ @intCast(std.os.off_t, this.max_length),
+ );
+ this.read_len = @intCast(SizeType, this.max_length);
+ } else {
+ this.read_len = @intCast(SizeType, stat_.?.size);
+ }
+ return;
+ } else |_| {
+
+ // this may still fail, in which case we just continue trying with fcopyfile
+ // it can fail when the input file already exists
+ // or if the output is not a directory
+ // or if it's a network volume
+ this.system_error = null;
+ }
+ }
+ }
+ }
+
+ this.doOpenFile(.both) catch return;
+ // Do we need to open only one file?
+ } else if (this.destination_fd == null_fd) {
+ this.source_fd = this.source_file_store.pathlike.fd;
+
+ this.doOpenFile(.destination) catch return;
+ // Do we need to open only one file?
+ } else if (this.source_fd == null_fd) {
+ this.destination_fd = this.destination_file_store.pathlike.fd;
+
+ this.doOpenFile(.source) catch return;
+ }
+
+ if (this.system_error != null) {
+ return;
+ }
+
+ std.debug.assert(this.destination_fd != null_fd);
+ std.debug.assert(this.source_fd != null_fd);
+
+ if (this.destination_file_store.pathlike == .fd) {}
+
+ const stat: std.os.Stat = stat_ orelse switch (JSC.Node.Syscall.fstat(this.source_fd)) {
+ .result => |result| result,
+ .err => |err| {
+ this.doClose();
+ this.system_error = err.toSystemError();
+ return;
+ },
+ };
+
+ if (os.S.ISDIR(stat.mode)) {
+ this.system_error = unsupported_directory_error;
+ this.doClose();
+ return;
+ }
+
+ if (stat.size != 0) {
+ this.max_length = @max(@min(@intCast(SizeType, stat.size), this.max_length), this.offset) - this.offset;
+ if (this.max_length == 0) {
+ this.doClose();
+ return;
+ }
+
+ if (os.S.ISREG(stat.mode) and
+ this.max_length > std.mem.page_size and
+ this.max_length != Blob.max_size)
+ {
+ bun.C.preallocate_file(this.destination_fd, 0, this.max_length) catch {};
+ }
+ }
+
+ if (comptime Environment.isLinux) {
+
+ // Bun.write(Bun.file("a"), Bun.file("b"))
+ if (os.S.ISREG(stat.mode) and (os.S.ISREG(this.destination_file_store.mode) or this.destination_file_store.mode == 0)) {
+ if (this.destination_file_store.is_atty orelse false) {
+ this.doCopyFileRange(.copy_file_range, true) catch {};
+ } else {
+ this.doCopyFileRange(.copy_file_range, false) catch {};
+ }
+
+ this.doClose();
+ return;
+ }
+
+ // $ bun run foo.js | bun run bar.js
+ if (os.S.ISFIFO(stat.mode) and os.S.ISFIFO(this.destination_file_store.mode)) {
+ if (this.destination_file_store.is_atty orelse false) {
+ this.doCopyFileRange(.splice, true) catch {};
+ } else {
+ this.doCopyFileRange(.splice, false) catch {};
+ }
+
+ this.doClose();
+ return;
+ }
+
+ if (os.S.ISREG(stat.mode) or os.S.ISCHR(stat.mode) or os.S.ISSOCK(stat.mode)) {
+ if (this.destination_file_store.is_atty orelse false) {
+ this.doCopyFileRange(.sendfile, true) catch {};
+ } else {
+ this.doCopyFileRange(.sendfile, false) catch {};
+ }
+
+ this.doClose();
+ return;
+ }
+
+ this.system_error = unsupported_non_regular_file_error;
+ this.doClose();
+ return;
+ }
+
+ if (comptime Environment.isMac) {
+ this.doFCopyFile() catch {
+ this.doClose();
+
+ return;
+ };
+ if (stat.size != 0 and @intCast(SizeType, stat.size) > this.max_length) {
+ _ = darwin.ftruncate(this.destination_fd, @intCast(std.os.off_t, this.max_length));
+ }
+
+ this.doClose();
+ } else {
+ @compileError("TODO: implement copyfile");
+ }
+ }
+ };
+ };
+
+ pub const FileStore = struct {
+ pathlike: JSC.Node.PathOrFileDescriptor,
+ mime_type: HTTPClient.MimeType = HTTPClient.MimeType.other,
+ is_atty: ?bool = null,
+ mode: JSC.Node.Mode = 0,
+ seekable: ?bool = null,
+ max_size: SizeType = Blob.max_size,
+
+ pub fn isSeekable(this: *const FileStore) ?bool {
+ if (this.seekable) |seekable| {
+ return seekable;
+ }
+
+ if (this.mode != 0) {
+ return std.os.S.ISREG(this.mode);
+ }
+
+ return null;
+ }
+
+ pub fn init(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?HTTPClient.MimeType) FileStore {
+ return .{ .pathlike = pathlike, .mime_type = mime_type orelse HTTPClient.MimeType.other };
+ }
+ };
+
+ pub const ByteStore = struct {
+ ptr: [*]u8 = undefined,
+ len: SizeType = 0,
+ cap: SizeType = 0,
+ allocator: std.mem.Allocator,
+
+ pub fn init(bytes: []u8, allocator: std.mem.Allocator) ByteStore {
+ return .{
+ .ptr = bytes.ptr,
+ .len = @truncate(SizeType, bytes.len),
+ .cap = @truncate(SizeType, bytes.len),
+ .allocator = allocator,
+ };
+ }
+
+ pub fn fromArrayList(list: std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !*ByteStore {
+ return ByteStore.init(list.items, allocator);
+ }
+
+ pub fn slice(this: ByteStore) []u8 {
+ return this.ptr[0..this.len];
+ }
+
+ pub fn deinit(this: *ByteStore) void {
+ this.allocator.free(this.ptr[0..this.cap]);
+ }
+
+ pub fn asArrayList(this: ByteStore) std.ArrayListUnmanaged(u8) {
+ return this.asArrayListLeak();
+ }
+
+ pub fn asArrayListLeak(this: ByteStore) std.ArrayListUnmanaged(u8) {
+ return .{
+ .items = this.ptr[0..this.len],
+ .capacity = this.cap,
+ };
+ }
+ };
+
+ pub fn getStream(
+ this: *Blob,
+ globalThis: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ var recommended_chunk_size: SizeType = 0;
+ var arguments_ = callframe.arguments(2);
+ var arguments = arguments_.ptr[0..arguments_.len];
+ if (arguments.len > 0) {
+ if (!arguments[0].isNumber() and !arguments[0].isUndefinedOrNull()) {
+ globalThis.throwInvalidArguments("chunkSize must be a number", .{});
+ return JSValue.jsUndefined();
+ }
+
+ recommended_chunk_size = @intCast(SizeType, @max(0, @truncate(i52, arguments[0].toInt64())));
+ }
+ return JSC.WebCore.ReadableStream.fromBlob(
+ globalThis,
+ this,
+ recommended_chunk_size,
+ );
+ }
+
+ fn promisified(
+ value: JSC.JSValue,
+ global: *JSGlobalObject,
+ ) JSC.JSValue {
+ if (value.isError()) {
+ return JSC.JSPromise.rejectedPromiseValue(global, value);
+ }
+
+ if (value.jsType() == .JSPromise)
+ return value;
+
+ return JSPromise.resolvedPromiseValue(
+ global,
+ value,
+ );
+ }
+
+ pub fn getText(
+ this: *Blob,
+ globalThis: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ return promisified(this.toString(globalThis, .clone), globalThis);
+ }
+
+ pub fn getTextTransfer(
+ this: *Blob,
+ globalObject: *JSC.JSGlobalObject,
+ ) JSC.JSValue {
+ return promisified(this.toString(globalObject, .transfer), globalObject);
+ }
+
+ pub fn getJSON(
+ this: *Blob,
+ globalThis: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ return promisified(this.toJSON(globalThis, .share), globalThis);
+ }
+
+ pub fn getArrayBufferTransfer(
+ this: *Blob,
+ globalThis: *JSC.JSGlobalObject,
+ ) JSC.JSValue {
+ return promisified(this.toArrayBuffer(globalThis, .transfer), globalThis);
+ }
+
+ pub fn getArrayBuffer(
+ this: *Blob,
+ globalThis: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ return promisified(this.toArrayBuffer(globalThis, .clone), globalThis);
+ }
+
+ pub fn getWriter(
+ this: *Blob,
+ globalThis: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ var arguments_ = callframe.arguments(1);
+ var arguments = arguments_.ptr[0..arguments_.len];
+
+ if (!arguments.ptr[0].isEmptyOrUndefinedOrNull() and !arguments.ptr[0].isObject()) {
+ globalThis.throwInvalidArguments("options must be an object or undefined", .{});
+ return JSValue.jsUndefined();
+ }
+
+ var store = this.store orelse {
+ globalThis.throwInvalidArguments("Blob is detached", .{});
+ return JSValue.jsUndefined();
+ };
+
+ if (store.data != .file) {
+ globalThis.throwInvalidArguments("Blob is read-only", .{});
+ return JSValue.jsUndefined();
+ }
+
+ var sink = JSC.WebCore.FileSink.init(globalThis.allocator(), null) catch |err| {
+ globalThis.throwInvalidArguments("Failed to create FileSink: {s}", .{@errorName(err)});
+ return JSValue.jsUndefined();
+ };
+
+ const input_path: JSC.WebCore.PathOrFileDescriptor = brk: {
+ if (store.data.file.pathlike == .fd) {
+ break :brk .{ .fd = store.data.file.pathlike.fd };
+ } else {
+ break :brk .{
+ .path = ZigString.Slice.fromUTF8NeverFree(
+ store.data.file.pathlike.path.slice(),
+ ).clone(
+ globalThis.allocator(),
+ ) catch unreachable,
+ };
+ }
+ };
+ defer input_path.deinit();
+
+ var stream_start: JSC.WebCore.StreamStart = .{
+ .FileSink = .{
+ .input_path = input_path,
+ },
+ };
+
+ if (arguments.len > 0 and arguments.ptr[0].isObject()) {
+ stream_start = JSC.WebCore.StreamStart.fromJSWithTag(globalThis, arguments[0], .FileSink);
+ stream_start.FileSink.input_path = input_path;
+ }
+
+ switch (sink.start(stream_start)) {
+ .err => |err| {
+ globalThis.vm().throwError(globalThis, err.toJSC(globalThis));
+ sink.finalize();
+
+ return JSC.JSValue.zero;
+ },
+ else => {},
+ }
+
+ return sink.toJS(globalThis);
+ }
+
+ /// https://w3c.github.io/FileAPI/#slice-method-algo
+ /// The slice() method returns a new Blob object with bytes ranging from the
+ /// optional start parameter up to but not including the optional end
+ /// parameter, and with a type attribute that is the value of the optional
+ /// contentType parameter. It must act as follows:
+ pub fn getSlice(
+ this: *Blob,
+ globalThis: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ var allocator = globalThis.allocator();
+ var arguments_ = callframe.arguments(2);
+ var args = arguments_.ptr[0..arguments_.len];
+
+ if (this.size == 0) {
+ const empty = Blob.initEmpty(globalThis);
+ var ptr = allocator.create(Blob) catch {
+ return JSC.JSValue.jsUndefined();
+ };
+ ptr.* = empty;
+ ptr.allocator = allocator;
+ return ptr.toJS(globalThis);
+ }
+
+ // If the optional start parameter is not used as a parameter when making this call, let relativeStart be 0.
+ var relativeStart: i64 = 0;
+
+ // If the optional end parameter is not used as a parameter when making this call, let relativeEnd be size.
+ var relativeEnd: i64 = @intCast(i64, this.size);
+
+ var args_iter = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), args);
+ if (args_iter.nextEat()) |start_| {
+ const start = start_.toInt64();
+ if (start < 0) {
+ // If the optional start parameter is negative, let relativeStart be start + size.
+ relativeStart = @intCast(i64, @max(start + @intCast(i64, this.size), 0));
+ } else {
+ // Otherwise, let relativeStart be start.
+ relativeStart = @min(@intCast(i64, start), @intCast(i64, this.size));
+ }
+ }
+
+ if (args_iter.nextEat()) |end_| {
+ const end = end_.toInt64();
+ // If end is negative, let relativeEnd be max((size + end), 0).
+ if (end < 0) {
+ // If the optional start parameter is negative, let relativeStart be start + size.
+ relativeEnd = @intCast(i64, @max(end + @intCast(i64, this.size), 0));
+ } else {
+ // Otherwise, let relativeStart be start.
+ relativeEnd = @min(@intCast(i64, end), @intCast(i64, this.size));
+ }
+ }
+
+ var content_type: string = "";
+ if (args_iter.nextEat()) |content_type_| {
+ if (content_type_.isString()) {
+ var zig_str = content_type_.getZigString(globalThis);
+ var slicer = zig_str.toSlice(bun.default_allocator);
+ defer slicer.deinit();
+ var slice = slicer.slice();
+ var content_type_buf = allocator.alloc(u8, slice.len) catch unreachable;
+ content_type = strings.copyLowercase(slice, content_type_buf);
+ }
+ }
+
+ const len = @intCast(SizeType, @max(relativeEnd - relativeStart, 0));
+
+ // This copies over the is_all_ascii flag
+ // which is okay because this will only be a <= slice
+ var blob = this.dupe();
+ blob.offset = @intCast(SizeType, relativeStart);
+ blob.size = len;
+ blob.content_type = content_type;
+ blob.content_type_allocated = content_type.len > 0;
+
+ var blob_ = allocator.create(Blob) catch unreachable;
+ blob_.* = blob;
+ blob_.allocator = allocator;
+ return blob_.toJS(globalThis);
+ }
+
+ pub fn getType(
+ this: *Blob,
+ globalThis: *JSC.JSGlobalObject,
+ ) callconv(.C) JSValue {
+ return ZigString.init(this.content_type).toValue(globalThis);
+ }
+
+ pub fn setType(
+ this: *Blob,
+ globalThis: *JSC.JSGlobalObject,
+ value: JSC.JSValue,
+ ) callconv(.C) bool {
+ var zig_str = value.getZigString(globalThis);
+ if (zig_str.is16Bit())
+ return false;
+
+ var slice = zig_str.trimmedSlice();
+ if (strings.eql(slice, this.content_type))
+ return true;
+
+ const prev_content_type = this.content_type;
+ {
+ defer if (this.content_type_allocated) bun.default_allocator.free(prev_content_type);
+ var content_type_buf = globalThis.allocator().alloc(u8, slice.len) catch unreachable;
+ this.content_type = strings.copyLowercase(slice, content_type_buf);
+ }
+
+ this.content_type_allocated = true;
+ return true;
+ }
+
+ pub fn getSize(this: *Blob, _: *JSC.JSGlobalObject) callconv(.C) JSValue {
+ if (this.size == Blob.max_size) {
+ this.resolveSize();
+ if (this.size == Blob.max_size and this.store != null) {
+ return JSC.jsNumber(std.math.inf(f64));
+ } else if (this.size == 0 and this.store != null) {
+ if (this.store.?.data == .file and
+ (this.store.?.data.file.seekable orelse true) == false and
+ this.store.?.data.file.max_size == Blob.max_size)
+ {
+ return JSC.jsNumber(std.math.inf(f64));
+ }
+ }
+ }
+
+ return JSValue.jsNumber(this.size);
+ }
+
+ pub fn resolveSize(this: *Blob) void {
+ if (this.store) |store| {
+ if (store.data == .bytes) {
+ const offset = this.offset;
+ const store_size = store.size();
+ if (store_size != Blob.max_size) {
+ this.offset = @min(store_size, offset);
+ this.size = store_size - offset;
+ }
+
+ return;
+ } else if (store.data == .file) {
+ if (store.data.file.seekable == null) {
+ if (store.data.file.pathlike == .path) {
+ var buffer: [bun.MAX_PATH_BYTES]u8 = undefined;
+ switch (JSC.Node.Syscall.stat(store.data.file.pathlike.path.sliceZ(&buffer))) {
+ .result => |stat| {
+ store.data.file.max_size = if (std.os.S.ISREG(stat.mode) or stat.size > 0)
+ @truncate(SizeType, @intCast(u64, @max(stat.size, 0)))
+ else
+ Blob.max_size;
+ store.data.file.mode = stat.mode;
+ store.data.file.seekable = std.os.S.ISREG(stat.mode);
+ },
+ // the file may not exist yet. Thats's okay.
+ else => {},
+ }
+ } else if (store.data.file.pathlike == .fd) {
+ switch (JSC.Node.Syscall.fstat(store.data.file.pathlike.fd)) {
+ .result => |stat| {
+ store.data.file.max_size = if (std.os.S.ISREG(stat.mode) or stat.size > 0)
+ @truncate(SizeType, @intCast(u64, @max(stat.size, 0)))
+ else
+ Blob.max_size;
+ store.data.file.mode = stat.mode;
+ store.data.file.seekable = std.os.S.ISREG(stat.mode);
+ },
+ // the file may not exist yet. Thats's okay.
+ else => {},
+ }
+ }
+ }
+
+ if (store.data.file.seekable != null and store.data.file.max_size != Blob.max_size) {
+ const store_size = store.data.file.max_size;
+ const offset = this.offset;
+
+ this.offset = @min(store_size, offset);
+ this.size = store_size -| offset;
+ return;
+ }
+ }
+
+ this.size = 0;
+ } else {
+ this.size = 0;
+ }
+ }
+
+ pub fn constructor(
+ globalThis: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) ?*Blob {
+ var allocator = globalThis.allocator();
+ var blob: Blob = undefined;
+ var arguments = callframe.arguments(2);
+ var args = arguments.ptr[0..arguments.len];
+
+ switch (args.len) {
+ 0 => {
+ var empty: []u8 = &[_]u8{};
+ blob = Blob.init(empty, allocator, globalThis);
+ },
+ else => {
+ blob = get(globalThis, args[0], false, true) catch |err| {
+ if (err == error.InvalidArguments) {
+ globalThis.throwInvalidArguments("new Blob() expects an Array", .{});
+ return null;
+ }
+ globalThis.throw("out of memory", .{});
+ return null;
+ };
+
+ if (args.len > 1) {
+ var options = args[0];
+ if (options.isCell()) {
+ // type, the ASCII-encoded string in lower case
+ // representing the media type of the Blob.
+ // Normative conditions for this member are provided
+ // in the § 3.1 Constructors.
+ if (options.get(globalThis, "type")) |content_type| {
+ if (content_type.isString()) {
+ var content_type_str = content_type.getZigString(globalThis);
+ if (!content_type_str.is16Bit()) {
+ var slice = content_type_str.trimmedSlice();
+ var content_type_buf = allocator.alloc(u8, slice.len) catch unreachable;
+ blob.content_type = strings.copyLowercase(slice, content_type_buf);
+ blob.content_type_allocated = true;
+ }
+ }
+ }
+ }
+ }
+
+ if (blob.content_type.len == 0) {
+ blob.content_type = "";
+ }
+ },
+ }
+
+ var blob_ = allocator.create(Blob) catch unreachable;
+ blob_.* = blob;
+ blob_.allocator = allocator;
+ return blob_;
+ }
+
+ pub fn finalize(this: *Blob) callconv(.C) void {
+ this.deinit();
+ }
+
+ pub fn initWithAllASCII(bytes: []u8, allocator: std.mem.Allocator, globalThis: *JSGlobalObject, is_all_ascii: bool) Blob {
+ // avoid allocating a Blob.Store if the buffer is actually empty
+ var store: ?*Blob.Store = null;
+ if (bytes.len > 0) {
+ store = Blob.Store.init(bytes, allocator) catch unreachable;
+ store.?.is_all_ascii = is_all_ascii;
+ }
+ return Blob{
+ .size = @truncate(SizeType, bytes.len),
+ .store = store,
+ .allocator = null,
+ .content_type = "",
+ .globalThis = globalThis,
+ .is_all_ascii = is_all_ascii,
+ };
+ }
+
+ pub fn init(bytes: []u8, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) Blob {
+ return Blob{
+ .size = @truncate(SizeType, bytes.len),
+ .store = if (bytes.len > 0)
+ Blob.Store.init(bytes, allocator) catch unreachable
+ else
+ null,
+ .allocator = null,
+ .content_type = "",
+ .globalThis = globalThis,
+ };
+ }
+
+ pub fn create(
+ bytes_: []const u8,
+ allocator: std.mem.Allocator,
+ globalThis: *JSGlobalObject,
+ was_string: bool,
+ ) Blob {
+ var bytes = allocator.dupe(u8, bytes_) catch @panic("Out of memory");
+ return Blob{
+ .size = @truncate(SizeType, bytes_.len),
+ .store = if (bytes.len > 0)
+ Blob.Store.init(bytes, allocator) catch unreachable
+ else
+ null,
+ .allocator = null,
+ .content_type = if (was_string) MimeType.text.value else "",
+ .globalThis = globalThis,
+ };
+ }
+
+ pub fn initWithStore(store: *Blob.Store, globalThis: *JSGlobalObject) Blob {
+ return Blob{
+ .size = store.size(),
+ .store = store,
+ .allocator = null,
+ .content_type = if (store.data == .file)
+ store.data.file.mime_type.value
+ else
+ "",
+ .globalThis = globalThis,
+ };
+ }
+
+ pub fn initEmpty(globalThis: *JSGlobalObject) Blob {
+ return Blob{
+ .size = 0,
+ .store = null,
+ .allocator = null,
+ .content_type = "",
+ .globalThis = globalThis,
+ };
+ }
+
+ // Transferring doesn't change the reference count
+ // It is a move
+ inline fn transfer(this: *Blob) void {
+ this.store = null;
+ }
+
+ pub fn detach(this: *Blob) void {
+ if (this.store != null) this.store.?.deref();
+ this.store = null;
+ }
+
+ /// This does not duplicate
+ /// This creates a new view
+ /// and increment the reference count
+ pub fn dupe(this: *const Blob) Blob {
+ if (this.store != null) this.store.?.ref();
+ var duped = this.*;
+ duped.allocator = null;
+ return duped;
+ }
+
+ pub fn deinit(this: *Blob) void {
+ this.detach();
+
+ if (this.allocator) |alloc| {
+ this.allocator = null;
+ alloc.destroy(this);
+ }
+ }
+
+ pub fn sharedView(this: *const Blob) []const u8 {
+ if (this.size == 0 or this.store == null) return "";
+ var slice_ = this.store.?.sharedView();
+ if (slice_.len == 0) return "";
+ slice_ = slice_[this.offset..];
+
+ return slice_[0..@min(slice_.len, @as(usize, this.size))];
+ }
+
+ pub const Lifetime = JSC.WebCore.Lifetime;
+ pub fn setIsASCIIFlag(this: *Blob, is_all_ascii: bool) void {
+ this.is_all_ascii = is_all_ascii;
+ // if this Blob represents the entire binary data
+ // which will be pretty common
+ // we can update the store's is_all_ascii flag
+ // and any other Blob that points to the same store
+ // can skip checking the encoding
+ if (this.size > 0 and this.offset == 0 and this.store.?.data == .bytes) {
+ this.store.?.is_all_ascii = is_all_ascii;
+ }
+ }
+
+ pub fn NewReadFileHandler(comptime Function: anytype) type {
+ return struct {
+ context: Blob,
+ promise: JSPromise.Strong = .{},
+ globalThis: *JSGlobalObject,
+ pub fn run(handler: *@This(), bytes_: Blob.Store.ReadFile.ResultType) void {
+ var promise = handler.promise.swap();
+ var blob = handler.context;
+ blob.allocator = null;
+ var globalThis = handler.globalThis;
+ bun.default_allocator.destroy(handler);
+ switch (bytes_) {
+ .result => |result| {
+ const bytes = result.buf;
+ if (blob.size > 0)
+ blob.size = @min(@truncate(u32, bytes.len), blob.size);
+ const value = Function(&blob, globalThis, bytes, .temporary);
+
+ // invalid JSON needs to be rejected
+ if (value.isAnyError()) {
+ promise.reject(globalThis, value);
+ } else {
+ promise.resolve(globalThis, value);
+ }
+ },
+ .err => |err| {
+ promise.reject(globalThis, err.toErrorInstance(globalThis));
+ },
+ }
+ }
+ };
+ }
+
+ pub const WriteFilePromise = struct {
+ promise: JSPromise.Strong = .{},
+ globalThis: *JSGlobalObject,
+ pub fn run(handler: *@This(), count: Blob.Store.WriteFile.ResultType) void {
+ var promise = handler.promise.swap();
+ var globalThis = handler.globalThis;
+ bun.default_allocator.destroy(handler);
+ const value = promise.asValue(globalThis);
+ value.ensureStillAlive();
+ switch (count) {
+ .err => |err| {
+ promise.reject(globalThis, err.toErrorInstance(globalThis));
+ },
+ .result => |wrote| {
+ promise.resolve(globalThis, JSC.JSValue.jsNumberFromUint64(wrote));
+ },
+ }
+ }
+ };
+
+ pub fn NewInternalReadFileHandler(comptime Context: type, comptime Function: anytype) type {
+ return struct {
+ pub fn run(handler: *anyopaque, bytes_: Store.ReadFile.ResultType) void {
+ Function(bun.cast(Context, handler), bytes_);
+ }
+ };
+ }
+
+ pub fn doReadFileInternal(this: *Blob, comptime Handler: type, ctx: Handler, comptime Function: anytype, global: *JSGlobalObject) void {
+ var file_read = Store.ReadFile.createWithCtx(
+ bun.default_allocator,
+ this.store.?,
+ ctx,
+ NewInternalReadFileHandler(Handler, Function).run,
+ this.offset,
+ this.size,
+ ) catch unreachable;
+ var read_file_task = Store.ReadFile.ReadFileTask.createOnJSThread(bun.default_allocator, global, file_read) catch unreachable;
+ read_file_task.schedule();
+ }
+
+ pub fn doReadFile(this: *Blob, comptime Function: anytype, global: *JSGlobalObject) JSValue {
+ const Handler = NewReadFileHandler(Function);
+ var promise = JSPromise.create(global);
+
+ var handler = Handler{
+ .context = this.*,
+ .globalThis = global,
+ };
+ const promise_value = promise.asValue(global);
+ promise_value.ensureStillAlive();
+ handler.promise.strong.set(global, promise_value);
+
+ 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_value;
+ }
+
+ pub fn needsToReadFile(this: *const Blob) bool {
+ return this.store != null and this.store.?.data == .file;
+ }
+
+ pub fn toStringWithBytes(this: *Blob, global: *JSGlobalObject, buf: []const u8, comptime lifetime: Lifetime) JSValue {
+ // null == unknown
+ // false == can't be
+ const could_be_all_ascii = this.is_all_ascii orelse this.store.?.is_all_ascii;
+
+ if (could_be_all_ascii == null or !could_be_all_ascii.?) {
+ // if toUTF16Alloc returns null, it means there are no non-ASCII characters
+ // instead of erroring, invalid characters will become a U+FFFD replacement character
+ if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch unreachable) |external| {
+ if (lifetime != .temporary)
+ this.setIsASCIIFlag(false);
+
+ if (lifetime == .transfer) {
+ this.detach();
+ }
+
+ if (lifetime == .temporary) {
+ bun.default_allocator.free(bun.constStrToU8(buf));
+ }
+
+ return ZigString.toExternalU16(external.ptr, external.len, global);
+ }
+
+ if (lifetime != .temporary) this.setIsASCIIFlag(true);
+ }
+
+ if (buf.len == 0) {
+ return ZigString.Empty.toValue(global);
+ }
+
+ switch (comptime lifetime) {
+ // strings are immutable
+ // we don't need to clone
+ .clone => {
+ this.store.?.ref();
+ return ZigString.init(buf).external(global, this.store.?, Store.external);
+ },
+ .transfer => {
+ var store = this.store.?;
+ std.debug.assert(store.data == .bytes);
+ this.transfer();
+ return ZigString.init(buf).external(global, store, Store.external);
+ },
+ // strings are immutable
+ // sharing isn't really a thing
+ .share => {
+ this.store.?.ref();
+ return ZigString.init(buf).external(global, this.store.?, Store.external);
+ },
+ .temporary => {
+ return ZigString.init(buf).toExternalValue(global);
+ },
+ }
+ }
+
+ pub fn toString(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
+ if (this.needsToReadFile()) {
+ return this.doReadFile(toStringWithBytes, global);
+ }
+
+ const view_: []u8 =
+ bun.constStrToU8(this.sharedView());
+
+ if (view_.len == 0)
+ return ZigString.Empty.toValue(global);
+
+ return toStringWithBytes(this, global, view_, lifetime);
+ }
+
+ pub fn toJSON(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
+ if (this.needsToReadFile()) {
+ return this.doReadFile(toJSONWithBytes, global);
+ }
+
+ var view_ = this.sharedView();
+
+ if (view_.len == 0)
+ return ZigString.Empty.toValue(global);
+
+ return toJSONWithBytes(this, global, view_, lifetime);
+ }
+
+ pub fn toJSONWithBytes(this: *Blob, global: *JSGlobalObject, buf: []const u8, comptime lifetime: Lifetime) JSValue {
+ // null == unknown
+ // false == can't be
+ const could_be_all_ascii = this.is_all_ascii orelse this.store.?.is_all_ascii;
+ defer if (comptime lifetime == .temporary) bun.default_allocator.free(bun.constStrToU8(buf));
+
+ if (could_be_all_ascii == null or !could_be_all_ascii.?) {
+ var stack_fallback = std.heap.stackFallback(4096, bun.default_allocator);
+ const allocator = stack_fallback.get();
+ // if toUTF16Alloc returns null, it means there are no non-ASCII characters
+ if (strings.toUTF16Alloc(allocator, buf, false) catch null) |external| {
+ if (comptime lifetime != .temporary) this.setIsASCIIFlag(false);
+ const result = ZigString.init16(external).toJSONObject(global);
+ allocator.free(external);
+ return result;
+ }
+
+ if (comptime lifetime != .temporary) this.setIsASCIIFlag(true);
+ }
+
+ if (comptime lifetime == .temporary) {
+ return ZigString.init(buf).toJSONObject(global);
+ } else {
+ return ZigString.init(buf).toJSONObject(global);
+ }
+ }
+
+ pub fn toArrayBufferWithBytes(this: *Blob, global: *JSGlobalObject, buf: []u8, comptime lifetime: Lifetime) JSValue {
+ switch (comptime lifetime) {
+ .clone => {
+ return JSC.ArrayBuffer.create(global, buf, .ArrayBuffer);
+ },
+ .share => {
+ this.store.?.ref();
+ return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJSWithContext(
+ global,
+ this.store.?,
+ JSC.BlobArrayBuffer_deallocator,
+ null,
+ );
+ },
+ .transfer => {
+ var store = this.store.?;
+ this.transfer();
+ return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJSWithContext(
+ global,
+ store,
+ JSC.BlobArrayBuffer_deallocator,
+ null,
+ );
+ },
+ .temporary => {
+ return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJS(
+ global,
+ null,
+ );
+ },
+ }
+ }
+
+ pub fn toArrayBuffer(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
+ if (this.needsToReadFile()) {
+ return this.doReadFile(toArrayBufferWithBytes, global);
+ }
+
+ var view_ = this.sharedView();
+
+ if (view_.len == 0)
+ return JSC.ArrayBuffer.create(global, "", .ArrayBuffer);
+
+ return toArrayBufferWithBytes(this, global, bun.constStrToU8(view_), lifetime);
+ }
+
+ pub inline fn get(
+ global: *JSGlobalObject,
+ arg: JSValue,
+ comptime move: bool,
+ comptime require_array: bool,
+ ) anyerror!Blob {
+ return fromJSMovable(global, arg, move, require_array);
+ }
+
+ pub inline fn fromJSMove(global: *JSGlobalObject, arg: JSValue) anyerror!Blob {
+ return fromJSWithoutDeferGC(global, arg, true, false);
+ }
+
+ pub inline fn fromJSClone(global: *JSGlobalObject, arg: JSValue) anyerror!Blob {
+ return fromJSWithoutDeferGC(global, arg, false, true);
+ }
+
+ pub inline fn fromJSCloneOptionalArray(global: *JSGlobalObject, arg: JSValue) anyerror!Blob {
+ return fromJSWithoutDeferGC(global, arg, false, false);
+ }
+
+ fn fromJSMovable(
+ global: *JSGlobalObject,
+ arg: JSValue,
+ comptime move: bool,
+ comptime require_array: bool,
+ ) anyerror!Blob {
+ const FromJSFunction = if (comptime move and !require_array)
+ fromJSMove
+ else if (!require_array)
+ fromJSCloneOptionalArray
+ else
+ fromJSClone;
+
+ return FromJSFunction(global, arg);
+ }
+
+ fn fromJSWithoutDeferGC(
+ global: *JSGlobalObject,
+ arg: JSValue,
+ comptime move: bool,
+ comptime require_array: bool,
+ ) anyerror!Blob {
+ var current = arg;
+ if (current.isUndefinedOrNull()) {
+ return Blob{ .globalThis = global };
+ }
+
+ var top_value = current;
+ var might_only_be_one_thing = false;
+ arg.ensureStillAlive();
+ defer arg.ensureStillAlive();
+ switch (current.jsTypeLoose()) {
+ .Array, .DerivedArray => {
+ var top_iter = JSC.JSArrayIterator.init(current, global);
+ might_only_be_one_thing = top_iter.len == 1;
+ if (top_iter.len == 0) {
+ return Blob{ .globalThis = global };
+ }
+ if (might_only_be_one_thing) {
+ top_value = top_iter.next().?;
+ }
+ },
+ else => {
+ might_only_be_one_thing = true;
+ if (require_array) {
+ return error.InvalidArguments;
+ }
+ },
+ }
+
+ if (might_only_be_one_thing or !move) {
+
+ // Fast path: one item, we don't need to join
+ switch (top_value.jsTypeLoose()) {
+ .Cell,
+ .NumberObject,
+ JSC.JSValue.JSType.String,
+ JSC.JSValue.JSType.StringObject,
+ JSC.JSValue.JSType.DerivedStringObject,
+ => {
+ var sliced = top_value.toSlice(global, bun.default_allocator);
+ const is_all_ascii = !sliced.isAllocated();
+ if (!sliced.isAllocated() and sliced.len > 0) {
+ sliced.ptr = @ptrCast([*]const u8, (try bun.default_allocator.dupe(u8, sliced.slice())).ptr);
+ sliced.allocator = NullableAllocator.init(bun.default_allocator);
+ }
+
+ return Blob.initWithAllASCII(bun.constStrToU8(sliced.slice()), bun.default_allocator, global, is_all_ascii);
+ },
+
+ JSC.JSValue.JSType.ArrayBuffer,
+ JSC.JSValue.JSType.Int8Array,
+ JSC.JSValue.JSType.Uint8Array,
+ JSC.JSValue.JSType.Uint8ClampedArray,
+ JSC.JSValue.JSType.Int16Array,
+ JSC.JSValue.JSType.Uint16Array,
+ JSC.JSValue.JSType.Int32Array,
+ JSC.JSValue.JSType.Uint32Array,
+ JSC.JSValue.JSType.Float32Array,
+ JSC.JSValue.JSType.Float64Array,
+ JSC.JSValue.JSType.BigInt64Array,
+ JSC.JSValue.JSType.BigUint64Array,
+ JSC.JSValue.JSType.DataView,
+ => {
+ var buf = try bun.default_allocator.dupe(u8, top_value.asArrayBuffer(global).?.byteSlice());
+
+ return Blob.init(buf, bun.default_allocator, global);
+ },
+
+ .DOMWrapper => {
+ if (top_value.as(Blob)) |blob| {
+ if (comptime move) {
+ var _blob = blob.*;
+ _blob.allocator = null;
+ blob.transfer();
+ return _blob;
+ } else {
+ return blob.dupe();
+ }
+ }
+ },
+
+ else => {},
+ }
+ }
+
+ var stack_allocator = std.heap.stackFallback(1024, bun.default_allocator);
+ var stack_mem_all = stack_allocator.get();
+ var stack: std.ArrayList(JSValue) = std.ArrayList(JSValue).init(stack_mem_all);
+ var joiner = StringJoiner{ .use_pool = false, .node_allocator = stack_mem_all };
+ var could_have_non_ascii = false;
+
+ defer if (stack_allocator.fixed_buffer_allocator.end_index >= 1024) stack.deinit();
+
+ while (true) {
+ switch (current.jsTypeLoose()) {
+ .NumberObject,
+ JSC.JSValue.JSType.String,
+ JSC.JSValue.JSType.StringObject,
+ JSC.JSValue.JSType.DerivedStringObject,
+ => {
+ var sliced = current.toSlice(global, bun.default_allocator);
+ const allocator = sliced.allocator.get();
+ could_have_non_ascii = could_have_non_ascii or allocator != null;
+ joiner.append(
+ sliced.slice(),
+ 0,
+ allocator,
+ );
+ },
+
+ .Array, .DerivedArray => {
+ var iter = JSC.JSArrayIterator.init(current, global);
+ try stack.ensureUnusedCapacity(iter.len);
+ var any_arrays = false;
+ while (iter.next()) |item| {
+ if (item.isUndefinedOrNull()) continue;
+
+ // When it's a string or ArrayBuffer inside an array, we can avoid the extra push/pop
+ // we only really want this for nested arrays
+ // However, we must preserve the order
+ // That means if there are any arrays
+ // we have to restart the loop
+ if (!any_arrays) {
+ switch (item.jsTypeLoose()) {
+ .NumberObject,
+ .Cell,
+ JSC.JSValue.JSType.String,
+ JSC.JSValue.JSType.StringObject,
+ JSC.JSValue.JSType.DerivedStringObject,
+ => {
+ var sliced = item.toSlice(global, bun.default_allocator);
+ const allocator = sliced.allocator.get();
+ could_have_non_ascii = could_have_non_ascii or allocator != null;
+ joiner.append(
+ sliced.slice(),
+ 0,
+ allocator,
+ );
+ continue;
+ },
+ JSC.JSValue.JSType.ArrayBuffer,
+ JSC.JSValue.JSType.Int8Array,
+ JSC.JSValue.JSType.Uint8Array,
+ JSC.JSValue.JSType.Uint8ClampedArray,
+ JSC.JSValue.JSType.Int16Array,
+ JSC.JSValue.JSType.Uint16Array,
+ JSC.JSValue.JSType.Int32Array,
+ JSC.JSValue.JSType.Uint32Array,
+ JSC.JSValue.JSType.Float32Array,
+ JSC.JSValue.JSType.Float64Array,
+ JSC.JSValue.JSType.BigInt64Array,
+ JSC.JSValue.JSType.BigUint64Array,
+ JSC.JSValue.JSType.DataView,
+ => {
+ could_have_non_ascii = true;
+ var buf = item.asArrayBuffer(global).?;
+ joiner.append(buf.byteSlice(), 0, null);
+ continue;
+ },
+ .Array, .DerivedArray => {
+ any_arrays = true;
+ could_have_non_ascii = true;
+ break;
+ },
+
+ .DOMWrapper => {
+ if (item.as(Blob)) |blob| {
+ could_have_non_ascii = could_have_non_ascii or !(blob.is_all_ascii orelse false);
+ joiner.append(blob.sharedView(), 0, null);
+ continue;
+ }
+ },
+ else => {},
+ }
+ }
+
+ stack.appendAssumeCapacity(item);
+ }
+ },
+
+ .DOMWrapper => {
+ if (current.as(Blob)) |blob| {
+ could_have_non_ascii = could_have_non_ascii or !(blob.is_all_ascii orelse false);
+ joiner.append(blob.sharedView(), 0, null);
+ }
+ },
+
+ JSC.JSValue.JSType.ArrayBuffer,
+ JSC.JSValue.JSType.Int8Array,
+ JSC.JSValue.JSType.Uint8Array,
+ JSC.JSValue.JSType.Uint8ClampedArray,
+ JSC.JSValue.JSType.Int16Array,
+ JSC.JSValue.JSType.Uint16Array,
+ JSC.JSValue.JSType.Int32Array,
+ JSC.JSValue.JSType.Uint32Array,
+ JSC.JSValue.JSType.Float32Array,
+ JSC.JSValue.JSType.Float64Array,
+ JSC.JSValue.JSType.BigInt64Array,
+ JSC.JSValue.JSType.BigUint64Array,
+ JSC.JSValue.JSType.DataView,
+ => {
+ var buf = current.asArrayBuffer(global).?;
+ joiner.append(buf.slice(), 0, null);
+ could_have_non_ascii = true;
+ },
+
+ else => {
+ var sliced = current.toSlice(global, bun.default_allocator);
+ const allocator = sliced.allocator.get();
+ could_have_non_ascii = could_have_non_ascii or allocator != null;
+ joiner.append(
+ sliced.slice(),
+ 0,
+ allocator,
+ );
+ },
+ }
+ current = stack.popOrNull() orelse break;
+ }
+
+ var joined = try joiner.done(bun.default_allocator);
+
+ if (!could_have_non_ascii) {
+ return Blob.initWithAllASCII(joined, bun.default_allocator, global, true);
+ }
+ return Blob.init(joined, bun.default_allocator, global);
+ }
+};
+
+pub const AnyBlob = union(enum) {
+ Blob: Blob,
+ // InlineBlob: InlineBlob,
+ InternalBlob: InternalBlob,
+
+ pub fn toJSON(this: *AnyBlob, global: *JSGlobalObject, comptime lifetime: JSC.WebCore.Lifetime) JSValue {
+ switch (this.*) {
+ .Blob => return this.Blob.toJSON(global, lifetime),
+ // .InlineBlob => {
+ // if (this.InlineBlob.len == 0) {
+ // return JSValue.jsNull();
+ // }
+ // var str = this.InlineBlob.toStringOwned(global);
+ // return str.parseJSON(global);
+ // },
+ .InternalBlob => {
+ if (this.InternalBlob.bytes.items.len == 0) {
+ return JSValue.jsNull();
+ }
+
+ const str = this.InternalBlob.toJSON(global);
+
+ // the GC will collect the string
+ this.* = .{
+ .Blob = .{},
+ };
+
+ return str;
+ },
+ }
+ }
+
+ pub fn toString(this: *AnyBlob, global: *JSGlobalObject, comptime lifetime: JSC.WebCore.Lifetime) JSValue {
+ switch (this.*) {
+ .Blob => return this.Blob.toString(global, lifetime),
+ // .InlineBlob => {
+ // const owned = this.InlineBlob.toStringOwned(global);
+ // this.* = .{ .InlineBlob = .{ .len = 0 } };
+ // return owned;
+ // },
+ .InternalBlob => {
+ if (this.InternalBlob.bytes.items.len == 0) {
+ return ZigString.Empty.toValue(global);
+ }
+
+ const owned = this.InternalBlob.toStringOwned(global);
+ this.* = .{ .Blob = .{} };
+ return owned;
+ },
+ }
+ }
+
+ pub fn toArrayBuffer(this: *AnyBlob, global: *JSGlobalObject, comptime lifetime: JSC.WebCore.Lifetime) JSValue {
+ switch (this.*) {
+ .Blob => return this.Blob.toArrayBuffer(global, lifetime),
+ // .InlineBlob => {
+ // if (this.InlineBlob.len == 0) {
+ // return JSC.ArrayBuffer.empty.toJS(global, null);
+ // }
+ // var bytes = this.InlineBlob.sliceConst();
+ // this.InlineBlob.len = 0;
+ // const value = JSC.ArrayBuffer.create(
+ // global,
+ // bytes,
+ // .ArrayBuffer,
+ // );
+ // return value;
+ // },
+ .InternalBlob => {
+ if (this.InternalBlob.bytes.items.len == 0) {
+ return JSC.ArrayBuffer.create(global, "", .ArrayBuffer);
+ }
+
+ var bytes = this.InternalBlob.toOwnedSlice();
+ this.* = .{ .Blob = .{} };
+ const value = JSC.ArrayBuffer.fromBytes(
+ bytes,
+ .ArrayBuffer,
+ );
+ return value.toJS(global, null);
+ },
+ }
+ }
+
+ pub inline fn size(this: *const AnyBlob) Blob.SizeType {
+ return switch (this.*) {
+ .Blob => this.Blob.size,
+ else => @truncate(Blob.SizeType, this.slice().len),
+ };
+ }
+
+ pub fn from(this: *AnyBlob, list: std.ArrayList(u8)) void {
+ this.* = .{
+ .InternalBlob = InternalBlob{
+ .bytes = list,
+ },
+ };
+ }
+
+ pub fn isDetached(this: *const AnyBlob) bool {
+ return switch (this.*) {
+ .Blob => |blob| blob.isDetached(),
+ else => this.slice().len == 0,
+ };
+ }
+
+ pub fn store(this: *const @This()) ?*Blob.Store {
+ if (this.* == .Blob) {
+ return this.Blob.store;
+ }
+
+ return null;
+ }
+
+ pub fn contentType(self: *const @This()) []const u8 {
+ return switch (self.*) {
+ .Blob => self.Blob.content_type,
+ // .InlineBlob => self.InlineBlob.contentType(),
+ .InternalBlob => self.InternalBlob.contentType(),
+ };
+ }
+
+ pub fn wasString(self: *const @This()) bool {
+ return switch (self.*) {
+ .Blob => self.Blob.is_all_ascii orelse false,
+ // .InlineBlob => self.InlineBlob.was_string,
+ .InternalBlob => self.InternalBlob.was_string,
+ };
+ }
+
+ pub inline fn slice(self: *const @This()) []const u8 {
+ return switch (self.*) {
+ .Blob => self.Blob.sharedView(),
+ // .InlineBlob => self.InlineBlob.sliceConst(),
+ .InternalBlob => self.InternalBlob.sliceConst(),
+ };
+ }
+
+ pub fn needsToReadFile(self: *const @This()) bool {
+ return switch (self.*) {
+ .Blob => self.Blob.needsToReadFile(),
+ // .InlineBlob => false,
+ .InternalBlob => false,
+ };
+ }
+
+ pub fn detach(self: *@This()) void {
+ return switch (self.*) {
+ .Blob => {
+ self.Blob.detach();
+ self.* = .{
+ .Blob = .{},
+ };
+ },
+ // .InlineBlob => {
+ // self.InlineBlob.len = 0;
+ // },
+ .InternalBlob => {
+ self.InternalBlob.bytes.clearAndFree();
+ self.* = .{
+ .Blob = .{},
+ };
+ },
+ };
+ }
+};
+
+/// A single-use Blob
+pub const InternalBlob = struct {
+ bytes: std.ArrayList(u8),
+ was_string: bool = false,
+
+ pub fn toStringOwned(this: *@This(), globalThis: *JSC.JSGlobalObject) JSValue {
+ if (strings.toUTF16Alloc(globalThis.allocator(), this.bytes.items, false) catch &[_]u16{}) |out| {
+ const return_value = ZigString.toExternalU16(out.ptr, out.len, globalThis);
+ return_value.ensureStillAlive();
+ this.deinit();
+ return return_value;
+ } else {
+ var str = ZigString.init(this.toOwnedSlice());
+ str.mark();
+ return str.toExternalValue(globalThis);
+ }
+ }
+
+ pub fn toJSON(this: *@This(), globalThis: *JSC.JSGlobalObject) JSValue {
+ const str_bytes = ZigString.init(this.bytes.items).withEncoding();
+ const json = str_bytes.toJSONObject(globalThis);
+ this.deinit();
+ return json;
+ }
+
+ pub inline fn sliceConst(this: *const @This()) []const u8 {
+ return this.bytes.items;
+ }
+
+ pub fn deinit(this: *@This()) void {
+ this.bytes.clearAndFree();
+ }
+
+ pub inline fn slice(this: @This()) []u8 {
+ return this.bytes.items;
+ }
+
+ pub fn toOwnedSlice(this: *@This()) []u8 {
+ var bytes = this.bytes.items;
+ this.bytes.items = &.{};
+ this.bytes.capacity = 0;
+ return bytes;
+ }
+
+ pub fn clearAndFree(this: *@This()) void {
+ this.bytes.clearAndFree();
+ }
+
+ pub fn contentType(self: *const @This()) []const u8 {
+ if (self.was_string) {
+ return MimeType.text.value;
+ }
+
+ return MimeType.other.value;
+ }
+};
+
+/// A blob which stores all the data in the same space as a real Blob
+/// This is an optimization for small Response and Request bodies
+/// It means that we can avoid an additional heap allocation for a small response
+pub const InlineBlob = extern struct {
+ const real_blob_size = @sizeOf(Blob);
+ pub const IntSize = u8;
+ pub const available_bytes = real_blob_size - @sizeOf(IntSize) - 1 - 1;
+ bytes: [available_bytes]u8 align(1) = undefined,
+ len: IntSize align(1) = 0,
+ was_string: bool align(1) = false,
+
+ pub fn concat(first: []const u8, second: []const u8) InlineBlob {
+ const total = first.len + second.len;
+ std.debug.assert(total <= available_bytes);
+
+ var inline_blob: JSC.WebCore.InlineBlob = .{};
+ var bytes_slice = inline_blob.bytes[0..total];
+
+ if (first.len > 0)
+ @memcpy(bytes_slice.ptr, first.ptr, first.len);
+
+ if (second.len > 0)
+ @memcpy(bytes_slice.ptr + first.len, second.ptr, second.len);
+
+ inline_blob.len = @truncate(@TypeOf(inline_blob.len), total);
+ return inline_blob;
+ }
+
+ fn internalInit(data: []const u8, was_string: bool) InlineBlob {
+ std.debug.assert(data.len <= available_bytes);
+
+ var blob = InlineBlob{
+ .len = @intCast(IntSize, data.len),
+ .was_string = was_string,
+ };
+
+ if (data.len > 0)
+ @memcpy(&blob.bytes, data.ptr, data.len);
+ return blob;
+ }
+
+ pub fn init(data: []const u8) InlineBlob {
+ return internalInit(data, false);
+ }
+
+ pub fn initString(data: []const u8) InlineBlob {
+ return internalInit(data, true);
+ }
+
+ pub fn toStringOwned(this: *@This(), globalThis: *JSC.JSGlobalObject) JSValue {
+ if (this.len == 0)
+ return ZigString.Empty.toValue(globalThis);
+
+ var str = ZigString.init(this.sliceConst());
+
+ if (!strings.isAllASCII(this.sliceConst())) {
+ str.markUTF8();
+ }
+
+ const out = str.toValueGC(globalThis);
+ out.ensureStillAlive();
+ this.len = 0;
+ return out;
+ }
+
+ pub fn contentType(self: *const @This()) []const u8 {
+ if (self.was_string) {
+ return MimeType.text.value;
+ }
+
+ return MimeType.other.value;
+ }
+
+ pub fn deinit(_: *@This()) void {}
+
+ pub inline fn slice(this: *@This()) []u8 {
+ return this.bytes[0..this.len];
+ }
+
+ pub inline fn sliceConst(this: *const @This()) []const u8 {
+ return this.bytes[0..this.len];
+ }
+
+ pub fn toOwnedSlice(this: *@This()) []u8 {
+ return this.slice();
+ }
+
+ pub fn clearAndFree(_: *@This()) void {}
+};
diff --git a/src/bun.js/webcore/body.zig b/src/bun.js/webcore/body.zig
new file mode 100644
index 000000000..e91de032f
--- /dev/null
+++ b/src/bun.js/webcore/body.zig
@@ -0,0 +1,1029 @@
+const std = @import("std");
+const Api = @import("../../api/schema.zig").Api;
+const bun = @import("bun");
+const RequestContext = @import("../../http.zig").RequestContext;
+const MimeType = @import("../../http.zig").MimeType;
+const ZigURL = @import("../../url.zig").URL;
+const HTTPClient = @import("bun").HTTP;
+const NetworkThread = HTTPClient.NetworkThread;
+const AsyncIO = NetworkThread.AsyncIO;
+const JSC = @import("bun").JSC;
+const js = JSC.C;
+
+const Method = @import("../../http/method.zig").Method;
+const FetchHeaders = JSC.FetchHeaders;
+const ObjectPool = @import("../../pool.zig").ObjectPool;
+const SystemError = JSC.SystemError;
+const Output = @import("bun").Output;
+const MutableString = @import("bun").MutableString;
+const strings = @import("bun").strings;
+const string = @import("bun").string;
+const default_allocator = @import("bun").default_allocator;
+const FeatureFlags = @import("bun").FeatureFlags;
+const ArrayBuffer = @import("../base.zig").ArrayBuffer;
+const Properties = @import("../base.zig").Properties;
+const NewClass = @import("../base.zig").NewClass;
+const d = @import("../base.zig").d;
+const castObj = @import("../base.zig").castObj;
+const getAllocator = @import("../base.zig").getAllocator;
+const JSPrivateDataPtr = @import("../base.zig").JSPrivateDataPtr;
+const GetJSPrivateData = @import("../base.zig").GetJSPrivateData;
+const Environment = @import("../../env.zig");
+const ZigString = JSC.ZigString;
+const IdentityContext = @import("../../identity_context.zig").IdentityContext;
+const JSPromise = JSC.JSPromise;
+const JSValue = JSC.JSValue;
+const JSError = JSC.JSError;
+const JSGlobalObject = JSC.JSGlobalObject;
+const NullableAllocator = @import("../../nullable_allocator.zig").NullableAllocator;
+
+const VirtualMachine = JSC.VirtualMachine;
+const Task = JSC.Task;
+const JSPrinter = @import("../../js_printer.zig");
+const picohttp = @import("bun").picohttp;
+const StringJoiner = @import("../../string_joiner.zig");
+const uws = @import("bun").uws;
+
+const Blob = JSC.WebCore.Blob;
+const InlineBlob = JSC.WebCore.InlineBlob;
+const AnyBlob = JSC.WebCore.AnyBlob;
+const InternalBlob = JSC.WebCore.InternalBlob;
+const Response = JSC.WebCore.Response;
+const Request = JSC.WebCore.Request;
+
+// https://developer.mozilla.org/en-US/docs/Web/API/Body
+pub const Body = struct {
+ init: Init = Init{ .headers = null, .status_code = 200 },
+ value: Value, // = Value.empty,
+
+ pub inline fn len(this: *const Body) Blob.SizeType {
+ return this.value.size();
+ }
+
+ pub fn slice(this: *const Body) []const u8 {
+ return this.value.slice();
+ }
+
+ pub fn use(this: *Body) Blob {
+ return this.value.use();
+ }
+
+ pub fn clone(this: *Body, globalThis: *JSGlobalObject) Body {
+ return Body{
+ .init = this.init.clone(globalThis),
+ .value = this.value.clone(globalThis),
+ };
+ }
+
+ pub fn writeFormat(this: *const Body, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void {
+ const Writer = @TypeOf(writer);
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("bodyUsed: ");
+ formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.value == .Used), .BooleanObject, enable_ansi_colors);
+ formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
+ try writer.writeAll("\n");
+
+ // if (this.init.headers) |headers| {
+ // try formatter.writeIndent(Writer, writer);
+ // try writer.writeAll("headers: ");
+ // try headers.leak().writeFormat(formatter, writer, comptime enable_ansi_colors);
+ // try writer.writeAll("\n");
+ // }
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("status: ");
+ formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(this.init.status_code), .NumberObject, enable_ansi_colors);
+ if (this.value == .Blob) {
+ try formatter.printComma(Writer, writer, enable_ansi_colors);
+ try writer.writeAll("\n");
+ try formatter.writeIndent(Writer, writer);
+ try this.value.Blob.writeFormat(formatter, writer, enable_ansi_colors);
+ } else if (this.value == .InternalBlob) {
+ try formatter.printComma(Writer, writer, enable_ansi_colors);
+ try writer.writeAll("\n");
+ try formatter.writeIndent(Writer, writer);
+ try Blob.writeFormatForSize(this.value.size(), writer, enable_ansi_colors);
+ } else if (this.value == .Locked) {
+ if (this.value.Locked.readable) |stream| {
+ try formatter.printComma(Writer, writer, enable_ansi_colors);
+ try writer.writeAll("\n");
+ try formatter.writeIndent(Writer, writer);
+ formatter.printAs(.Object, Writer, writer, stream.value, stream.value.jsType(), enable_ansi_colors);
+ }
+ }
+ }
+
+ pub fn deinit(this: *Body, _: std.mem.Allocator) void {
+ if (this.init.headers) |headers| {
+ this.init.headers = null;
+
+ headers.deref();
+ }
+ this.value.deinit();
+ }
+
+ pub const Init = struct {
+ headers: ?*FetchHeaders = null,
+ status_code: u16,
+ method: Method = Method.GET,
+
+ pub fn clone(this: Init, _: *JSGlobalObject) Init {
+ var that = this;
+ var headers = this.headers;
+ if (headers) |head| {
+ that.headers = head.cloneThis();
+ }
+
+ return that;
+ }
+
+ pub fn init(allocator: std.mem.Allocator, ctx: *JSGlobalObject, response_init: JSC.JSValue, js_type: JSC.JSValue.JSType) !?Init {
+ var result = Init{ .status_code = 200 };
+
+ if (!response_init.isCell())
+ return null;
+
+ if (js_type == .DOMWrapper) {
+ // fast path: it's a Request object or a Response object
+ // we can skip calling JS getters
+ if (response_init.as(Request)) |req| {
+ if (req.headers) |headers| {
+ result.headers = headers.cloneThis();
+ }
+
+ result.method = req.method;
+ return result;
+ }
+
+ if (response_init.as(Response)) |req| {
+ return req.body.init.clone(ctx);
+ }
+ }
+
+ if (response_init.fastGet(ctx, .headers)) |headers| {
+ if (headers.as(FetchHeaders)) |orig| {
+ result.headers = orig.cloneThis();
+ } else {
+ result.headers = FetchHeaders.createFromJS(ctx.ptr(), headers);
+ }
+ }
+
+ if (response_init.fastGet(ctx, .status)) |status_value| {
+ const number = status_value.to(i32);
+ if (number > 0)
+ result.status_code = @truncate(u16, @intCast(u32, number));
+ }
+
+ if (response_init.fastGet(ctx, .method)) |method_value| {
+ var method_str = method_value.toSlice(ctx, allocator);
+ defer method_str.deinit();
+ if (method_str.len > 0) {
+ result.method = Method.which(method_str.slice()) orelse .GET;
+ }
+ }
+
+ if (result.headers == null and result.status_code < 200) return null;
+ return result;
+ }
+ };
+
+ pub const PendingValue = struct {
+ promise: ?JSValue = null,
+ readable: ?JSC.WebCore.ReadableStream = null,
+ // writable: JSC.WebCore.Sink
+
+ global: *JSGlobalObject,
+ task: ?*anyopaque = null,
+
+ /// runs after the data is available.
+ onReceiveValue: ?*const fn (ctx: *anyopaque, value: *Value) void = null,
+
+ /// conditionally runs when requesting data
+ /// used in HTTP server to ignore request bodies unless asked for it
+ onStartBuffering: ?*const fn (ctx: *anyopaque) void = null,
+
+ onStartStreaming: ?*const fn (ctx: *anyopaque) JSC.WebCore.DrainResult = null,
+
+ deinit: bool = false,
+ action: Action = Action.none,
+
+ pub fn toAnyBlob(this: *PendingValue) ?AnyBlob {
+ if (this.promise != null)
+ return null;
+
+ return this.toAnyBlobAllowPromise();
+ }
+
+ pub fn toAnyBlobAllowPromise(this: *PendingValue) ?AnyBlob {
+ var stream = if (this.readable != null) &this.readable.? else return null;
+
+ if (stream.toAnyBlob(this.global)) |blob| {
+ this.readable = null;
+ return blob;
+ }
+
+ return null;
+ }
+
+ pub fn setPromise(value: *PendingValue, globalThis: *JSC.JSGlobalObject, action: Action) JSValue {
+ value.action = action;
+
+ if (value.readable) |readable| {
+ // switch (readable.ptr) {
+ // .JavaScript
+ // }
+ switch (action) {
+ .getText, .getJSON, .getBlob, .getArrayBuffer => {
+ switch (readable.ptr) {
+ .Blob => unreachable,
+ else => {},
+ }
+ value.promise = switch (action) {
+ .getJSON => globalThis.readableStreamToJSON(readable.value),
+ .getArrayBuffer => globalThis.readableStreamToArrayBuffer(readable.value),
+ .getText => globalThis.readableStreamToText(readable.value),
+ .getBlob => globalThis.readableStreamToBlob(readable.value),
+ else => unreachable,
+ };
+ value.promise.?.ensureStillAlive();
+ readable.value.unprotect();
+
+ // js now owns the memory
+ value.readable = null;
+
+ return value.promise.?;
+ },
+ .none => {},
+ }
+ }
+
+ {
+ var promise = JSC.JSPromise.create(globalThis);
+ const promise_value = promise.asValue(globalThis);
+ value.promise = promise_value;
+
+ if (value.onStartBuffering) |onStartBuffering| {
+ value.onStartBuffering = null;
+ onStartBuffering(value.task.?);
+ }
+ return promise_value;
+ }
+ }
+
+ pub const Action = enum {
+ none,
+ getText,
+ getJSON,
+ getArrayBuffer,
+ getBlob,
+ };
+ };
+
+ /// This is a duplex stream!
+ pub const Value = union(Tag) {
+ Blob: Blob,
+ /// Single-use Blob
+ /// Avoids a heap allocation.
+ InternalBlob: InternalBlob,
+ /// Single-use Blob that stores the bytes in the Value itself.
+ // InlineBlob: InlineBlob,
+ Locked: PendingValue,
+ Used: void,
+ Empty: void,
+ Error: JSValue,
+
+ pub fn toBlobIfPossible(this: *Value) void {
+ if (this.* != .Locked)
+ return;
+
+ if (this.Locked.toAnyBlob()) |blob| {
+ this.* = switch (blob) {
+ .Blob => .{ .Blob = blob.Blob },
+ .InternalBlob => .{ .InternalBlob = blob.InternalBlob },
+ // .InlineBlob => .{ .InlineBlob = blob.InlineBlob },
+ };
+ }
+ }
+
+ pub fn size(this: *const Value) Blob.SizeType {
+ return switch (this.*) {
+ .Blob => this.Blob.size,
+ .InternalBlob => @truncate(Blob.SizeType, this.InternalBlob.sliceConst().len),
+ // .InlineBlob => @truncate(Blob.SizeType, this.InlineBlob.sliceConst().len),
+ else => 0,
+ };
+ }
+
+ pub fn estimatedSize(this: *const Value) usize {
+ return switch (this.*) {
+ .InternalBlob => this.InternalBlob.sliceConst().len,
+ // .InlineBlob => this.InlineBlob.sliceConst().len,
+ else => 0,
+ };
+ }
+
+ pub fn createBlobValue(data: []u8, allocator: std.mem.Allocator, was_string: bool) Value {
+ // if (data.len <= InlineBlob.available_bytes) {
+ // var _blob = InlineBlob{
+ // .bytes = undefined,
+ // .was_string = was_string,
+ // .len = @truncate(InlineBlob.IntSize, data.len),
+ // };
+ // @memcpy(&_blob.bytes, data.ptr, data.len);
+ // allocator.free(data);
+ // return Value{
+ // .InlineBlob = _blob,
+ // };
+ // }
+
+ return Value{
+ .InternalBlob = InternalBlob{
+ .bytes = std.ArrayList(u8).fromOwnedSlice(allocator, data),
+ .was_string = was_string,
+ },
+ };
+ }
+
+ pub const Tag = enum {
+ Blob,
+ InternalBlob,
+ // InlineBlob,
+ Locked,
+ Used,
+ Empty,
+ Error,
+ };
+
+ // pub const empty = Value{ .Empty = void{} };
+
+ pub fn toReadableStream(this: *Value, globalThis: *JSGlobalObject) JSValue {
+ JSC.markBinding(@src());
+
+ switch (this.*) {
+ .Used, .Empty => {
+ return JSC.WebCore.ReadableStream.empty(globalThis);
+ },
+ .InternalBlob,
+ .Blob,
+ // .InlineBlob,
+ => {
+ var blob = this.use();
+ defer blob.detach();
+ blob.resolveSize();
+ const value = JSC.WebCore.ReadableStream.fromBlob(globalThis, &blob, blob.size);
+
+ this.* = .{
+ .Locked = .{
+ .readable = JSC.WebCore.ReadableStream.fromJS(value, globalThis).?,
+ .global = globalThis,
+ },
+ };
+ this.Locked.readable.?.value.protect();
+
+ return value;
+ },
+ .Locked => {
+ var locked = &this.Locked;
+ if (locked.readable) |readable| {
+ return readable.value;
+ }
+ var drain_result: JSC.WebCore.DrainResult = .{
+ .estimated_size = 0,
+ };
+
+ if (locked.onStartStreaming) |drain| {
+ locked.onStartStreaming = null;
+ drain_result = drain(locked.task.?);
+ }
+
+ if (drain_result == .empty or drain_result == .aborted) {
+ this.* = .{ .Empty = void{} };
+ return JSC.WebCore.ReadableStream.empty(globalThis);
+ }
+
+ var reader = bun.default_allocator.create(JSC.WebCore.ByteStream.Source) catch unreachable;
+ reader.* = .{
+ .context = undefined,
+ .globalThis = globalThis,
+ };
+
+ reader.context.setup();
+
+ if (drain_result == .estimated_size) {
+ reader.context.highWaterMark = @truncate(Blob.SizeType, drain_result.estimated_size);
+ reader.context.size_hint = @truncate(Blob.SizeType, drain_result.estimated_size);
+ } else if (drain_result == .owned) {
+ reader.context.buffer = drain_result.owned.list;
+ reader.context.size_hint = @truncate(Blob.SizeType, drain_result.owned.size_hint);
+ }
+
+ locked.readable = .{
+ .ptr = .{ .Bytes = &reader.context },
+ .value = reader.toJS(globalThis),
+ };
+
+ locked.readable.?.value.protect();
+ return locked.readable.?.value;
+ },
+
+ else => unreachable,
+ }
+ }
+
+ pub fn fromJS(globalThis: *JSGlobalObject, value: JSValue) ?Value {
+ value.ensureStillAlive();
+
+ if (value.isEmptyOrUndefinedOrNull()) {
+ return Body.Value{
+ .Empty = void{},
+ };
+ }
+
+ const js_type = value.jsType();
+
+ if (js_type.isStringLike()) {
+ var str = value.getZigString(globalThis);
+ if (str.len == 0) {
+ return Body.Value{
+ .Empty = {},
+ };
+ }
+
+ // if (str.is16Bit()) {
+ // if (str.maxUTF8ByteLength() < InlineBlob.available_bytes or
+ // (str.len <= InlineBlob.available_bytes and str.utf8ByteLength() <= InlineBlob.available_bytes))
+ // {
+ // var blob = InlineBlob{
+ // .was_string = true,
+ // .bytes = undefined,
+ // .len = 0,
+ // };
+ // if (comptime Environment.allow_assert) {
+ // std.debug.assert(str.utf8ByteLength() <= InlineBlob.available_bytes);
+ // }
+
+ // const result = strings.copyUTF16IntoUTF8(
+ // blob.bytes[0..blob.bytes.len],
+ // []const u16,
+ // str.utf16SliceAligned(),
+ // );
+ // blob.len = @intCast(InlineBlob.IntSize, result.written);
+ // std.debug.assert(@as(usize, result.read) == str.len);
+ // std.debug.assert(@as(usize, result.written) <= InlineBlob.available_bytes);
+
+ // return Body.Value{
+ // .InlineBlob = blob,
+ // };
+ // }
+ // } else {
+ // if (str.maxUTF8ByteLength() <= InlineBlob.available_bytes or
+ // (str.len <= InlineBlob.available_bytes and str.utf8ByteLength() <= InlineBlob.available_bytes))
+ // {
+ // var blob = InlineBlob{
+ // .was_string = true,
+ // .bytes = undefined,
+ // .len = 0,
+ // };
+ // if (comptime Environment.allow_assert) {
+ // std.debug.assert(str.utf8ByteLength() <= InlineBlob.available_bytes);
+ // }
+ // const result = strings.copyLatin1IntoUTF8(
+ // blob.bytes[0..blob.bytes.len],
+ // []const u8,
+ // str.slice(),
+ // );
+ // blob.len = @intCast(InlineBlob.IntSize, result.written);
+ // std.debug.assert(@as(usize, result.read) == str.len);
+ // std.debug.assert(@as(usize, result.written) <= InlineBlob.available_bytes);
+ // return Body.Value{
+ // .InlineBlob = blob,
+ // };
+ // }
+ // }
+
+ var buffer = str.toOwnedSlice(bun.default_allocator) catch {
+ globalThis.vm().throwError(globalThis, ZigString.static("Failed to clone string").toErrorInstance(globalThis));
+ return null;
+ };
+
+ return Body.Value{
+ .InternalBlob = .{
+ .bytes = std.ArrayList(u8).fromOwnedSlice(bun.default_allocator, buffer),
+ .was_string = true,
+ },
+ };
+ }
+
+ if (js_type.isTypedArray()) {
+ if (value.asArrayBuffer(globalThis)) |buffer| {
+ var bytes = buffer.byteSlice();
+
+ if (bytes.len == 0) {
+ return Body.Value{
+ .Empty = {},
+ };
+ }
+
+ // if (bytes.len <= InlineBlob.available_bytes) {
+ // return Body.Value{
+ // .InlineBlob = InlineBlob.init(bytes),
+ // };
+ // }
+
+ return Body.Value{
+ .InternalBlob = .{
+ .bytes = std.ArrayList(u8){
+ .items = bun.default_allocator.dupe(u8, bytes) catch {
+ globalThis.vm().throwError(globalThis, ZigString.static("Failed to clone ArrayBufferView").toErrorInstance(globalThis));
+ return null;
+ },
+ .capacity = bytes.len,
+ .allocator = bun.default_allocator,
+ },
+ .was_string = false,
+ },
+ };
+ }
+ }
+
+ if (js_type == .DOMWrapper) {
+ if (value.as(Blob)) |blob| {
+ return Body.Value{
+ .Blob = blob.dupe(),
+ };
+ }
+ }
+
+ value.ensureStillAlive();
+
+ if (JSC.WebCore.ReadableStream.fromJS(value, globalThis)) |readable| {
+ switch (readable.ptr) {
+ .Blob => |blob| {
+ var result: Value = .{
+ .Blob = Blob.initWithStore(blob.store, globalThis),
+ };
+ blob.store.ref();
+
+ readable.done();
+
+ if (!blob.done) {
+ blob.done = true;
+ blob.deinit();
+ }
+ return result;
+ },
+ else => {},
+ }
+
+ return Body.Value.fromReadableStream(readable, globalThis);
+ }
+
+ return Body.Value{
+ .Blob = Blob.get(globalThis, value, true, false) catch |err| {
+ if (err == error.InvalidArguments) {
+ globalThis.throwInvalidArguments("Expected an Array", .{});
+ return null;
+ }
+
+ globalThis.throwInvalidArguments("Invalid Body object", .{});
+ return null;
+ },
+ };
+ }
+
+ pub fn fromReadableStream(readable: JSC.WebCore.ReadableStream, globalThis: *JSGlobalObject) Value {
+ if (readable.isLocked(globalThis)) {
+ return .{ .Error = ZigString.init("Cannot use a locked ReadableStream").toErrorInstance(globalThis) };
+ }
+
+ readable.value.protect();
+ return .{
+ .Locked = .{
+ .readable = readable,
+ .global = globalThis,
+ },
+ };
+ }
+
+ pub fn resolve(to_resolve: *Value, new: *Value, global: *JSGlobalObject) void {
+ if (to_resolve.* == .Locked) {
+ var locked = &to_resolve.Locked;
+ if (locked.readable) |readable| {
+ readable.done();
+ locked.readable = null;
+ }
+
+ if (locked.onReceiveValue) |callback| {
+ locked.onReceiveValue = null;
+ callback(locked.task.?, new);
+ return;
+ }
+
+ if (locked.promise) |promise_| {
+ const promise = promise_.asAnyPromise().?;
+ locked.promise = null;
+
+ switch (locked.action) {
+ .getText => {
+ switch (new.*) {
+ .InternalBlob,
+ // .InlineBlob,
+ => {
+ var blob = new.useAsAnyBlob();
+ promise.resolve(global, blob.toString(global, .transfer));
+ },
+ else => {
+ var blob = new.use();
+ promise.resolve(global, blob.toString(global, .transfer));
+ },
+ }
+ },
+ .getJSON => {
+ var blob = new.useAsAnyBlob();
+ const json_value = blob.toJSON(global, .share);
+ blob.detach();
+
+ if (json_value.isAnyError()) {
+ promise.reject(global, json_value);
+ } else {
+ promise.resolve(global, json_value);
+ }
+ },
+ .getArrayBuffer => {
+ var blob = new.useAsAnyBlob();
+ promise.resolve(global, blob.toArrayBuffer(global, .transfer));
+ },
+ else => {
+ var ptr = bun.default_allocator.create(Blob) catch unreachable;
+ ptr.* = new.use();
+ ptr.allocator = bun.default_allocator;
+ promise.resolve(global, ptr.toJS(global));
+ },
+ }
+ JSC.C.JSValueUnprotect(global, promise_.asObjectRef());
+ }
+ }
+ }
+ pub fn slice(this: *const Value) []const u8 {
+ return switch (this.*) {
+ .Blob => this.Blob.sharedView(),
+ .InternalBlob => this.InternalBlob.sliceConst(),
+ // .InlineBlob => this.InlineBlob.sliceConst(),
+ else => "",
+ };
+ }
+
+ pub fn use(this: *Value) Blob {
+ this.toBlobIfPossible();
+
+ switch (this.*) {
+ .Blob => {
+ var new_blob = this.Blob;
+ std.debug.assert(new_blob.allocator == null); // owned by Body
+ this.* = .{ .Used = {} };
+ return new_blob;
+ },
+ .InternalBlob => {
+ var new_blob = Blob.init(
+ this.InternalBlob.toOwnedSlice(),
+ // we will never resize it from here
+ // we have to use the default allocator
+ // even if it was actually allocated on a different thread
+ bun.default_allocator,
+ JSC.VirtualMachine.get().global,
+ );
+ if (this.InternalBlob.was_string) {
+ new_blob.content_type = MimeType.text.value;
+ }
+
+ this.* = .{ .Used = {} };
+ return new_blob;
+ },
+ // .InlineBlob => {
+ // const cloned = this.InlineBlob.bytes;
+ // const new_blob = Blob.create(
+ // cloned[0..this.InlineBlob.len],
+ // bun.default_allocator,
+ // JSC.VirtualMachine.get().global,
+ // this.InlineBlob.was_string,
+ // );
+
+ // this.* = .{ .Used = {} };
+ // return new_blob;
+ // },
+ else => {
+ return Blob.initEmpty(undefined);
+ },
+ }
+ }
+
+ pub fn tryUseAsAnyBlob(this: *Value) ?AnyBlob {
+ const any_blob: AnyBlob = switch (this.*) {
+ .Blob => AnyBlob{ .Blob = this.Blob },
+ .InternalBlob => AnyBlob{ .InternalBlob = this.InternalBlob },
+ // .InlineBlob => AnyBlob{ .InlineBlob = this.InlineBlob },
+ .Locked => this.Locked.toAnyBlobAllowPromise() orelse return null,
+ else => return null,
+ };
+
+ this.* = .{ .Used = {} };
+ return any_blob;
+ }
+
+ pub fn useAsAnyBlob(this: *Value) AnyBlob {
+ const any_blob: AnyBlob = switch (this.*) {
+ .Blob => .{ .Blob = this.Blob },
+ .InternalBlob => .{ .InternalBlob = this.InternalBlob },
+ // .InlineBlob => .{ .InlineBlob = this.InlineBlob },
+ .Locked => this.Locked.toAnyBlobAllowPromise() orelse AnyBlob{ .Blob = .{} },
+ else => .{ .Blob = Blob.initEmpty(undefined) },
+ };
+
+ this.* = .{ .Used = {} };
+ return any_blob;
+ }
+
+ pub fn toErrorInstance(this: *Value, error_instance: JSC.JSValue, global: *JSGlobalObject) void {
+ if (this.* == .Locked) {
+ var locked = this.Locked;
+ locked.deinit = true;
+ if (locked.promise) |promise| {
+ if (promise.asAnyPromise()) |internal| {
+ internal.reject(global, error_instance);
+ }
+ JSC.C.JSValueUnprotect(global, promise.asObjectRef());
+ locked.promise = null;
+ }
+
+ if (locked.readable) |readable| {
+ readable.done();
+ locked.readable = null;
+ }
+
+ this.* = .{ .Error = error_instance };
+ if (locked.onReceiveValue) |onReceiveValue| {
+ locked.onReceiveValue = null;
+ onReceiveValue(locked.task.?, this);
+ }
+ return;
+ }
+
+ this.* = .{ .Error = error_instance };
+ }
+
+ pub fn toErrorString(this: *Value, comptime err: string, global: *JSGlobalObject) void {
+ var error_str = ZigString.init(err);
+ var error_instance = error_str.toErrorInstance(global);
+ return this.toErrorInstance(error_instance, global);
+ }
+
+ pub fn toError(this: *Value, err: anyerror, global: *JSGlobalObject) void {
+ var error_str = ZigString.init(std.fmt.allocPrint(
+ bun.default_allocator,
+ "Error reading file {s}",
+ .{@errorName(err)},
+ ) catch unreachable);
+ error_str.mark();
+ var error_instance = error_str.toErrorInstance(global);
+ return this.toErrorInstance(error_instance, global);
+ }
+
+ pub fn deinit(this: *Value) void {
+ const tag = @as(Tag, this.*);
+ if (tag == .Locked) {
+ if (!this.Locked.deinit) {
+ this.Locked.deinit = true;
+
+ if (this.Locked.readable) |*readable| {
+ readable.done();
+ }
+ }
+
+ return;
+ }
+
+ if (tag == .InternalBlob) {
+ this.InternalBlob.clearAndFree();
+ this.* = Value{ .Empty = {} }; //Value.empty;
+ }
+
+ if (tag == .Blob) {
+ this.Blob.deinit();
+ this.* = Value{ .Empty = {} }; //Value.empty;
+ }
+
+ if (tag == .Error) {
+ JSC.C.JSValueUnprotect(VirtualMachine.get().global, this.Error.asObjectRef());
+ }
+ }
+
+ pub fn clone(this: *Value, globalThis: *JSC.JSGlobalObject) Value {
+ if (this.* == .InternalBlob) {
+ var internal_blob = this.InternalBlob;
+ this.* = .{
+ .Blob = Blob.init(
+ internal_blob.toOwnedSlice(),
+ internal_blob.bytes.allocator,
+ globalThis,
+ ),
+ };
+ }
+
+ // if (this.* == .InlineBlob) {
+ // return this.*;
+ // }
+
+ if (this.* == .Blob) {
+ return Value{ .Blob = this.Blob.dupe() };
+ }
+
+ return Value{ .Empty = {} };
+ }
+ };
+
+ pub fn @"404"(_: js.JSContextRef) Body {
+ return Body{
+ .init = Init{
+ .headers = null,
+ .status_code = 404,
+ },
+ .value = Value{ .Empty = {} }, //Value.empty,
+ };
+ }
+
+ pub fn @"200"(_: js.JSContextRef) Body {
+ return Body{
+ .init = Init{
+ .status_code = 200,
+ },
+ .value = Value{ .Empty = {} }, //Value.empty,
+ };
+ }
+
+ pub fn extract(
+ globalThis: *JSGlobalObject,
+ value: JSValue,
+ ) ?Body {
+ return extractBody(
+ globalThis,
+ value,
+ false,
+ JSValue.zero,
+ .Cell,
+ );
+ }
+
+ pub fn extractWithInit(
+ globalThis: *JSGlobalObject,
+ value: JSValue,
+ init: JSValue,
+ init_type: JSValue.JSType,
+ ) ?Body {
+ return extractBody(
+ globalThis,
+ value,
+ true,
+ init,
+ init_type,
+ );
+ }
+
+ // https://github.com/WebKit/webkit/blob/main/Source/WebCore/Modules/fetch/FetchBody.cpp#L45
+ inline fn extractBody(
+ globalThis: *JSGlobalObject,
+ value: JSValue,
+ comptime has_init: bool,
+ init: JSValue,
+ init_type: JSC.JSValue.JSType,
+ ) ?Body {
+ var body = Body{
+ .value = Value{ .Empty = {} },
+ .init = Init{ .headers = null, .status_code = 200 },
+ };
+ var allocator = getAllocator(globalThis);
+
+ if (comptime has_init) {
+ if (Init.init(allocator, globalThis, init, init_type)) |maybeInit| {
+ if (maybeInit) |init_| {
+ body.init = init_;
+ }
+ } else |_| {}
+ }
+
+ body.value = Value.fromJS(globalThis, value) orelse return null;
+ if (body.value == .Blob)
+ std.debug.assert(body.value.Blob.allocator == null); // owned by Body
+
+ return body;
+ }
+};
+
+pub fn BodyMixin(comptime Type: type) type {
+ return struct {
+ pub fn getText(
+ this: *Type,
+ globalThis: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ var value: *Body.Value = this.getBodyValue();
+ if (value.* == .Used) {
+ return handleBodyAlreadyUsed(globalThis);
+ }
+
+ if (value.* == .Locked) {
+ return value.Locked.setPromise(globalThis, .getText);
+ }
+
+ var blob = value.useAsAnyBlob();
+ return JSC.JSPromise.wrap(globalThis, blob.toString(globalThis, .transfer));
+ }
+
+ pub fn getBody(
+ this: *Type,
+ globalThis: *JSC.JSGlobalObject,
+ ) callconv(.C) JSValue {
+ var body: *Body.Value = this.getBodyValue();
+
+ if (body.* == .Used) {
+ // TODO: make this closed
+ return JSC.WebCore.ReadableStream.empty(globalThis);
+ }
+
+ return body.toReadableStream(globalThis);
+ }
+
+ pub fn getBodyUsed(
+ this: *Type,
+ _: *JSC.JSGlobalObject,
+ ) callconv(.C) JSValue {
+ return JSValue.jsBoolean(this.getBodyValue().* == .Used);
+ }
+
+ pub fn getJSON(
+ this: *Type,
+ globalObject: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ var value: *Body.Value = this.getBodyValue();
+ if (value.* == .Used) {
+ return handleBodyAlreadyUsed(globalObject);
+ }
+
+ if (value.* == .Locked) {
+ return value.Locked.setPromise(globalObject, .getJSON);
+ }
+
+ var blob = value.useAsAnyBlob();
+ return JSC.JSPromise.wrap(globalObject, blob.toJSON(globalObject, .share));
+ }
+
+ fn handleBodyAlreadyUsed(globalObject: *JSC.JSGlobalObject) JSValue {
+ return JSC.JSPromise.rejectedPromiseValue(
+ globalObject,
+ ZigString.static("Body already used").toErrorInstance(globalObject),
+ );
+ }
+
+ pub fn getArrayBuffer(
+ this: *Type,
+ globalObject: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ var value: *Body.Value = this.getBodyValue();
+
+ if (value.* == .Used) {
+ return handleBodyAlreadyUsed(globalObject);
+ }
+
+ if (value.* == .Locked) {
+ return value.Locked.setPromise(globalObject, .getArrayBuffer);
+ }
+
+ var blob: AnyBlob = value.useAsAnyBlob();
+ return JSC.JSPromise.wrap(globalObject, blob.toArrayBuffer(globalObject, .transfer));
+ }
+
+ pub fn getBlob(
+ this: *Type,
+ globalObject: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ var value: *Body.Value = this.getBodyValue();
+
+ if (value.* == .Used) {
+ return handleBodyAlreadyUsed(globalObject);
+ }
+
+ if (value.* == .Locked) {
+ return value.Locked.setPromise(globalObject, .getBlob);
+ }
+
+ var blob = value.use();
+ var ptr = getAllocator(globalObject).create(Blob) catch unreachable;
+ ptr.* = blob;
+ blob.allocator = getAllocator(globalObject);
+ return JSC.JSPromise.resolvedPromiseValue(globalObject, ptr.toJS(globalObject));
+ }
+ };
+}
diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig
index 19c370143..dda9c0bc4 100644
--- a/src/bun.js/webcore/encoding.zig
+++ b/src/bun.js/webcore/encoding.zig
@@ -35,7 +35,7 @@ const JSValue = JSC.JSValue;
const JSError = JSC.JSError;
const JSGlobalObject = JSC.JSGlobalObject;
-const VirtualMachine = @import("../javascript.zig").VirtualMachine;
+const VirtualMachine = JSC.VirtualMachine;
const Task = @import("../javascript.zig").Task;
const picohttp = @import("bun").picohttp;
diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig
new file mode 100644
index 000000000..2204a286a
--- /dev/null
+++ b/src/bun.js/webcore/request.zig
@@ -0,0 +1,454 @@
+const std = @import("std");
+const Api = @import("../../api/schema.zig").Api;
+const bun = @import("bun");
+const RequestContext = @import("../../http.zig").RequestContext;
+const MimeType = @import("../../http.zig").MimeType;
+const ZigURL = @import("../../url.zig").URL;
+const HTTPClient = @import("bun").HTTP;
+const NetworkThread = HTTPClient.NetworkThread;
+const AsyncIO = NetworkThread.AsyncIO;
+const JSC = @import("bun").JSC;
+const js = JSC.C;
+
+const Method = @import("../../http/method.zig").Method;
+const FetchHeaders = JSC.FetchHeaders;
+const ObjectPool = @import("../../pool.zig").ObjectPool;
+const SystemError = JSC.SystemError;
+const Output = @import("bun").Output;
+const MutableString = @import("bun").MutableString;
+const strings = @import("bun").strings;
+const string = @import("bun").string;
+const default_allocator = @import("bun").default_allocator;
+const FeatureFlags = @import("bun").FeatureFlags;
+const ArrayBuffer = @import("../base.zig").ArrayBuffer;
+const Properties = @import("../base.zig").Properties;
+const NewClass = @import("../base.zig").NewClass;
+const d = @import("../base.zig").d;
+const castObj = @import("../base.zig").castObj;
+const getAllocator = @import("../base.zig").getAllocator;
+const JSPrivateDataPtr = @import("../base.zig").JSPrivateDataPtr;
+const GetJSPrivateData = @import("../base.zig").GetJSPrivateData;
+const Environment = @import("../../env.zig");
+const ZigString = JSC.ZigString;
+const IdentityContext = @import("../../identity_context.zig").IdentityContext;
+const JSPromise = JSC.JSPromise;
+const JSValue = JSC.JSValue;
+const JSError = JSC.JSError;
+const JSGlobalObject = JSC.JSGlobalObject;
+const NullableAllocator = @import("../../nullable_allocator.zig").NullableAllocator;
+
+const VirtualMachine = JSC.VirtualMachine;
+const Task = JSC.Task;
+const JSPrinter = @import("../../js_printer.zig");
+const picohttp = @import("bun").picohttp;
+const StringJoiner = @import("../../string_joiner.zig");
+const uws = @import("bun").uws;
+
+const InlineBlob = JSC.WebCore.InlineBlob;
+const AnyBlob = JSC.WebCore.AnyBlob;
+const InternalBlob = JSC.WebCore.InternalBlob;
+const BodyMixin = JSC.WebCore.BodyMixin;
+const Body = JSC.WebCore.Body;
+const Blob = JSC.WebCore.Blob;
+
+// https://developer.mozilla.org/en-US/docs/Web/API/Request
+pub const Request = struct {
+ url: []const u8 = "",
+ url_was_allocated: bool = false,
+
+ headers: ?*FetchHeaders = null,
+ body: Body.Value = Body.Value{ .Empty = {} },
+ method: Method = Method.GET,
+ uws_request: ?*uws.Request = null,
+ https: bool = false,
+ upgrader: ?*anyopaque = null,
+
+ // We must report a consistent value for this
+ reported_estimated_size: ?u63 = null,
+
+ const RequestMixin = BodyMixin(@This());
+ pub usingnamespace JSC.Codegen.JSRequest;
+
+ pub const getText = RequestMixin.getText;
+ pub const getBody = RequestMixin.getBody;
+ pub const getBodyUsed = RequestMixin.getBodyUsed;
+ pub const getJSON = RequestMixin.getJSON;
+ pub const getArrayBuffer = RequestMixin.getArrayBuffer;
+ pub const getBlob = RequestMixin.getBlob;
+
+ pub fn estimatedSize(this: *Request) callconv(.C) usize {
+ return this.reported_estimated_size orelse brk: {
+ this.reported_estimated_size = @truncate(u63, this.body.estimatedSize() + this.sizeOfURL() + @sizeOf(Request));
+ break :brk this.reported_estimated_size.?;
+ };
+ }
+
+ pub fn writeFormat(this: *Request, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void {
+ const Writer = @TypeOf(writer);
+ try writer.print("Request ({}) {{\n", .{bun.fmt.size(this.body.slice().len)});
+ {
+ formatter.indent += 1;
+ defer formatter.indent -|= 1;
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("method: \"");
+ try writer.writeAll(std.mem.span(@tagName(this.method)));
+ try writer.writeAll("\"");
+ formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
+ try writer.writeAll("\n");
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("url: \"");
+ try this.ensureURL();
+ try writer.print(comptime Output.prettyFmt("<r><b>{s}<r>", enable_ansi_colors), .{this.url});
+
+ try writer.writeAll("\"");
+ if (this.body == .Blob) {
+ try writer.writeAll("\n");
+ try formatter.writeIndent(Writer, writer);
+ try this.body.Blob.writeFormat(formatter, writer, enable_ansi_colors);
+ } else if (this.body == .InternalBlob) {
+ try writer.writeAll("\n");
+ try formatter.writeIndent(Writer, writer);
+ if (this.body.size() == 0) {
+ try Blob.initEmpty(undefined).writeFormat(formatter, writer, enable_ansi_colors);
+ } else {
+ try Blob.writeFormatForSize(this.body.size(), writer, enable_ansi_colors);
+ }
+ } else if (this.body == .Locked) {
+ if (this.body.Locked.readable) |stream| {
+ try writer.writeAll("\n");
+ try formatter.writeIndent(Writer, writer);
+ formatter.printAs(.Object, Writer, writer, stream.value, stream.value.jsType(), enable_ansi_colors);
+ }
+ }
+ }
+ try writer.writeAll("\n");
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("}");
+ }
+
+ pub fn fromRequestContext(ctx: *RequestContext) !Request {
+ var req = Request{
+ .url = std.mem.span(ctx.getFullURL()),
+ .body = .{ .Empty = {} },
+ .method = ctx.method,
+ .headers = FetchHeaders.createFromPicoHeaders(ctx.request.headers),
+ .url_was_allocated = true,
+ };
+ return req;
+ }
+
+ pub fn mimeType(this: *const Request) string {
+ if (this.headers) |headers| {
+ if (headers.fastGet(.ContentType)) |content_type| {
+ return content_type.slice();
+ }
+ }
+
+ switch (this.body) {
+ .Blob => |blob| {
+ if (blob.content_type.len > 0) {
+ return blob.content_type;
+ }
+
+ return MimeType.other.value;
+ },
+ .InternalBlob => return this.body.InternalBlob.contentType(),
+ // .InlineBlob => return this.body.InlineBlob.contentType(),
+ .Error, .Used, .Locked, .Empty => return MimeType.other.value,
+ }
+ }
+
+ pub fn getCache(
+ _: *Request,
+ globalThis: *JSC.JSGlobalObject,
+ ) callconv(.C) JSC.JSValue {
+ return ZigString.init(Properties.UTF8.default).toValueGC(globalThis);
+ }
+ pub fn getCredentials(
+ _: *Request,
+ globalThis: *JSC.JSGlobalObject,
+ ) callconv(.C) JSC.JSValue {
+ return ZigString.init(Properties.UTF8.include).toValueGC(globalThis);
+ }
+ pub fn getDestination(
+ _: *Request,
+ globalThis: *JSC.JSGlobalObject,
+ ) callconv(.C) JSC.JSValue {
+ return ZigString.init("").toValueGC(globalThis);
+ }
+
+ pub fn getIntegrity(
+ _: *Request,
+ globalThis: *JSC.JSGlobalObject,
+ ) callconv(.C) JSC.JSValue {
+ return ZigString.Empty.toValueGC(globalThis);
+ }
+
+ pub fn getMethod(
+ this: *Request,
+ globalThis: *JSC.JSGlobalObject,
+ ) callconv(.C) JSC.JSValue {
+ const string_contents: string = switch (this.method) {
+ .GET => "GET",
+ .HEAD => "HEAD",
+ .PATCH => "PATCH",
+ .PUT => "PUT",
+ .POST => "POST",
+ .OPTIONS => "OPTIONS",
+ .CONNECT => "CONNECT",
+ .TRACE => "TRACE",
+ .DELETE => "DELETE",
+ };
+
+ return ZigString.init(string_contents).toValueGC(globalThis);
+ }
+
+ pub fn getMode(
+ _: *Request,
+ globalThis: *JSC.JSGlobalObject,
+ ) callconv(.C) JSC.JSValue {
+ return ZigString.init(Properties.UTF8.navigate).toValue(globalThis);
+ }
+
+ pub fn finalize(this: *Request) callconv(.C) void {
+ if (this.headers) |headers| {
+ headers.deref();
+ this.headers = null;
+ }
+
+ if (this.url_was_allocated) {
+ bun.default_allocator.free(bun.constStrToU8(this.url));
+ }
+
+ this.body.deinit();
+
+ bun.default_allocator.destroy(this);
+ }
+
+ pub fn getRedirect(
+ _: *Request,
+ globalThis: *JSC.JSGlobalObject,
+ ) callconv(.C) JSC.JSValue {
+ return ZigString.init(Properties.UTF8.follow).toValueGC(globalThis);
+ }
+ pub fn getReferrer(
+ this: *Request,
+ globalObject: *JSC.JSGlobalObject,
+ ) callconv(.C) JSC.JSValue {
+ if (this.headers) |headers_ref| {
+ if (headers_ref.get("referrer")) |referrer| {
+ return ZigString.init(referrer).toValueGC(globalObject);
+ }
+ }
+
+ return ZigString.init("").toValueGC(globalObject);
+ }
+ pub fn getReferrerPolicy(
+ _: *Request,
+ globalThis: *JSC.JSGlobalObject,
+ ) callconv(.C) JSC.JSValue {
+ return ZigString.init("").toValueGC(globalThis);
+ }
+ pub fn getUrl(
+ this: *Request,
+ globalObject: *JSC.JSGlobalObject,
+ ) callconv(.C) JSC.JSValue {
+ this.ensureURL() catch {
+ globalObject.throw("Failed to join URL", .{});
+ return .zero;
+ };
+
+ return ZigString.init(this.url).withEncoding().toValueGC(globalObject);
+ }
+
+ pub fn sizeOfURL(this: *const Request) usize {
+ if (this.url.len > 0)
+ return this.url.len;
+
+ if (this.uws_request) |req| {
+ const fmt = ZigURL.HostFormatter{
+ .is_https = this.https,
+ .host = req.header("host") orelse "",
+ };
+
+ return this.getProtocol().len + req.url().len + std.fmt.count("{any}", .{fmt});
+ }
+
+ return 0;
+ }
+
+ pub fn getProtocol(this: *const Request) []const u8 {
+ if (this.https)
+ return "https://";
+
+ return "http://";
+ }
+
+ pub fn ensureURL(this: *Request) !void {
+ if (this.url.len > 0) return;
+
+ if (this.uws_request) |req| {
+ const req_url = req.url();
+ if (req.header("host")) |host| {
+ const fmt = ZigURL.HostFormatter{
+ .is_https = this.https,
+ .host = host,
+ };
+ const url = try std.fmt.allocPrint(bun.default_allocator, "{s}{any}{s}", .{
+ this.getProtocol(),
+ fmt,
+ req_url,
+ });
+ if (comptime Environment.allow_assert) {
+ std.debug.assert(this.sizeOfURL() == url.len);
+ }
+ this.url = url;
+ this.url_was_allocated = true;
+ } else {
+ if (comptime Environment.allow_assert) {
+ std.debug.assert(this.sizeOfURL() == req_url.len);
+ }
+ this.url = try bun.default_allocator.dupe(u8, req_url);
+ this.url_was_allocated = true;
+ }
+ }
+ }
+
+ pub fn constructInto(
+ globalThis: *JSC.JSGlobalObject,
+ arguments: []const JSC.JSValue,
+ ) ?Request {
+ var request = Request{};
+
+ switch (arguments.len) {
+ 0 => {},
+ 1 => {
+ const urlOrObject = arguments[0];
+ const url_or_object_type = urlOrObject.jsType();
+ if (url_or_object_type.isStringLike()) {
+ request.url = (arguments[0].toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch {
+ return null;
+ }).slice();
+ request.url_was_allocated = request.url.len > 0;
+ } else {
+ if (urlOrObject.fastGet(globalThis, .body)) |body_| {
+ if (Body.Value.fromJS(globalThis, body_)) |body| {
+ request.body = body;
+ } else {
+ return null;
+ }
+ }
+
+ if (Body.Init.init(getAllocator(globalThis), globalThis, arguments[0], url_or_object_type) catch null) |req_init| {
+ request.headers = req_init.headers;
+ request.method = req_init.method;
+ }
+
+ if (urlOrObject.fastGet(globalThis, .url)) |url| {
+ request.url = (url.toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch {
+ return null;
+ }).slice();
+ request.url_was_allocated = request.url.len > 0;
+ }
+ }
+ },
+ else => {
+ if (arguments[1].fastGet(globalThis, .body)) |body_| {
+ if (Body.Value.fromJS(globalThis, body_)) |body| {
+ request.body = body;
+ } else {
+ return null;
+ }
+ }
+
+ if (Body.Init.init(getAllocator(globalThis), globalThis, arguments[1], arguments[1].jsType()) catch null) |req_init| {
+ request.headers = req_init.headers;
+ request.method = req_init.method;
+ }
+
+ request.url = (arguments[0].toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch {
+ return null;
+ }).slice();
+ request.url_was_allocated = request.url.len > 0;
+ },
+ }
+
+ return request;
+ }
+
+ pub fn constructor(
+ globalThis: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) ?*Request {
+ const arguments_ = callframe.arguments(2);
+ const arguments = arguments_.ptr[0..arguments_.len];
+
+ const request = constructInto(globalThis, arguments) orelse return null;
+ var request_ = getAllocator(globalThis).create(Request) catch return null;
+ request_.* = request;
+ return request_;
+ }
+
+ pub fn getBodyValue(
+ this: *Request,
+ ) *Body.Value {
+ return &this.body;
+ }
+
+ pub fn doClone(
+ this: *Request,
+ globalThis: *JSC.JSGlobalObject,
+ _: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ var cloned = this.clone(getAllocator(globalThis), globalThis);
+ return cloned.toJS(globalThis);
+ }
+
+ pub fn getHeaders(
+ this: *Request,
+ globalThis: *JSC.JSGlobalObject,
+ ) callconv(.C) JSC.JSValue {
+ if (this.headers == null) {
+ if (this.uws_request) |req| {
+ this.headers = FetchHeaders.createFromUWS(globalThis, req);
+ } else {
+ this.headers = FetchHeaders.createEmpty();
+ }
+ }
+
+ return this.headers.?.toJS(globalThis);
+ }
+
+ pub fn cloneInto(
+ this: *Request,
+ req: *Request,
+ allocator: std.mem.Allocator,
+ globalThis: *JSGlobalObject,
+ ) void {
+ this.ensureURL() catch {};
+
+ req.* = Request{
+ .body = this.body.clone(globalThis),
+ .url = allocator.dupe(u8, this.url) catch {
+ globalThis.throw("Failed to clone request", .{});
+ return;
+ },
+ .method = this.method,
+ };
+
+ if (this.headers) |head| {
+ req.headers = head.cloneThis();
+ } else if (this.uws_request) |uws_req| {
+ req.headers = FetchHeaders.createFromUWS(globalThis, uws_req);
+ this.headers = req.headers.?.cloneThis().?;
+ }
+ }
+
+ pub fn clone(this: *Request, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) *Request {
+ var req = allocator.create(Request) catch unreachable;
+ this.cloneInto(req, allocator, globalThis);
+ return req;
+ }
+};
diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig
index 7ee20fdd5..917cdbb0a 100644
--- a/src/bun.js/webcore/response.zig
+++ b/src/bun.js/webcore/response.zig
@@ -37,13 +37,21 @@ const JSError = JSC.JSError;
const JSGlobalObject = JSC.JSGlobalObject;
const NullableAllocator = @import("../../nullable_allocator.zig").NullableAllocator;
-const VirtualMachine = @import("../javascript.zig").VirtualMachine;
+const VirtualMachine = JSC.VirtualMachine;
const Task = JSC.Task;
const JSPrinter = @import("../../js_printer.zig");
const picohttp = @import("bun").picohttp;
const StringJoiner = @import("../../string_joiner.zig");
const uws = @import("bun").uws;
+const InlineBlob = JSC.WebCore.InlineBlob;
+const AnyBlob = JSC.WebCore.AnyBlob;
+const InternalBlob = JSC.WebCore.InternalBlob;
+const BodyMixin = JSC.WebCore.BodyMixin;
+const Body = JSC.WebCore.Body;
+const Request = JSC.WebCore.Request;
+const Blob = JSC.WebCore.Blob;
+
pub const Response = struct {
const ResponseMixin = BodyMixin(@This());
pub usingnamespace JSC.Codegen.JSResponse;
@@ -970,4726 +978,6 @@ pub const Headers = struct {
}
};
-const PathOrBlob = union(enum) {
- path: JSC.Node.PathOrFileDescriptor,
- blob: Blob,
-
- pub fn fromJSNoCopy(ctx: js.JSContextRef, args: *JSC.Node.ArgumentsSlice, exception: js.ExceptionRef) ?PathOrBlob {
- if (JSC.Node.PathOrFileDescriptor.fromJS(ctx, args, args.arena.allocator(), exception)) |path| {
- return PathOrBlob{
- .path = path,
- };
- }
-
- const arg = args.nextEat() orelse return null;
-
- if (arg.as(Blob)) |blob| {
- return PathOrBlob{
- .blob = blob.*,
- };
- }
-
- return null;
- }
-};
-
-pub const Blob = struct {
- pub usingnamespace JSC.Codegen.JSBlob;
-
- size: SizeType = 0,
- offset: SizeType = 0,
- /// When set, the blob will be freed on finalization callbacks
- /// If the blob is contained in Response or Request, this must be null
- allocator: ?std.mem.Allocator = null,
- store: ?*Store = null,
- content_type: string = "",
- content_type_allocated: bool = false,
-
- /// JavaScriptCore strings are either latin1 or UTF-16
- /// When UTF-16, they're nearly always due to non-ascii characters
- is_all_ascii: ?bool = null,
-
- globalThis: *JSGlobalObject = undefined,
-
- /// Max int of double precision
- /// 9 petabytes is probably enough for awhile
- /// We want to avoid coercing to a BigInt because that's a heap allocation
- /// and it's generally just harder to use
- pub const SizeType = u52;
- pub const max_size = std.math.maxInt(SizeType);
-
- pub fn contentType(this: *const Blob) string {
- return this.content_type;
- }
-
- pub fn isDetached(this: *const Blob) bool {
- return this.store == null;
- }
-
- pub fn writeFormatForSize(size: usize, writer: anytype, comptime enable_ansi_colors: bool) !void {
- try writer.writeAll(comptime Output.prettyFmt("<r>Blob<r>", enable_ansi_colors));
- try writer.print(
- comptime Output.prettyFmt(" (<yellow>{any}<r>)", enable_ansi_colors),
- .{
- bun.fmt.size(size),
- },
- );
- }
- pub fn writeFormat(this: *const Blob, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void {
- const Writer = @TypeOf(writer);
-
- if (this.isDetached()) {
- try writer.writeAll(comptime Output.prettyFmt("<d>[<r>Blob<r> detached<d>]<r>", enable_ansi_colors));
- return;
- }
-
- {
- var store = this.store.?;
- switch (store.data) {
- .file => |file| {
- try writer.writeAll(comptime Output.prettyFmt("<r>FileRef<r>", enable_ansi_colors));
- switch (file.pathlike) {
- .path => |path| {
- try writer.print(
- comptime Output.prettyFmt(" (<green>\"{s}\"<r>)<r>", enable_ansi_colors),
- .{
- path.slice(),
- },
- );
- },
- .fd => |fd| {
- try writer.print(
- comptime Output.prettyFmt(" (<r>fd: <yellow>{d}<r>)<r>", enable_ansi_colors),
- .{
- fd,
- },
- );
- },
- }
- },
- .bytes => {
- try writeFormatForSize(this.size, writer, enable_ansi_colors);
- },
- }
- }
-
- if (this.content_type.len > 0 or this.offset > 0) {
- try writer.writeAll(" {\n");
- {
- formatter.indent += 1;
- defer formatter.indent -= 1;
-
- if (this.content_type.len > 0) {
- try formatter.writeIndent(Writer, writer);
- try writer.print(
- comptime Output.prettyFmt("type: <green>\"{s}\"<r>", enable_ansi_colors),
- .{
- this.content_type,
- },
- );
-
- if (this.offset > 0) {
- formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
- }
-
- try writer.writeAll("\n");
- }
-
- if (this.offset > 0) {
- try formatter.writeIndent(Writer, writer);
-
- try writer.print(
- comptime Output.prettyFmt("offset: <yellow>{d}<r>\n", enable_ansi_colors),
- .{
- this.offset,
- },
- );
- }
- }
-
- try formatter.writeIndent(Writer, writer);
- try writer.writeAll("}");
- }
- }
-
- const CopyFilePromiseHandler = struct {
- promise: *JSPromise,
- globalThis: *JSGlobalObject,
- pub fn run(handler: *@This(), blob_: Store.CopyFile.ResultType) void {
- var promise = handler.promise;
- var globalThis = handler.globalThis;
- bun.default_allocator.destroy(handler);
- var blob = blob_ catch |err| {
- var error_string = ZigString.init(
- std.fmt.allocPrint(bun.default_allocator, "Failed to write file \"{s}\"", .{std.mem.span(@errorName(err))}) catch unreachable,
- );
- error_string.mark();
-
- promise.reject(globalThis, error_string.toErrorInstance(globalThis));
- return;
- };
- var _blob = bun.default_allocator.create(Blob) catch unreachable;
- _blob.* = blob;
- _blob.allocator = bun.default_allocator;
- promise.resolve(
- globalThis,
- );
- }
- };
-
- const WriteFileWaitFromLockedValueTask = struct {
- file_blob: Blob,
- globalThis: *JSGlobalObject,
- promise: *JSPromise,
-
- pub fn thenWrap(this: *anyopaque, value: *Body.Value) void {
- then(bun.cast(*WriteFileWaitFromLockedValueTask, this), value);
- }
-
- pub fn then(this: *WriteFileWaitFromLockedValueTask, value: *Body.Value) void {
- var promise = this.promise;
- var globalThis = this.globalThis;
- var file_blob = this.file_blob;
- switch (value.*) {
- .Error => |err| {
- file_blob.detach();
- _ = value.use();
- bun.default_allocator.destroy(this);
- promise.reject(globalThis, err);
- },
- .Used => {
- file_blob.detach();
- _ = value.use();
- bun.default_allocator.destroy(this);
- promise.reject(globalThis, ZigString.init("Body was used after it was consumed").toErrorInstance(globalThis));
- },
- // .InlineBlob,
- .InternalBlob,
- .Empty,
- .Blob,
- => {
- var blob = value.use();
- // TODO: this should be one promise not two!
- const new_promise = writeFileWithSourceDestination(globalThis, &blob, &file_blob);
- if (JSC.JSValue.fromRef(new_promise.?).asAnyPromise()) |_promise| {
- switch (_promise.status(globalThis.vm())) {
- .Pending => {
- promise.resolve(
- globalThis,
- JSC.JSValue.fromRef(new_promise.?),
- );
- },
- .Rejected => {
- promise.reject(globalThis, _promise.result(globalThis.vm()));
- },
- else => {
- promise.resolve(globalThis, _promise.result(globalThis.vm()));
- },
- }
- }
-
- file_blob.detach();
- bun.default_allocator.destroy(this);
- },
- .Locked => {
- value.Locked.onReceiveValue = thenWrap;
- value.Locked.task = this;
- },
- }
- }
- };
-
- pub fn writeFileWithSourceDestination(
- ctx: JSC.C.JSContextRef,
- source_blob: *Blob,
- destination_blob: *Blob,
- ) js.JSObjectRef {
- const destination_type = std.meta.activeTag(destination_blob.store.?.data);
-
- // Writing an empty string to a file is a no-op
- if (source_blob.store == null) {
- destination_blob.detach();
- return JSC.JSPromise.resolvedPromiseValue(ctx.ptr(), JSC.JSValue.jsNumber(0)).asObjectRef();
- }
-
- const source_type = std.meta.activeTag(source_blob.store.?.data);
-
- if (destination_type == .file and source_type == .bytes) {
- var write_file_promise = bun.default_allocator.create(WriteFilePromise) catch unreachable;
- var promise = JSC.JSPromise.create(ctx.ptr());
- const promise_value = promise.asValue(ctx);
- write_file_promise.* = .{
- .globalThis = ctx.ptr(),
- };
- write_file_promise.promise.strong.set(ctx, promise_value);
- promise_value.ensureStillAlive();
-
- var file_copier = Store.WriteFile.create(
- bun.default_allocator,
- destination_blob.*,
- source_blob.*,
- *WriteFilePromise,
- write_file_promise,
- WriteFilePromise.run,
- ) catch unreachable;
- var task = Store.WriteFile.WriteFileTask.createOnJSThread(bun.default_allocator, ctx.ptr(), file_copier) catch unreachable;
- task.schedule();
- return promise_value.asObjectRef();
- }
- // If this is file <> file, we can just copy the file
- else if (destination_type == .file and source_type == .file) {
- var file_copier = Store.CopyFile.create(
- bun.default_allocator,
- destination_blob.store.?,
- source_blob.store.?,
-
- destination_blob.offset,
- destination_blob.size,
- ctx.ptr(),
- ) catch unreachable;
- file_copier.schedule();
- return file_copier.promise.value().asObjectRef();
- } else if (destination_type == .bytes and source_type == .bytes) {
- // If this is bytes <> bytes, we can just duplicate it
- // this is an edgecase
- // it will happen if someone did Bun.write(new Blob([123]), new Blob([456]))
- // eventually, this could be like Buffer.concat
- var clone = source_blob.dupe();
- clone.allocator = bun.default_allocator;
- var cloned = bun.default_allocator.create(Blob) catch unreachable;
- cloned.* = clone;
- return JSPromise.resolvedPromiseValue(ctx.ptr(), cloned.toJS(ctx)).asObjectRef();
- } else if (destination_type == .bytes and source_type == .file) {
- var fake_call_frame: [8]JSC.JSValue = undefined;
- @memset(@ptrCast([*]u8, &fake_call_frame), 0, @sizeOf(@TypeOf(fake_call_frame)));
- const blob_value =
- source_blob.getSlice(ctx, @ptrCast(*JSC.CallFrame, &fake_call_frame));
-
- return JSPromise.resolvedPromiseValue(
- ctx.ptr(),
- blob_value,
- ).asObjectRef();
- }
-
- unreachable;
- }
- pub fn writeFile(
- _: void,
- ctx: js.JSContextRef,
- _: js.JSObjectRef,
- _: js.JSObjectRef,
- arguments: []const js.JSValueRef,
- exception: js.ExceptionRef,
- ) js.JSObjectRef {
- var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments);
- defer args.deinit();
- // accept a path or a blob
- var path_or_blob = PathOrBlob.fromJSNoCopy(ctx, &args, exception) orelse {
- exception.* = JSC.toInvalidArguments("Bun.write expects a path, file descriptor or a blob", .{}, ctx).asObjectRef();
- return null;
- };
-
- var data = args.nextEat() orelse {
- exception.* = JSC.toInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}, ctx).asObjectRef();
- return null;
- };
-
- if (data.isEmptyOrUndefinedOrNull()) {
- exception.* = JSC.toInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}, ctx).asObjectRef();
- return null;
- }
-
- if (path_or_blob == .blob and path_or_blob.blob.store == null) {
- exception.* = JSC.toInvalidArguments("Blob is detached", .{}, ctx).asObjectRef();
- return null;
- }
-
- var needs_async = false;
- if (data.isString()) {
- const len = data.getLengthOfArray(ctx);
-
- if (len < 256 * 1024 or bun.isMissingIOUring()) {
- const str = data.getZigString(ctx);
-
- const pathlike: JSC.Node.PathOrFileDescriptor = if (path_or_blob == .path)
- path_or_blob.path
- else
- path_or_blob.blob.store.?.data.file.pathlike;
-
- if (pathlike == .path) {
- const result = writeStringToFileFast(
- ctx,
- pathlike,
- str,
- &needs_async,
- true,
- );
- if (!needs_async) {
- return result.asObjectRef();
- }
- } else {
- const result = writeStringToFileFast(
- ctx,
- pathlike,
- str,
- &needs_async,
- false,
- );
- if (!needs_async) {
- return result.asObjectRef();
- }
- }
- }
- } else if (data.asArrayBuffer(ctx)) |buffer_view| {
- if (buffer_view.byte_len < 256 * 1024 or bun.isMissingIOUring()) {
- const pathlike: JSC.Node.PathOrFileDescriptor = if (path_or_blob == .path)
- path_or_blob.path
- else
- path_or_blob.blob.store.?.data.file.pathlike;
-
- if (pathlike == .path) {
- const result = writeBytesToFileFast(
- ctx,
- pathlike,
- buffer_view.byteSlice(),
- &needs_async,
- true,
- );
-
- if (!needs_async) {
- return result.asObjectRef();
- }
- } else {
- const result = writeBytesToFileFast(
- ctx,
- pathlike,
- buffer_view.byteSlice(),
- &needs_async,
- false,
- );
-
- if (!needs_async) {
- return result.asObjectRef();
- }
- }
- }
- }
-
- // if path_or_blob is a path, convert it into a file blob
- var destination_blob: Blob = if (path_or_blob == .path)
- Blob.findOrCreateFileFromPath(path_or_blob.path, ctx.ptr())
- else
- path_or_blob.blob.dupe();
-
- if (destination_blob.store == null) {
- exception.* = JSC.toInvalidArguments("Writing to an empty blob is not implemented yet", .{}, ctx).asObjectRef();
- return null;
- }
-
- // TODO: implement a writeev() fast path
- var source_blob: Blob = brk: {
- if (data.as(Response)) |response| {
- switch (response.body.value) {
- // .InlineBlob,
- .InternalBlob,
- .Used,
- .Empty,
- .Blob,
- => {
- break :brk response.body.use();
- },
- .Error => {
- destination_blob.detach();
- const err = response.body.value.Error;
- JSC.C.JSValueUnprotect(ctx, err.asObjectRef());
- _ = response.body.value.use();
- return JSC.JSPromise.rejectedPromiseValue(ctx.ptr(), err).asObjectRef();
- },
- .Locked => {
- var task = bun.default_allocator.create(WriteFileWaitFromLockedValueTask) catch unreachable;
- var promise = JSC.JSPromise.create(ctx.ptr());
- task.* = WriteFileWaitFromLockedValueTask{
- .globalThis = ctx.ptr(),
- .file_blob = destination_blob,
- .promise = promise,
- };
-
- response.body.value.Locked.task = task;
- response.body.value.Locked.onReceiveValue = WriteFileWaitFromLockedValueTask.thenWrap;
-
- return promise.asValue(ctx.ptr()).asObjectRef();
- },
- }
- }
-
- if (data.as(Request)) |request| {
- switch (request.body) {
- // .InlineBlob,
- .InternalBlob,
- .Used,
- .Empty,
- .Blob,
- => {
- break :brk request.body.use();
- },
- .Error => {
- destination_blob.detach();
- const err = request.body.Error;
- JSC.C.JSValueUnprotect(ctx, err.asObjectRef());
- _ = request.body.use();
- return JSC.JSPromise.rejectedPromiseValue(ctx.ptr(), err).asObjectRef();
- },
- .Locked => {
- var task = bun.default_allocator.create(WriteFileWaitFromLockedValueTask) catch unreachable;
- var promise = JSC.JSPromise.create(ctx.ptr());
- task.* = WriteFileWaitFromLockedValueTask{
- .globalThis = ctx.ptr(),
- .file_blob = destination_blob,
- .promise = promise,
- };
-
- request.body.Locked.task = task;
- request.body.Locked.onReceiveValue = WriteFileWaitFromLockedValueTask.thenWrap;
-
- return promise.asValue(ctx.ptr()).asObjectRef();
- },
- }
- }
-
- break :brk Blob.get(
- ctx.ptr(),
- data,
- false,
- false,
- ) catch |err| {
- if (err == error.InvalidArguments) {
- exception.* = JSC.toInvalidArguments(
- "Expected an Array",
- .{},
- ctx,
- ).asObjectRef();
- return null;
- }
-
- exception.* = JSC.toInvalidArguments(
- "Out of memory",
- .{},
- ctx,
- ).asObjectRef();
- return null;
- };
- };
-
- return writeFileWithSourceDestination(ctx, &source_blob, &destination_blob);
- }
-
- const write_permissions = 0o664;
-
- fn writeStringToFileFast(
- globalThis: *JSC.JSGlobalObject,
- pathlike: JSC.Node.PathOrFileDescriptor,
- str: ZigString,
- needs_async: *bool,
- comptime needs_open: bool,
- ) JSC.JSValue {
- const fd: bun.FileDescriptor = if (comptime !needs_open) pathlike.fd else brk: {
- var file_path: [bun.MAX_PATH_BYTES]u8 = undefined;
- switch (JSC.Node.Syscall.open(
- pathlike.path.sliceZ(&file_path),
- // we deliberately don't use O_TRUNC here
- // it's a perf optimization
- std.os.O.WRONLY | std.os.O.CREAT | std.os.O.NONBLOCK,
- write_permissions,
- )) {
- .result => |result| {
- break :brk result;
- },
- .err => |err| {
- return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
- },
- }
- unreachable;
- };
-
- var truncate = needs_open or str.len == 0;
- var jsc_vm = globalThis.bunVM();
- var written: usize = 0;
-
- defer {
- // we only truncate if it's a path
- // if it's a file descriptor, we assume they want manual control over that behavior
- if (truncate) {
- _ = JSC.Node.Syscall.system.ftruncate(fd, @intCast(i64, written));
- }
-
- if (needs_open) {
- _ = JSC.Node.Syscall.close(fd);
- }
- }
- if (str.len == 0) {} else if (str.is16Bit()) {
- var decoded = str.toSlice(jsc_vm.allocator);
- defer decoded.deinit();
-
- var remain = decoded.slice();
- const end = remain.ptr + remain.len;
-
- while (remain.ptr != end) {
- const result = JSC.Node.Syscall.write(fd, remain);
- switch (result) {
- .result => |res| {
- written += res;
- remain = remain[res..];
- if (res == 0) break;
- },
- .err => |err| {
- truncate = false;
- if (err.getErrno() == .AGAIN) {
- needs_async.* = true;
- return .zero;
- }
- return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
- },
- }
- }
- } else if (str.isUTF8() or strings.isAllASCII(str.slice())) {
- var remain = str.slice();
- const end = remain.ptr + remain.len;
-
- while (remain.ptr != end) {
- const result = JSC.Node.Syscall.write(fd, remain);
- switch (result) {
- .result => |res| {
- written += res;
- remain = remain[res..];
- if (res == 0) break;
- },
- .err => |err| {
- truncate = false;
- if (err.getErrno() == .AGAIN) {
- needs_async.* = true;
- return .zero;
- }
-
- return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
- },
- }
- }
- } else {
- var decoded = str.toOwnedSlice(jsc_vm.allocator) catch {
- return JSC.JSPromise.rejectedPromiseValue(globalThis, ZigString.static("Out of memory").toErrorInstance(globalThis));
- };
- defer jsc_vm.allocator.free(decoded);
- var remain = decoded;
- const end = remain.ptr + remain.len;
- while (remain.ptr != end) {
- const result = JSC.Node.Syscall.write(fd, remain);
- switch (result) {
- .result => |res| {
- written += res;
- remain = remain[res..];
- if (res == 0) break;
- },
- .err => |err| {
- truncate = false;
- if (err.getErrno() == .AGAIN) {
- needs_async.* = true;
- return .zero;
- }
-
- return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
- },
- }
- }
- }
-
- return JSC.JSPromise.resolvedPromiseValue(globalThis, JSC.JSValue.jsNumber(written));
- }
-
- fn writeBytesToFileFast(
- globalThis: *JSC.JSGlobalObject,
- pathlike: JSC.Node.PathOrFileDescriptor,
- bytes: []const u8,
- needs_async: *bool,
- comptime needs_open: bool,
- ) JSC.JSValue {
- const fd: bun.FileDescriptor = if (comptime !needs_open) pathlike.fd else brk: {
- var file_path: [bun.MAX_PATH_BYTES]u8 = undefined;
- switch (JSC.Node.Syscall.open(
- pathlike.path.sliceZ(&file_path),
- // we deliberately don't use O_TRUNC here
- // it's a perf optimization
- std.os.O.WRONLY | std.os.O.CREAT | std.os.O.NONBLOCK,
- write_permissions,
- )) {
- .result => |result| {
- break :brk result;
- },
- .err => |err| {
- return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
- },
- }
- unreachable;
- };
-
- var truncate = needs_open or bytes.len == 0;
- var written: usize = 0;
- defer {
- if (truncate) {
- _ = JSC.Node.Syscall.system.ftruncate(fd, @intCast(i64, written));
- }
-
- if (needs_open) {
- _ = JSC.Node.Syscall.close(fd);
- }
- }
-
- var remain = bytes;
- const end = remain.ptr + remain.len;
-
- while (remain.ptr != end) {
- const result = JSC.Node.Syscall.write(fd, remain);
- switch (result) {
- .result => |res| {
- written += res;
- remain = remain[res..];
- if (res == 0) break;
- },
- .err => |err| {
- truncate = false;
- if (err.getErrno() == .AGAIN) {
- needs_async.* = true;
- return .zero;
- }
- return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
- },
- }
- }
-
- return JSC.JSPromise.resolvedPromiseValue(globalThis, JSC.JSValue.jsNumber(written));
- }
-
- pub fn constructFile(
- _: void,
- ctx: js.JSContextRef,
- _: js.JSObjectRef,
- _: js.JSObjectRef,
- arguments: []const js.JSValueRef,
- exception: js.ExceptionRef,
- ) js.JSObjectRef {
- var vm = ctx.bunVM();
- var args = JSC.Node.ArgumentsSlice.from(vm, arguments);
- defer args.deinit();
-
- const path = JSC.Node.PathOrFileDescriptor.fromJS(ctx, &args, args.arena.allocator(), exception) orelse {
- exception.* = JSC.toInvalidArguments("Expected file path string or file descriptor", .{}, ctx).asObjectRef();
- return js.JSValueMakeUndefined(ctx);
- };
-
- const blob = Blob.findOrCreateFileFromPath(path, ctx.ptr());
-
- var ptr = vm.allocator.create(Blob) catch unreachable;
- ptr.* = blob;
- ptr.allocator = vm.allocator;
- return ptr.toJS(ctx).asObjectRef();
- }
-
- pub fn findOrCreateFileFromPath(path_: JSC.Node.PathOrFileDescriptor, globalThis: *JSGlobalObject) Blob {
- var vm = globalThis.bunVM();
- const allocator = vm.allocator;
-
- const path: JSC.Node.PathOrFileDescriptor = brk: {
- switch (path_) {
- .path => {
- const slice = path_.path.slice();
- var cloned = (allocator.dupeZ(u8, slice) catch unreachable)[0..slice.len];
-
- break :brk .{
- .path = .{
- .string = bun.PathString.init(cloned),
- },
- };
- },
- .fd => {
- switch (path_.fd) {
- std.os.STDIN_FILENO => return Blob.initWithStore(
- vm.rareData().stdin(),
- globalThis,
- ),
- std.os.STDERR_FILENO => return Blob.initWithStore(
- vm.rareData().stderr(),
- globalThis,
- ),
- std.os.STDOUT_FILENO => return Blob.initWithStore(
- vm.rareData().stdout(),
- globalThis,
- ),
- else => {},
- }
- break :brk path_;
- },
- }
- };
-
- return Blob.initWithStore(Blob.Store.initFile(path, null, allocator) catch unreachable, globalThis);
- }
-
- pub const Store = struct {
- data: Data,
-
- mime_type: MimeType = MimeType.other,
- ref_count: u32 = 0,
- is_all_ascii: ?bool = null,
- allocator: std.mem.Allocator,
-
- pub fn size(this: *const Store) SizeType {
- return switch (this.data) {
- .bytes => this.data.bytes.len,
- .file => Blob.max_size,
- };
- }
-
- pub const Map = std.HashMap(u64, *JSC.WebCore.Blob.Store, IdentityContext(u64), 80);
-
- pub const Data = union(enum) {
- bytes: ByteStore,
- file: FileStore,
- };
-
- pub fn ref(this: *Store) void {
- std.debug.assert(this.ref_count > 0);
- this.ref_count += 1;
- }
-
- pub fn external(ptr: ?*anyopaque, _: ?*anyopaque, _: usize) callconv(.C) void {
- if (ptr == null) return;
- var this = bun.cast(*Store, ptr);
- this.deref();
- }
-
- pub fn initFile(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?HTTPClient.MimeType, allocator: std.mem.Allocator) !*Store {
- var store = try allocator.create(Blob.Store);
- store.* = .{
- .data = .{
- .file = FileStore.init(
- pathlike,
- mime_type orelse brk: {
- if (pathlike == .path) {
- const sliced = pathlike.path.slice();
- if (sliced.len > 0) {
- var extname = std.fs.path.extension(sliced);
- extname = std.mem.trim(u8, extname, ".");
- if (HTTPClient.MimeType.byExtensionNoDefault(extname)) |mime| {
- break :brk mime;
- }
- }
- }
-
- break :brk null;
- },
- ),
- },
- .allocator = allocator,
- .ref_count = 1,
- };
- return store;
- }
-
- pub fn init(bytes: []u8, allocator: std.mem.Allocator) !*Store {
- var store = try allocator.create(Blob.Store);
- store.* = .{
- .data = .{ .bytes = ByteStore.init(bytes, allocator) },
- .allocator = allocator,
- .ref_count = 1,
- };
- return store;
- }
-
- pub fn sharedView(this: Store) []u8 {
- if (this.data == .bytes)
- return this.data.bytes.slice();
-
- return &[_]u8{};
- }
-
- pub fn deref(this: *Blob.Store) void {
- std.debug.assert(this.ref_count >= 1);
- this.ref_count -= 1;
- if (this.ref_count == 0) {
- this.deinit();
- }
- }
-
- pub fn deinit(this: *Blob.Store) void {
- const allocator = this.allocator;
-
- switch (this.data) {
- .bytes => |*bytes| {
- bytes.deinit();
- },
- .file => |file| {
- if (file.pathlike == .path) {
- allocator.free(bun.constStrToU8(file.pathlike.path.slice()));
- }
- },
- }
-
- allocator.destroy(this);
- }
-
- pub fn fromArrayList(list: std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !*Blob.Store {
- return try Blob.Store.init(list.items, allocator);
- }
-
- pub fn FileOpenerMixin(comptime This: type) type {
- return struct {
- open_completion: AsyncIO.Completion = undefined,
- context: *This,
-
- const State = @This();
-
- const __opener_flags = std.os.O.NONBLOCK | std.os.O.CLOEXEC;
- const open_flags_ = if (@hasDecl(This, "open_flags"))
- This.open_flags | __opener_flags
- else
- std.os.O.RDONLY | __opener_flags;
-
- pub fn getFdMac(this: *This) bun.FileDescriptor {
- var buf: [bun.MAX_PATH_BYTES]u8 = undefined;
- var path_string = if (@hasField(This, "file_store"))
- this.file_store.pathlike.path
- else
- this.file_blob.store.?.data.file.pathlike.path;
-
- var path = path_string.sliceZ(&buf);
-
- this.opened_fd = switch (JSC.Node.Syscall.open(path, open_flags_, JSC.Node.default_permission)) {
- .result => |fd| fd,
- .err => |err| {
- this.errno = AsyncIO.asError(err.errno);
- this.system_error = err.withPath(path_string.slice()).toSystemError();
- this.opened_fd = null_fd;
- return null_fd;
- },
- };
-
- return this.opened_fd;
- }
-
- pub const OpenCallback = *const fn (*This, bun.FileDescriptor) void;
-
- pub fn getFd(this: *This, comptime Callback: OpenCallback) void {
- if (this.opened_fd != null_fd) {
- Callback(this, this.opened_fd);
- return;
- }
-
- if (comptime Environment.isMac) {
- Callback(this, this.getFdMac());
- } else {
- this.getFdLinux(Callback);
- }
- }
-
- const WrappedOpenCallback = *const fn (*State, *HTTPClient.NetworkThread.Completion, AsyncIO.OpenError!bun.FileDescriptor) void;
- fn OpenCallbackWrapper(comptime Callback: OpenCallback) WrappedOpenCallback {
- return struct {
- const callback = Callback;
- const StateHolder = State;
- pub fn onOpen(state: *State, completion: *HTTPClient.NetworkThread.Completion, result: AsyncIO.OpenError!bun.FileDescriptor) void {
- var this = state.context;
- var path_buffer = completion.operation.open.path;
- defer bun.default_allocator.free(bun.span(path_buffer));
- defer bun.default_allocator.destroy(state);
- this.opened_fd = result catch {
- this.errno = AsyncIO.asError(-completion.result);
- // do not use path_buffer here because it is a temporary
- var path_string = if (@hasField(This, "file_store"))
- this.file_store.pathlike.path
- else
- this.file_blob.store.?.data.file.pathlike.path;
-
- this.system_error = .{
- .syscall = ZigString.init("open"),
- .code = ZigString.init(std.mem.span(@errorName(this.errno.?))),
- .path = ZigString.init(path_string.slice()),
- };
-
- // assert we never end up reusing the memory
- std.debug.assert(@ptrToInt(this.system_error.?.path.slice().ptr) != @ptrToInt(path_buffer));
-
- callback(this, null_fd);
- return;
- };
-
- callback(this, this.opened_fd);
- }
- }.onOpen;
- }
-
- pub fn getFdLinux(this: *This, comptime callback: OpenCallback) void {
- var aio = &AsyncIO.global;
-
- var path_string = if (@hasField(This, "file_store"))
- this.file_store.pathlike.path
- else
- this.file_blob.store.?.data.file.pathlike.path;
-
- var holder = bun.default_allocator.create(State) catch unreachable;
- holder.* = .{
- .context = this,
- };
- var path_buffer = bun.default_allocator.dupeZ(u8, path_string.slice()) catch unreachable;
- aio.open(
- *State,
- holder,
- comptime OpenCallbackWrapper(callback),
- &holder.open_completion,
- path_buffer,
- open_flags_,
- JSC.Node.default_permission,
- );
- }
- };
- }
-
- pub fn FileCloserMixin(comptime This: type) type {
- return struct {
- const Closer = @This();
- close_completion: AsyncIO.Completion = undefined,
-
- pub fn doClose(this: *This) void {
- const fd = this.opened_fd;
- std.debug.assert(fd != null_fd);
- var aio = &AsyncIO.global;
-
- var closer = bun.default_allocator.create(Closer) catch unreachable;
-
- aio.close(
- *Closer,
- closer,
- onClose,
- &closer.close_completion,
- fd,
- );
- this.opened_fd = null_fd;
- }
-
- pub fn onClose(closer: *Closer, _: *HTTPClient.NetworkThread.Completion, _: AsyncIO.CloseError!void) void {
- bun.default_allocator.destroy(closer);
- }
- };
- }
-
- pub const ReadFile = struct {
- file_store: FileStore,
- byte_store: ByteStore = ByteStore{ .allocator = bun.default_allocator },
- store: ?*Store = null,
- offset: SizeType = 0,
- max_length: SizeType = Blob.max_size,
- opened_fd: bun.FileDescriptor = null_fd,
- read_completion: HTTPClient.NetworkThread.Completion = undefined,
- read_len: SizeType = 0,
- read_off: SizeType = 0,
- size: SizeType = 0,
- buffer: []u8 = undefined,
- task: HTTPClient.NetworkThread.Task = undefined,
- system_error: ?JSC.SystemError = null,
- errno: ?anyerror = null,
- onCompleteCtx: *anyopaque = undefined,
- onCompleteCallback: OnReadFileCallback = undefined,
- io_task: ?*ReadFileTask = null,
-
- pub const Read = struct {
- buf: []u8,
- is_temporary: bool = false,
- total_size: SizeType = 0,
- };
- pub const ResultType = SystemError.Maybe(Read);
-
- pub const OnReadFileCallback = *const fn (ctx: *anyopaque, bytes: ResultType) void;
-
- pub usingnamespace FileOpenerMixin(ReadFile);
- pub usingnamespace FileCloserMixin(ReadFile);
-
- pub fn createWithCtx(
- allocator: std.mem.Allocator,
- store: *Store,
- onReadFileContext: *anyopaque,
- onCompleteCallback: OnReadFileCallback,
- off: SizeType,
- max_len: SizeType,
- ) !*ReadFile {
- var read_file = try allocator.create(ReadFile);
- read_file.* = ReadFile{
- .file_store = store.data.file,
- .offset = off,
- .max_length = max_len,
- .store = store,
- .onCompleteCtx = onReadFileContext,
- .onCompleteCallback = onCompleteCallback,
- };
- store.ref();
- return read_file;
- }
-
- pub fn create(
- allocator: std.mem.Allocator,
- store: *Store,
- off: SizeType,
- max_len: SizeType,
- comptime Context: type,
- context: Context,
- comptime callback: fn (ctx: Context, bytes: ResultType) void,
- ) !*ReadFile {
- const Handler = struct {
- pub fn run(ptr: *anyopaque, bytes: ResultType) void {
- callback(bun.cast(Context, ptr), bytes);
- }
- };
-
- return try ReadFile.createWithCtx(allocator, store, @ptrCast(*anyopaque, context), Handler.run, off, max_len);
- }
-
- pub fn doRead(this: *ReadFile) void {
- 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..@min(remaining.len, this.max_length - this.read_off)],
- this.offset + this.read_off,
- );
- }
-
- pub const ReadFileTask = JSC.IOTask(@This());
-
- pub fn then(this: *ReadFile, _: *JSC.JSGlobalObject) void {
- var cb = this.onCompleteCallback;
- var cb_ctx = this.onCompleteCtx;
-
- if (this.store == null and this.system_error != null) {
- var system_error = this.system_error.?;
- bun.default_allocator.destroy(this);
- cb(cb_ctx, ResultType{ .err = system_error });
- return;
- } else if (this.store == null) {
- bun.default_allocator.destroy(this);
- cb(cb_ctx, ResultType{ .err = SystemError{
- .code = ZigString.init("INTERNAL_ERROR"),
- .path = ZigString.Empty,
- .message = ZigString.init("assertion failure - store should not be null"),
- .syscall = ZigString.init("read"),
- } });
- return;
- }
-
- var store = this.store.?;
- var buf = this.buffer;
-
- defer store.deref();
- defer bun.default_allocator.destroy(this);
- if (this.system_error) |err| {
- cb(cb_ctx, ResultType{ .err = err });
- return;
- }
-
- cb(cb_ctx, .{ .result = .{ .buf = buf, .total_size = this.size, .is_temporary = true } });
- }
- pub fn run(this: *ReadFile, task: *ReadFileTask) void {
- this.runAsync(task);
- }
-
- pub fn onRead(this: *ReadFile, completion: *HTTPClient.NetworkThread.Completion, result: AsyncIO.ReadError!usize) void {
- defer this.doReadLoop();
-
- this.read_len = @truncate(SizeType, result catch |err| {
- if (@hasField(HTTPClient.NetworkThread.Completion, "result")) {
- this.errno = AsyncIO.asError(-completion.result);
- this.system_error = (JSC.Node.Syscall.Error{
- .errno = @intCast(JSC.Node.Syscall.Error.Int, -completion.result),
- .syscall = .read,
- }).toSystemError();
- } else {
- this.system_error = JSC.SystemError{
- .code = ZigString.init(std.mem.span(@errorName(err))),
- .path = if (this.file_store.pathlike == .path)
- ZigString.init(this.file_store.pathlike.path.slice())
- else
- ZigString.Empty,
- .syscall = ZigString.init("read"),
- };
-
- this.errno = err;
- }
-
- this.read_len = 0;
- return;
- });
- }
-
- fn runAsync(this: *ReadFile, task: *ReadFileTask) void {
- this.io_task = task;
-
- if (this.file_store.pathlike == .fd) {
- this.opened_fd = this.file_store.pathlike.fd;
- }
-
- this.getFd(runAsyncWithFD);
- }
-
- fn onFinish(this: *ReadFile) void {
- const fd = this.opened_fd;
- const file = &this.file_store;
- const needs_close = fd != null_fd and file.pathlike == .path and fd > 2;
-
- this.size = @max(this.read_len, this.size);
-
- if (needs_close) {
- this.doClose();
- }
-
- var io_task = this.io_task.?;
- this.io_task = null;
- io_task.onFinish();
- }
-
- fn resolveSize(this: *ReadFile, fd: bun.FileDescriptor) void {
- const stat: std.os.Stat = switch (JSC.Node.Syscall.fstat(fd)) {
- .result => |result| result,
- .err => |err| {
- this.errno = AsyncIO.asError(err.errno);
- this.system_error = err.toSystemError();
- return;
- },
- };
- if (std.os.S.ISDIR(stat.mode)) {
- this.errno = error.EISDIR;
- this.system_error = JSC.SystemError{
- .code = ZigString.init("EISDIR"),
- .path = if (this.file_store.pathlike == .path)
- ZigString.init(this.file_store.pathlike.path.slice())
- else
- ZigString.Empty,
- .message = ZigString.init("Directories cannot be read like files"),
- .syscall = ZigString.init("read"),
- };
- return;
- }
-
- if (stat.size > 0 and std.os.S.ISREG(stat.mode)) {
- this.size = @min(
- @truncate(SizeType, @intCast(SizeType, @max(@intCast(i64, stat.size), 0))),
- this.max_length,
- );
- // read up to 4k at a time if
- // they didn't explicitly set a size and we're reading from something that's not a regular file
- } else if (stat.size == 0 and !std.os.S.ISREG(stat.mode)) {
- this.size = if (this.max_length == Blob.max_size)
- 4096
- else
- this.max_length;
- }
- }
-
- fn runAsyncWithFD(this: *ReadFile, fd: bun.FileDescriptor) void {
- if (this.errno != null) {
- this.onFinish();
- return;
- }
-
- this.resolveSize(fd);
- if (this.errno != null)
- return this.onFinish();
-
- if (this.size == 0) {
- this.buffer = &[_]u8{};
- this.byte_store = ByteStore.init(this.buffer, bun.default_allocator);
-
- this.onFinish();
- }
-
- this.buffer = bun.default_allocator.alloc(u8, this.size) catch |err| {
- this.errno = err;
- this.onFinish();
- return;
- };
- this.read_len = 0;
- this.doReadLoop();
- }
-
- fn doReadLoop(this: *ReadFile) void {
- this.read_off += this.read_len;
- var remain = this.buffer[@min(this.read_off, @truncate(Blob.SizeType, this.buffer.len))..];
-
- if (remain.len > 0 and this.errno == null) {
- this.doRead();
- return;
- }
-
- _ = bun.default_allocator.resize(this.buffer, this.read_off);
- this.buffer = this.buffer[0..this.read_off];
- this.byte_store = ByteStore.init(this.buffer, bun.default_allocator);
- this.onFinish();
- }
- };
-
- pub const WriteFile = struct {
- file_blob: Blob,
- bytes_blob: Blob,
-
- opened_fd: bun.FileDescriptor = null_fd,
- system_error: ?JSC.SystemError = null,
- errno: ?anyerror = null,
- write_completion: HTTPClient.NetworkThread.Completion = undefined,
- task: HTTPClient.NetworkThread.Task = undefined,
- io_task: ?*WriteFileTask = null,
-
- onCompleteCtx: *anyopaque = undefined,
- onCompleteCallback: OnWriteFileCallback = undefined,
- wrote: usize = 0,
-
- pub const ResultType = SystemError.Maybe(SizeType);
- pub const OnWriteFileCallback = *const fn (ctx: *anyopaque, count: ResultType) void;
-
- pub usingnamespace FileOpenerMixin(WriteFile);
- pub usingnamespace FileCloserMixin(WriteFile);
-
- // Do not open with APPEND because we may use pwrite()
- pub const open_flags = std.os.O.WRONLY | std.os.O.CREAT | std.os.O.TRUNC;
-
- pub fn createWithCtx(
- allocator: std.mem.Allocator,
- file_blob: Blob,
- bytes_blob: Blob,
- onWriteFileContext: *anyopaque,
- onCompleteCallback: OnWriteFileCallback,
- ) !*WriteFile {
- var read_file = try allocator.create(WriteFile);
- read_file.* = WriteFile{
- .file_blob = file_blob,
- .bytes_blob = bytes_blob,
- .onCompleteCtx = onWriteFileContext,
- .onCompleteCallback = onCompleteCallback,
- };
- file_blob.store.?.ref();
- bytes_blob.store.?.ref();
- return read_file;
- }
-
- pub fn create(
- allocator: std.mem.Allocator,
- file_blob: Blob,
- bytes_blob: Blob,
- comptime Context: type,
- context: Context,
- comptime callback: fn (ctx: Context, bytes: ResultType) void,
- ) !*WriteFile {
- const Handler = struct {
- pub fn run(ptr: *anyopaque, bytes: ResultType) void {
- callback(bun.cast(Context, ptr), bytes);
- }
- };
-
- return try WriteFile.createWithCtx(
- allocator,
- file_blob,
- bytes_blob,
- @ptrCast(*anyopaque, context),
- Handler.run,
- );
- }
-
- pub fn doWrite(
- this: *WriteFile,
- buffer: []const u8,
- file_offset: u64,
- ) void {
- var aio = &AsyncIO.global;
- this.wrote = 0;
- const fd = this.opened_fd;
- std.debug.assert(fd != null_fd);
- aio.write(
- *WriteFile,
- this,
- onWrite,
- &this.write_completion,
- fd,
- buffer,
- if (fd > 2) file_offset else 0,
- );
- }
-
- pub const WriteFileTask = JSC.IOTask(@This());
-
- pub fn then(this: *WriteFile, _: *JSC.JSGlobalObject) void {
- var cb = this.onCompleteCallback;
- var cb_ctx = this.onCompleteCtx;
-
- this.bytes_blob.store.?.deref();
- this.file_blob.store.?.deref();
-
- if (this.system_error) |err| {
- bun.default_allocator.destroy(this);
- cb(cb_ctx, .{
- .err = err,
- });
- return;
- }
-
- const wrote = this.wrote;
- bun.default_allocator.destroy(this);
- cb(cb_ctx, .{ .result = @truncate(SizeType, wrote) });
- }
- pub fn run(this: *WriteFile, task: *WriteFileTask) void {
- this.io_task = task;
- this.runAsync();
- }
-
- pub fn onWrite(this: *WriteFile, _: *HTTPClient.NetworkThread.Completion, result: AsyncIO.WriteError!usize) void {
- defer this.doWriteLoop();
- this.wrote += @truncate(SizeType, result catch |errno| {
- this.errno = errno;
- this.system_error = this.system_error orelse JSC.SystemError{
- .code = ZigString.init(std.mem.span(@errorName(errno))),
- .syscall = ZigString.init("write"),
- };
-
- this.wrote = 0;
- return;
- });
- }
-
- fn runAsync(this: *WriteFile) void {
- this.getFd(runWithFD);
- }
-
- fn onFinish(this: *WriteFile) void {
- const fd = this.opened_fd;
- const file = this.file_blob.store.?.data.file;
- const needs_close = fd != null_fd and file.pathlike == .path and fd > 2;
-
- if (needs_close) {
- this.doClose();
- }
-
- var io_task = this.io_task.?;
- this.io_task = null;
- io_task.onFinish();
- }
-
- fn runWithFD(this: *WriteFile, fd: bun.FileDescriptor) void {
- if (fd == null_fd or this.errno != null) {
- this.onFinish();
- return;
- }
-
- this.doWriteLoop();
- }
-
- fn doWriteLoop(this: *WriteFile) void {
- var remain = this.bytes_blob.sharedView();
- var file_offset = this.file_blob.offset;
-
- const this_tick = file_offset + this.wrote;
- remain = remain[@min(this.wrote, remain.len)..];
-
- if (remain.len > 0 and this.errno == null) {
- this.doWrite(remain, this_tick);
- } else {
- this.onFinish();
- }
- }
- };
-
- pub const IOWhich = enum {
- source,
- destination,
- both,
- };
-
- const unsupported_directory_error = SystemError{
- .errno = @intCast(c_int, @enumToInt(bun.C.SystemErrno.EISDIR)),
- .message = ZigString.init("That doesn't work on folders"),
- .syscall = ZigString.init("fstat"),
- };
- const unsupported_non_regular_file_error = SystemError{
- .errno = @intCast(c_int, @enumToInt(bun.C.SystemErrno.ENOTSUP)),
- .message = ZigString.init("Non-regular files aren't supported yet"),
- .syscall = ZigString.init("fstat"),
- };
-
- // blocking, but off the main thread
- pub const CopyFile = struct {
- destination_file_store: FileStore,
- source_file_store: FileStore,
- store: ?*Store = null,
- source_store: ?*Store = null,
- offset: SizeType = 0,
- size: SizeType = 0,
- max_length: SizeType = Blob.max_size,
- destination_fd: bun.FileDescriptor = null_fd,
- source_fd: bun.FileDescriptor = null_fd,
-
- system_error: ?SystemError = null,
-
- read_len: SizeType = 0,
- read_off: SizeType = 0,
-
- globalThis: *JSGlobalObject,
-
- pub const ResultType = anyerror!SizeType;
-
- pub const Callback = *const fn (ctx: *anyopaque, len: ResultType) void;
- pub const CopyFilePromiseTask = JSC.ConcurrentPromiseTask(CopyFile);
- pub const CopyFilePromiseTaskEventLoopTask = CopyFilePromiseTask.EventLoopTask;
-
- pub fn create(
- allocator: std.mem.Allocator,
- store: *Store,
- source_store: *Store,
- off: SizeType,
- max_len: SizeType,
- globalThis: *JSC.JSGlobalObject,
- ) !*CopyFilePromiseTask {
- var read_file = try allocator.create(CopyFile);
- read_file.* = CopyFile{
- .store = store,
- .source_store = source_store,
- .offset = off,
- .max_length = max_len,
- .globalThis = globalThis,
- .destination_file_store = store.data.file,
- .source_file_store = source_store.data.file,
- };
- store.ref();
- source_store.ref();
- return try CopyFilePromiseTask.createOnJSThread(allocator, globalThis, read_file);
- }
-
- const linux = std.os.linux;
- const darwin = std.os.darwin;
-
- pub fn deinit(this: *CopyFile) void {
- if (this.source_file_store.pathlike == .path) {
- if (this.source_file_store.pathlike.path == .string and this.system_error == null) {
- bun.default_allocator.free(bun.constStrToU8(this.source_file_store.pathlike.path.slice()));
- }
- }
- this.store.?.deref();
-
- bun.default_allocator.destroy(this);
- }
-
- pub fn reject(this: *CopyFile, promise: *JSC.JSPromise) void {
- var globalThis = this.globalThis;
- var system_error: SystemError = this.system_error orelse SystemError{};
- if (this.source_file_store.pathlike == .path and system_error.path.len == 0) {
- system_error.path = ZigString.init(this.source_file_store.pathlike.path.slice());
- system_error.path.mark();
- }
-
- if (system_error.message.len == 0) {
- system_error.message = ZigString.init("Failed to copy file");
- }
-
- var instance = system_error.toErrorInstance(this.globalThis);
- if (this.store) |store| {
- store.deref();
- }
- promise.reject(globalThis, instance);
- }
-
- pub fn then(this: *CopyFile, promise: *JSC.JSPromise) void {
- this.source_store.?.deref();
-
- if (this.system_error != null) {
- this.reject(promise);
- return;
- }
-
- promise.resolve(this.globalThis, JSC.JSValue.jsNumberFromUint64(this.read_len));
- }
-
- pub fn run(this: *CopyFile) void {
- this.runAsync();
- }
-
- pub fn doClose(this: *CopyFile) void {
- const close_input = this.destination_file_store.pathlike != .fd and this.destination_fd != null_fd;
- const close_output = this.source_file_store.pathlike != .fd and this.source_fd != null_fd;
-
- if (close_input and close_output) {
- this.doCloseFile(.both);
- } else if (close_input) {
- this.doCloseFile(.destination);
- } else if (close_output) {
- this.doCloseFile(.source);
- }
- }
-
- const os = std.os;
-
- pub fn doCloseFile(this: *CopyFile, comptime which: IOWhich) void {
- switch (which) {
- .both => {
- _ = JSC.Node.Syscall.close(this.destination_fd);
- _ = JSC.Node.Syscall.close(this.source_fd);
- },
- .destination => {
- _ = JSC.Node.Syscall.close(this.destination_fd);
- },
- .source => {
- _ = JSC.Node.Syscall.close(this.source_fd);
- },
- }
- }
-
- const O = if (Environment.isLinux) linux.O else std.os.O;
- const open_destination_flags = O.CLOEXEC | O.CREAT | O.WRONLY | O.TRUNC;
- const open_source_flags = O.CLOEXEC | O.RDONLY;
-
- pub fn doOpenFile(this: *CopyFile, comptime which: IOWhich) !void {
- // open source file first
- // if it fails, we don't want the extra destination file hanging out
- if (which == .both or which == .source) {
- this.source_fd = switch (JSC.Node.Syscall.open(
- this.source_file_store.pathlike.path.sliceZAssume(),
- open_source_flags,
- 0,
- )) {
- .result => |result| result,
- .err => |errno| {
- this.system_error = errno.toSystemError();
- return AsyncIO.asError(errno.errno);
- },
- };
- }
-
- if (which == .both or which == .destination) {
- this.destination_fd = switch (JSC.Node.Syscall.open(
- this.destination_file_store.pathlike.path.sliceZAssume(),
- open_destination_flags,
- JSC.Node.default_permission,
- )) {
- .result => |result| result,
- .err => |errno| {
- if (which == .both) {
- _ = JSC.Node.Syscall.close(this.source_fd);
- this.source_fd = 0;
- }
-
- this.system_error = errno.toSystemError();
- return AsyncIO.asError(errno.errno);
- },
- };
- }
- }
-
- const TryWith = enum {
- sendfile,
- copy_file_range,
- splice,
-
- pub const tag = std.EnumMap(TryWith, JSC.Node.Syscall.Tag).init(.{
- .sendfile = .sendfile,
- .copy_file_range = .copy_file_range,
- .splice = .splice,
- });
- };
-
- pub fn doCopyFileRange(
- this: *CopyFile,
- comptime use: TryWith,
- comptime clear_append_if_invalid: bool,
- ) anyerror!void {
- this.read_off += this.offset;
-
- var remain = @as(usize, this.max_length);
- if (remain == max_size or remain == 0) {
- // sometimes stat lies
- // let's give it 4096 and see how it goes
- remain = 4096;
- }
-
- var total_written: usize = 0;
- const src_fd = this.source_fd;
- const dest_fd = this.destination_fd;
-
- defer {
- this.read_len = @truncate(SizeType, total_written);
- }
-
- var has_unset_append = false;
-
- while (true) {
- const written = switch (comptime use) {
- .copy_file_range => linux.copy_file_range(src_fd, null, dest_fd, null, remain, 0),
- .sendfile => linux.sendfile(dest_fd, src_fd, null, remain),
- .splice => bun.C.splice(src_fd, null, dest_fd, null, remain, 0),
- };
-
- switch (linux.getErrno(written)) {
- .SUCCESS => {},
-
- .INVAL => {
- if (comptime clear_append_if_invalid) {
- if (!has_unset_append) {
- // https://kylelaker.com/2018/08/31/stdout-oappend.html
- // make() can set STDOUT / STDERR to O_APPEND
- // this messes up sendfile()
- has_unset_append = true;
- const flags = linux.fcntl(dest_fd, linux.F.GETFL, 0);
- if ((flags & O.APPEND) != 0) {
- _ = linux.fcntl(dest_fd, linux.F.SETFL, flags ^ O.APPEND);
- continue;
- }
- }
- }
-
- this.system_error = (JSC.Node.Syscall.Error{
- .errno = @intCast(JSC.Node.Syscall.Error.Int, @enumToInt(linux.E.INVAL)),
- .syscall = TryWith.tag.get(use).?,
- }).toSystemError();
- return AsyncIO.asError(linux.E.INVAL);
- },
- else => |errno| {
- this.system_error = (JSC.Node.Syscall.Error{
- .errno = @intCast(JSC.Node.Syscall.Error.Int, @enumToInt(errno)),
- .syscall = TryWith.tag.get(use).?,
- }).toSystemError();
- return AsyncIO.asError(errno);
- },
- }
-
- // wrote zero bytes means EOF
- remain -|= written;
- total_written += written;
- if (written == 0 or remain == 0) break;
- }
- }
-
- pub fn doFCopyFile(this: *CopyFile) anyerror!void {
- switch (JSC.Node.Syscall.fcopyfile(this.source_fd, this.destination_fd, os.system.COPYFILE_DATA)) {
- .err => |errno| {
- this.system_error = errno.toSystemError();
-
- return AsyncIO.asError(errno.errno);
- },
- .result => {},
- }
- }
-
- pub fn doClonefile(this: *CopyFile) anyerror!void {
- var source_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
- var dest_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
-
- switch (JSC.Node.Syscall.clonefile(
- this.source_file_store.pathlike.path.sliceZ(&source_buf),
- this.destination_file_store.pathlike.path.sliceZ(
- &dest_buf,
- ),
- )) {
- .err => |errno| {
- this.system_error = errno.toSystemError();
- return AsyncIO.asError(errno.errno);
- },
- .result => {},
- }
- }
-
- pub fn runAsync(this: *CopyFile) void {
- // defer task.onFinish();
-
- var stat_: ?std.os.Stat = null;
-
- if (this.destination_file_store.pathlike == .fd) {
- this.destination_fd = this.destination_file_store.pathlike.fd;
- }
-
- if (this.source_file_store.pathlike == .fd) {
- this.source_fd = this.source_file_store.pathlike.fd;
- }
-
- // Do we need to open both files?
- if (this.destination_fd == null_fd and this.source_fd == null_fd) {
-
- // First, we attempt to clonefile() on macOS
- // This is the fastest way to copy a file.
- if (comptime Environment.isMac) {
- if (this.offset == 0 and this.source_file_store.pathlike == .path and this.destination_file_store.pathlike == .path) {
- do_clonefile: {
-
- // stat the output file, make sure it:
- // 1. Exists
- switch (JSC.Node.Syscall.stat(this.source_file_store.pathlike.path.sliceZAssume())) {
- .result => |result| {
- stat_ = result;
-
- if (os.S.ISDIR(result.mode)) {
- this.system_error = unsupported_directory_error;
- return;
- }
-
- if (!os.S.ISREG(result.mode))
- break :do_clonefile;
- },
- .err => |err| {
- // If we can't stat it, we also can't copy it.
- this.system_error = err.toSystemError();
- return;
- },
- }
-
- if (this.doClonefile()) {
- if (this.max_length != Blob.max_size and this.max_length < @intCast(SizeType, stat_.?.size)) {
- // If this fails...well, there's not much we can do about it.
- _ = bun.C.truncate(
- this.destination_file_store.pathlike.path.sliceZAssume(),
- @intCast(std.os.off_t, this.max_length),
- );
- this.read_len = @intCast(SizeType, this.max_length);
- } else {
- this.read_len = @intCast(SizeType, stat_.?.size);
- }
- return;
- } else |_| {
-
- // this may still fail, in which case we just continue trying with fcopyfile
- // it can fail when the input file already exists
- // or if the output is not a directory
- // or if it's a network volume
- this.system_error = null;
- }
- }
- }
- }
-
- this.doOpenFile(.both) catch return;
- // Do we need to open only one file?
- } else if (this.destination_fd == null_fd) {
- this.source_fd = this.source_file_store.pathlike.fd;
-
- this.doOpenFile(.destination) catch return;
- // Do we need to open only one file?
- } else if (this.source_fd == null_fd) {
- this.destination_fd = this.destination_file_store.pathlike.fd;
-
- this.doOpenFile(.source) catch return;
- }
-
- if (this.system_error != null) {
- return;
- }
-
- std.debug.assert(this.destination_fd != null_fd);
- std.debug.assert(this.source_fd != null_fd);
-
- if (this.destination_file_store.pathlike == .fd) {}
-
- const stat: std.os.Stat = stat_ orelse switch (JSC.Node.Syscall.fstat(this.source_fd)) {
- .result => |result| result,
- .err => |err| {
- this.doClose();
- this.system_error = err.toSystemError();
- return;
- },
- };
-
- if (os.S.ISDIR(stat.mode)) {
- this.system_error = unsupported_directory_error;
- this.doClose();
- return;
- }
-
- if (stat.size != 0) {
- this.max_length = @max(@min(@intCast(SizeType, stat.size), this.max_length), this.offset) - this.offset;
- if (this.max_length == 0) {
- this.doClose();
- return;
- }
-
- if (os.S.ISREG(stat.mode) and
- this.max_length > std.mem.page_size and
- this.max_length != Blob.max_size)
- {
- bun.C.preallocate_file(this.destination_fd, 0, this.max_length) catch {};
- }
- }
-
- if (comptime Environment.isLinux) {
-
- // Bun.write(Bun.file("a"), Bun.file("b"))
- if (os.S.ISREG(stat.mode) and (os.S.ISREG(this.destination_file_store.mode) or this.destination_file_store.mode == 0)) {
- if (this.destination_file_store.is_atty orelse false) {
- this.doCopyFileRange(.copy_file_range, true) catch {};
- } else {
- this.doCopyFileRange(.copy_file_range, false) catch {};
- }
-
- this.doClose();
- return;
- }
-
- // $ bun run foo.js | bun run bar.js
- if (os.S.ISFIFO(stat.mode) and os.S.ISFIFO(this.destination_file_store.mode)) {
- if (this.destination_file_store.is_atty orelse false) {
- this.doCopyFileRange(.splice, true) catch {};
- } else {
- this.doCopyFileRange(.splice, false) catch {};
- }
-
- this.doClose();
- return;
- }
-
- if (os.S.ISREG(stat.mode) or os.S.ISCHR(stat.mode) or os.S.ISSOCK(stat.mode)) {
- if (this.destination_file_store.is_atty orelse false) {
- this.doCopyFileRange(.sendfile, true) catch {};
- } else {
- this.doCopyFileRange(.sendfile, false) catch {};
- }
-
- this.doClose();
- return;
- }
-
- this.system_error = unsupported_non_regular_file_error;
- this.doClose();
- return;
- }
-
- if (comptime Environment.isMac) {
- this.doFCopyFile() catch {
- this.doClose();
-
- return;
- };
- if (stat.size != 0 and @intCast(SizeType, stat.size) > this.max_length) {
- _ = darwin.ftruncate(this.destination_fd, @intCast(std.os.off_t, this.max_length));
- }
-
- this.doClose();
- } else {
- @compileError("TODO: implement copyfile");
- }
- }
- };
- };
-
- pub const FileStore = struct {
- pathlike: JSC.Node.PathOrFileDescriptor,
- mime_type: HTTPClient.MimeType = HTTPClient.MimeType.other,
- is_atty: ?bool = null,
- mode: JSC.Node.Mode = 0,
- seekable: ?bool = null,
- max_size: SizeType = Blob.max_size,
-
- pub fn isSeekable(this: *const FileStore) ?bool {
- if (this.seekable) |seekable| {
- return seekable;
- }
-
- if (this.mode != 0) {
- return std.os.S.ISREG(this.mode);
- }
-
- return null;
- }
-
- pub fn init(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?HTTPClient.MimeType) FileStore {
- return .{ .pathlike = pathlike, .mime_type = mime_type orelse HTTPClient.MimeType.other };
- }
- };
-
- pub const ByteStore = struct {
- ptr: [*]u8 = undefined,
- len: SizeType = 0,
- cap: SizeType = 0,
- allocator: std.mem.Allocator,
-
- pub fn init(bytes: []u8, allocator: std.mem.Allocator) ByteStore {
- return .{
- .ptr = bytes.ptr,
- .len = @truncate(SizeType, bytes.len),
- .cap = @truncate(SizeType, bytes.len),
- .allocator = allocator,
- };
- }
-
- pub fn fromArrayList(list: std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !*ByteStore {
- return ByteStore.init(list.items, allocator);
- }
-
- pub fn slice(this: ByteStore) []u8 {
- return this.ptr[0..this.len];
- }
-
- pub fn deinit(this: *ByteStore) void {
- this.allocator.free(this.ptr[0..this.cap]);
- }
-
- pub fn asArrayList(this: ByteStore) std.ArrayListUnmanaged(u8) {
- return this.asArrayListLeak();
- }
-
- pub fn asArrayListLeak(this: ByteStore) std.ArrayListUnmanaged(u8) {
- return .{
- .items = this.ptr[0..this.len],
- .capacity = this.cap,
- };
- }
- };
-
- pub fn getStream(
- this: *Blob,
- globalThis: *JSC.JSGlobalObject,
- callframe: *JSC.CallFrame,
- ) callconv(.C) JSC.JSValue {
- var recommended_chunk_size: SizeType = 0;
- var arguments_ = callframe.arguments(2);
- var arguments = arguments_.ptr[0..arguments_.len];
- if (arguments.len > 0) {
- if (!arguments[0].isNumber() and !arguments[0].isUndefinedOrNull()) {
- globalThis.throwInvalidArguments("chunkSize must be a number", .{});
- return JSValue.jsUndefined();
- }
-
- recommended_chunk_size = @intCast(SizeType, @max(0, @truncate(i52, arguments[0].toInt64())));
- }
- return JSC.WebCore.ReadableStream.fromBlob(
- globalThis,
- this,
- recommended_chunk_size,
- );
- }
-
- fn promisified(
- value: JSC.JSValue,
- global: *JSGlobalObject,
- ) JSC.JSValue {
- if (value.isError()) {
- return JSC.JSPromise.rejectedPromiseValue(global, value);
- }
-
- if (value.jsType() == .JSPromise)
- return value;
-
- return JSPromise.resolvedPromiseValue(
- global,
- value,
- );
- }
-
- pub fn getText(
- this: *Blob,
- globalThis: *JSC.JSGlobalObject,
- _: *JSC.CallFrame,
- ) callconv(.C) JSC.JSValue {
- return promisified(this.toString(globalThis, .clone), globalThis);
- }
-
- pub fn getTextTransfer(
- this: *Blob,
- globalObject: *JSC.JSGlobalObject,
- ) JSC.JSValue {
- return promisified(this.toString(globalObject, .transfer), globalObject);
- }
-
- pub fn getJSON(
- this: *Blob,
- globalThis: *JSC.JSGlobalObject,
- _: *JSC.CallFrame,
- ) callconv(.C) JSC.JSValue {
- return promisified(this.toJSON(globalThis, .share), globalThis);
- }
-
- pub fn getArrayBufferTransfer(
- this: *Blob,
- globalThis: *JSC.JSGlobalObject,
- ) JSC.JSValue {
- return promisified(this.toArrayBuffer(globalThis, .transfer), globalThis);
- }
-
- pub fn getArrayBuffer(
- this: *Blob,
- globalThis: *JSC.JSGlobalObject,
- _: *JSC.CallFrame,
- ) callconv(.C) JSValue {
- return promisified(this.toArrayBuffer(globalThis, .clone), globalThis);
- }
-
- pub fn getWriter(
- this: *Blob,
- globalThis: *JSC.JSGlobalObject,
- callframe: *JSC.CallFrame,
- ) callconv(.C) JSC.JSValue {
- var arguments_ = callframe.arguments(1);
- var arguments = arguments_.ptr[0..arguments_.len];
-
- if (!arguments.ptr[0].isEmptyOrUndefinedOrNull() and !arguments.ptr[0].isObject()) {
- globalThis.throwInvalidArguments("options must be an object or undefined", .{});
- return JSValue.jsUndefined();
- }
-
- var store = this.store orelse {
- globalThis.throwInvalidArguments("Blob is detached", .{});
- return JSValue.jsUndefined();
- };
-
- if (store.data != .file) {
- globalThis.throwInvalidArguments("Blob is read-only", .{});
- return JSValue.jsUndefined();
- }
-
- var sink = JSC.WebCore.FileSink.init(globalThis.allocator(), null) catch |err| {
- globalThis.throwInvalidArguments("Failed to create FileSink: {s}", .{@errorName(err)});
- return JSValue.jsUndefined();
- };
-
- const input_path: JSC.WebCore.PathOrFileDescriptor = brk: {
- if (store.data.file.pathlike == .fd) {
- break :brk .{ .fd = store.data.file.pathlike.fd };
- } else {
- break :brk .{
- .path = ZigString.Slice.fromUTF8NeverFree(
- store.data.file.pathlike.path.slice(),
- ).clone(
- globalThis.allocator(),
- ) catch unreachable,
- };
- }
- };
- defer input_path.deinit();
-
- var stream_start: JSC.WebCore.StreamStart = .{
- .FileSink = .{
- .input_path = input_path,
- },
- };
-
- if (arguments.len > 0 and arguments.ptr[0].isObject()) {
- stream_start = JSC.WebCore.StreamStart.fromJSWithTag(globalThis, arguments[0], .FileSink);
- stream_start.FileSink.input_path = input_path;
- }
-
- switch (sink.start(stream_start)) {
- .err => |err| {
- globalThis.vm().throwError(globalThis, err.toJSC(globalThis));
- sink.finalize();
-
- return JSC.JSValue.zero;
- },
- else => {},
- }
-
- return sink.toJS(globalThis);
- }
-
- /// https://w3c.github.io/FileAPI/#slice-method-algo
- /// The slice() method returns a new Blob object with bytes ranging from the
- /// optional start parameter up to but not including the optional end
- /// parameter, and with a type attribute that is the value of the optional
- /// contentType parameter. It must act as follows:
- pub fn getSlice(
- this: *Blob,
- globalThis: *JSC.JSGlobalObject,
- callframe: *JSC.CallFrame,
- ) callconv(.C) JSC.JSValue {
- var allocator = globalThis.allocator();
- var arguments_ = callframe.arguments(2);
- var args = arguments_.ptr[0..arguments_.len];
-
- if (this.size == 0) {
- const empty = Blob.initEmpty(globalThis);
- var ptr = allocator.create(Blob) catch {
- return JSC.JSValue.jsUndefined();
- };
- ptr.* = empty;
- ptr.allocator = allocator;
- return ptr.toJS(globalThis);
- }
-
- // If the optional start parameter is not used as a parameter when making this call, let relativeStart be 0.
- var relativeStart: i64 = 0;
-
- // If the optional end parameter is not used as a parameter when making this call, let relativeEnd be size.
- var relativeEnd: i64 = @intCast(i64, this.size);
-
- var args_iter = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), args);
- if (args_iter.nextEat()) |start_| {
- const start = start_.toInt64();
- if (start < 0) {
- // If the optional start parameter is negative, let relativeStart be start + size.
- relativeStart = @intCast(i64, @max(start + @intCast(i64, this.size), 0));
- } else {
- // Otherwise, let relativeStart be start.
- relativeStart = @min(@intCast(i64, start), @intCast(i64, this.size));
- }
- }
-
- if (args_iter.nextEat()) |end_| {
- const end = end_.toInt64();
- // If end is negative, let relativeEnd be max((size + end), 0).
- if (end < 0) {
- // If the optional start parameter is negative, let relativeStart be start + size.
- relativeEnd = @intCast(i64, @max(end + @intCast(i64, this.size), 0));
- } else {
- // Otherwise, let relativeStart be start.
- relativeEnd = @min(@intCast(i64, end), @intCast(i64, this.size));
- }
- }
-
- var content_type: string = "";
- if (args_iter.nextEat()) |content_type_| {
- if (content_type_.isString()) {
- var zig_str = content_type_.getZigString(globalThis);
- var slicer = zig_str.toSlice(bun.default_allocator);
- defer slicer.deinit();
- var slice = slicer.slice();
- var content_type_buf = allocator.alloc(u8, slice.len) catch unreachable;
- content_type = strings.copyLowercase(slice, content_type_buf);
- }
- }
-
- const len = @intCast(SizeType, @max(relativeEnd - relativeStart, 0));
-
- // This copies over the is_all_ascii flag
- // which is okay because this will only be a <= slice
- var blob = this.dupe();
- blob.offset = @intCast(SizeType, relativeStart);
- blob.size = len;
- blob.content_type = content_type;
- blob.content_type_allocated = content_type.len > 0;
-
- var blob_ = allocator.create(Blob) catch unreachable;
- blob_.* = blob;
- blob_.allocator = allocator;
- return blob_.toJS(globalThis);
- }
-
- pub fn getType(
- this: *Blob,
- globalThis: *JSC.JSGlobalObject,
- ) callconv(.C) JSValue {
- return ZigString.init(this.content_type).toValue(globalThis);
- }
-
- pub fn setType(
- this: *Blob,
- globalThis: *JSC.JSGlobalObject,
- value: JSC.JSValue,
- ) callconv(.C) bool {
- var zig_str = value.getZigString(globalThis);
- if (zig_str.is16Bit())
- return false;
-
- var slice = zig_str.trimmedSlice();
- if (strings.eql(slice, this.content_type))
- return true;
-
- const prev_content_type = this.content_type;
- {
- defer if (this.content_type_allocated) bun.default_allocator.free(prev_content_type);
- var content_type_buf = globalThis.allocator().alloc(u8, slice.len) catch unreachable;
- this.content_type = strings.copyLowercase(slice, content_type_buf);
- }
-
- this.content_type_allocated = true;
- return true;
- }
-
- pub fn getSize(this: *Blob, _: *JSC.JSGlobalObject) callconv(.C) JSValue {
- if (this.size == Blob.max_size) {
- this.resolveSize();
- if (this.size == Blob.max_size and this.store != null) {
- return JSC.jsNumber(std.math.inf(f64));
- } else if (this.size == 0 and this.store != null) {
- if (this.store.?.data == .file and
- (this.store.?.data.file.seekable orelse true) == false and
- this.store.?.data.file.max_size == Blob.max_size)
- {
- return JSC.jsNumber(std.math.inf(f64));
- }
- }
- }
-
- return JSValue.jsNumber(this.size);
- }
-
- pub fn resolveSize(this: *Blob) void {
- if (this.store) |store| {
- if (store.data == .bytes) {
- const offset = this.offset;
- const store_size = store.size();
- if (store_size != Blob.max_size) {
- this.offset = @min(store_size, offset);
- this.size = store_size - offset;
- }
-
- return;
- } else if (store.data == .file) {
- if (store.data.file.seekable == null) {
- if (store.data.file.pathlike == .path) {
- var buffer: [bun.MAX_PATH_BYTES]u8 = undefined;
- switch (JSC.Node.Syscall.stat(store.data.file.pathlike.path.sliceZ(&buffer))) {
- .result => |stat| {
- store.data.file.max_size = if (std.os.S.ISREG(stat.mode) or stat.size > 0)
- @truncate(SizeType, @intCast(u64, @max(stat.size, 0)))
- else
- Blob.max_size;
- store.data.file.mode = stat.mode;
- store.data.file.seekable = std.os.S.ISREG(stat.mode);
- },
- // the file may not exist yet. Thats's okay.
- else => {},
- }
- } else if (store.data.file.pathlike == .fd) {
- switch (JSC.Node.Syscall.fstat(store.data.file.pathlike.fd)) {
- .result => |stat| {
- store.data.file.max_size = if (std.os.S.ISREG(stat.mode) or stat.size > 0)
- @truncate(SizeType, @intCast(u64, @max(stat.size, 0)))
- else
- Blob.max_size;
- store.data.file.mode = stat.mode;
- store.data.file.seekable = std.os.S.ISREG(stat.mode);
- },
- // the file may not exist yet. Thats's okay.
- else => {},
- }
- }
- }
-
- if (store.data.file.seekable != null and store.data.file.max_size != Blob.max_size) {
- const store_size = store.data.file.max_size;
- const offset = this.offset;
-
- this.offset = @min(store_size, offset);
- this.size = store_size -| offset;
- return;
- }
- }
-
- this.size = 0;
- } else {
- this.size = 0;
- }
- }
-
- pub fn constructor(
- globalThis: *JSC.JSGlobalObject,
- callframe: *JSC.CallFrame,
- ) callconv(.C) ?*Blob {
- var allocator = globalThis.allocator();
- var blob: Blob = undefined;
- var arguments = callframe.arguments(2);
- var args = arguments.ptr[0..arguments.len];
-
- switch (args.len) {
- 0 => {
- var empty: []u8 = &[_]u8{};
- blob = Blob.init(empty, allocator, globalThis);
- },
- else => {
- blob = get(globalThis, args[0], false, true) catch |err| {
- if (err == error.InvalidArguments) {
- globalThis.throwInvalidArguments("new Blob() expects an Array", .{});
- return null;
- }
- globalThis.throw("out of memory", .{});
- return null;
- };
-
- if (args.len > 1) {
- var options = args[0];
- if (options.isCell()) {
- // type, the ASCII-encoded string in lower case
- // representing the media type of the Blob.
- // Normative conditions for this member are provided
- // in the § 3.1 Constructors.
- if (options.get(globalThis, "type")) |content_type| {
- if (content_type.isString()) {
- var content_type_str = content_type.getZigString(globalThis);
- if (!content_type_str.is16Bit()) {
- var slice = content_type_str.trimmedSlice();
- var content_type_buf = allocator.alloc(u8, slice.len) catch unreachable;
- blob.content_type = strings.copyLowercase(slice, content_type_buf);
- blob.content_type_allocated = true;
- }
- }
- }
- }
- }
-
- if (blob.content_type.len == 0) {
- blob.content_type = "";
- }
- },
- }
-
- var blob_ = allocator.create(Blob) catch unreachable;
- blob_.* = blob;
- blob_.allocator = allocator;
- return blob_;
- }
-
- pub fn finalize(this: *Blob) callconv(.C) void {
- this.deinit();
- }
-
- pub fn initWithAllASCII(bytes: []u8, allocator: std.mem.Allocator, globalThis: *JSGlobalObject, is_all_ascii: bool) Blob {
- // avoid allocating a Blob.Store if the buffer is actually empty
- var store: ?*Blob.Store = null;
- if (bytes.len > 0) {
- store = Blob.Store.init(bytes, allocator) catch unreachable;
- store.?.is_all_ascii = is_all_ascii;
- }
- return Blob{
- .size = @truncate(SizeType, bytes.len),
- .store = store,
- .allocator = null,
- .content_type = "",
- .globalThis = globalThis,
- .is_all_ascii = is_all_ascii,
- };
- }
-
- pub fn init(bytes: []u8, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) Blob {
- return Blob{
- .size = @truncate(SizeType, bytes.len),
- .store = if (bytes.len > 0)
- Blob.Store.init(bytes, allocator) catch unreachable
- else
- null,
- .allocator = null,
- .content_type = "",
- .globalThis = globalThis,
- };
- }
-
- pub fn create(
- bytes_: []const u8,
- allocator: std.mem.Allocator,
- globalThis: *JSGlobalObject,
- was_string: bool,
- ) Blob {
- var bytes = allocator.dupe(u8, bytes_) catch @panic("Out of memory");
- return Blob{
- .size = @truncate(SizeType, bytes_.len),
- .store = if (bytes.len > 0)
- Blob.Store.init(bytes, allocator) catch unreachable
- else
- null,
- .allocator = null,
- .content_type = if (was_string) MimeType.text.value else "",
- .globalThis = globalThis,
- };
- }
-
- pub fn initWithStore(store: *Blob.Store, globalThis: *JSGlobalObject) Blob {
- return Blob{
- .size = store.size(),
- .store = store,
- .allocator = null,
- .content_type = if (store.data == .file)
- store.data.file.mime_type.value
- else
- "",
- .globalThis = globalThis,
- };
- }
-
- pub fn initEmpty(globalThis: *JSGlobalObject) Blob {
- return Blob{
- .size = 0,
- .store = null,
- .allocator = null,
- .content_type = "",
- .globalThis = globalThis,
- };
- }
-
- // Transferring doesn't change the reference count
- // It is a move
- inline fn transfer(this: *Blob) void {
- this.store = null;
- }
-
- pub fn detach(this: *Blob) void {
- if (this.store != null) this.store.?.deref();
- this.store = null;
- }
-
- /// This does not duplicate
- /// This creates a new view
- /// and increment the reference count
- pub fn dupe(this: *const Blob) Blob {
- if (this.store != null) this.store.?.ref();
- var duped = this.*;
- duped.allocator = null;
- return duped;
- }
-
- pub fn deinit(this: *Blob) void {
- this.detach();
-
- if (this.allocator) |alloc| {
- this.allocator = null;
- alloc.destroy(this);
- }
- }
-
- pub fn sharedView(this: *const Blob) []const u8 {
- if (this.size == 0 or this.store == null) return "";
- var slice_ = this.store.?.sharedView();
- if (slice_.len == 0) return "";
- slice_ = slice_[this.offset..];
-
- return slice_[0..@min(slice_.len, @as(usize, this.size))];
- }
-
- pub const Lifetime = JSC.WebCore.Lifetime;
- pub fn setIsASCIIFlag(this: *Blob, is_all_ascii: bool) void {
- this.is_all_ascii = is_all_ascii;
- // if this Blob represents the entire binary data
- // which will be pretty common
- // we can update the store's is_all_ascii flag
- // and any other Blob that points to the same store
- // can skip checking the encoding
- if (this.size > 0 and this.offset == 0 and this.store.?.data == .bytes) {
- this.store.?.is_all_ascii = is_all_ascii;
- }
- }
-
- pub fn NewReadFileHandler(comptime Function: anytype) type {
- return struct {
- context: Blob,
- promise: JSPromise.Strong = .{},
- globalThis: *JSGlobalObject,
- pub fn run(handler: *@This(), bytes_: Blob.Store.ReadFile.ResultType) void {
- var promise = handler.promise.swap();
- var blob = handler.context;
- blob.allocator = null;
- var globalThis = handler.globalThis;
- bun.default_allocator.destroy(handler);
- switch (bytes_) {
- .result => |result| {
- const bytes = result.buf;
- if (blob.size > 0)
- blob.size = @min(@truncate(u32, bytes.len), blob.size);
- const value = Function(&blob, globalThis, bytes, .temporary);
-
- // invalid JSON needs to be rejected
- if (value.isAnyError()) {
- promise.reject(globalThis, value);
- } else {
- promise.resolve(globalThis, value);
- }
- },
- .err => |err| {
- promise.reject(globalThis, err.toErrorInstance(globalThis));
- },
- }
- }
- };
- }
-
- pub const WriteFilePromise = struct {
- promise: JSPromise.Strong = .{},
- globalThis: *JSGlobalObject,
- pub fn run(handler: *@This(), count: Blob.Store.WriteFile.ResultType) void {
- var promise = handler.promise.swap();
- var globalThis = handler.globalThis;
- bun.default_allocator.destroy(handler);
- const value = promise.asValue(globalThis);
- value.ensureStillAlive();
- switch (count) {
- .err => |err| {
- promise.reject(globalThis, err.toErrorInstance(globalThis));
- },
- .result => |wrote| {
- promise.resolve(globalThis, JSC.JSValue.jsNumberFromUint64(wrote));
- },
- }
- }
- };
-
- pub fn NewInternalReadFileHandler(comptime Context: type, comptime Function: anytype) type {
- return struct {
- pub fn run(handler: *anyopaque, bytes_: Store.ReadFile.ResultType) void {
- Function(bun.cast(Context, handler), bytes_);
- }
- };
- }
-
- pub fn doReadFileInternal(this: *Blob, comptime Handler: type, ctx: Handler, comptime Function: anytype, global: *JSGlobalObject) void {
- var file_read = Store.ReadFile.createWithCtx(
- bun.default_allocator,
- this.store.?,
- ctx,
- NewInternalReadFileHandler(Handler, Function).run,
- this.offset,
- this.size,
- ) catch unreachable;
- var read_file_task = Store.ReadFile.ReadFileTask.createOnJSThread(bun.default_allocator, global, file_read) catch unreachable;
- read_file_task.schedule();
- }
-
- pub fn doReadFile(this: *Blob, comptime Function: anytype, global: *JSGlobalObject) JSValue {
- const Handler = NewReadFileHandler(Function);
- var promise = JSPromise.create(global);
-
- var handler = Handler{
- .context = this.*,
- .globalThis = global,
- };
- const promise_value = promise.asValue(global);
- promise_value.ensureStillAlive();
- handler.promise.strong.set(global, promise_value);
-
- 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_value;
- }
-
- pub fn needsToReadFile(this: *const Blob) bool {
- return this.store != null and this.store.?.data == .file;
- }
-
- pub fn toStringWithBytes(this: *Blob, global: *JSGlobalObject, buf: []const u8, comptime lifetime: Lifetime) JSValue {
- // null == unknown
- // false == can't be
- const could_be_all_ascii = this.is_all_ascii orelse this.store.?.is_all_ascii;
-
- if (could_be_all_ascii == null or !could_be_all_ascii.?) {
- // if toUTF16Alloc returns null, it means there are no non-ASCII characters
- // instead of erroring, invalid characters will become a U+FFFD replacement character
- if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch unreachable) |external| {
- if (lifetime != .temporary)
- this.setIsASCIIFlag(false);
-
- if (lifetime == .transfer) {
- this.detach();
- }
-
- if (lifetime == .temporary) {
- bun.default_allocator.free(bun.constStrToU8(buf));
- }
-
- return ZigString.toExternalU16(external.ptr, external.len, global);
- }
-
- if (lifetime != .temporary) this.setIsASCIIFlag(true);
- }
-
- if (buf.len == 0) {
- return ZigString.Empty.toValue(global);
- }
-
- switch (comptime lifetime) {
- // strings are immutable
- // we don't need to clone
- .clone => {
- this.store.?.ref();
- return ZigString.init(buf).external(global, this.store.?, Store.external);
- },
- .transfer => {
- var store = this.store.?;
- std.debug.assert(store.data == .bytes);
- this.transfer();
- return ZigString.init(buf).external(global, store, Store.external);
- },
- // strings are immutable
- // sharing isn't really a thing
- .share => {
- this.store.?.ref();
- return ZigString.init(buf).external(global, this.store.?, Store.external);
- },
- .temporary => {
- return ZigString.init(buf).toExternalValue(global);
- },
- }
- }
-
- pub fn toString(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
- if (this.needsToReadFile()) {
- return this.doReadFile(toStringWithBytes, global);
- }
-
- const view_: []u8 =
- bun.constStrToU8(this.sharedView());
-
- if (view_.len == 0)
- return ZigString.Empty.toValue(global);
-
- return toStringWithBytes(this, global, view_, lifetime);
- }
-
- pub fn toJSON(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
- if (this.needsToReadFile()) {
- return this.doReadFile(toJSONWithBytes, global);
- }
-
- var view_ = this.sharedView();
-
- if (view_.len == 0)
- return ZigString.Empty.toValue(global);
-
- return toJSONWithBytes(this, global, view_, lifetime);
- }
-
- pub fn toJSONWithBytes(this: *Blob, global: *JSGlobalObject, buf: []const u8, comptime lifetime: Lifetime) JSValue {
- // null == unknown
- // false == can't be
- const could_be_all_ascii = this.is_all_ascii orelse this.store.?.is_all_ascii;
- defer if (comptime lifetime == .temporary) bun.default_allocator.free(bun.constStrToU8(buf));
-
- if (could_be_all_ascii == null or !could_be_all_ascii.?) {
- var stack_fallback = std.heap.stackFallback(4096, bun.default_allocator);
- const allocator = stack_fallback.get();
- // if toUTF16Alloc returns null, it means there are no non-ASCII characters
- if (strings.toUTF16Alloc(allocator, buf, false) catch null) |external| {
- if (comptime lifetime != .temporary) this.setIsASCIIFlag(false);
- const result = ZigString.init16(external).toJSONObject(global);
- allocator.free(external);
- return result;
- }
-
- if (comptime lifetime != .temporary) this.setIsASCIIFlag(true);
- }
-
- if (comptime lifetime == .temporary) {
- return ZigString.init(buf).toJSONObject(global);
- } else {
- return ZigString.init(buf).toJSONObject(global);
- }
- }
-
- pub fn toArrayBufferWithBytes(this: *Blob, global: *JSGlobalObject, buf: []u8, comptime lifetime: Lifetime) JSValue {
- switch (comptime lifetime) {
- .clone => {
- return JSC.ArrayBuffer.create(global, buf, .ArrayBuffer);
- },
- .share => {
- this.store.?.ref();
- return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJSWithContext(
- global,
- this.store.?,
- JSC.BlobArrayBuffer_deallocator,
- null,
- );
- },
- .transfer => {
- var store = this.store.?;
- this.transfer();
- return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJSWithContext(
- global,
- store,
- JSC.BlobArrayBuffer_deallocator,
- null,
- );
- },
- .temporary => {
- return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJS(
- global,
- null,
- );
- },
- }
- }
-
- pub fn toArrayBuffer(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
- if (this.needsToReadFile()) {
- return this.doReadFile(toArrayBufferWithBytes, global);
- }
-
- var view_ = this.sharedView();
-
- if (view_.len == 0)
- return JSC.ArrayBuffer.create(global, "", .ArrayBuffer);
-
- return toArrayBufferWithBytes(this, global, bun.constStrToU8(view_), lifetime);
- }
-
- pub inline fn get(
- global: *JSGlobalObject,
- arg: JSValue,
- comptime move: bool,
- comptime require_array: bool,
- ) anyerror!Blob {
- return fromJSMovable(global, arg, move, require_array);
- }
-
- pub inline fn fromJSMove(global: *JSGlobalObject, arg: JSValue) anyerror!Blob {
- return fromJSWithoutDeferGC(global, arg, true, false);
- }
-
- pub inline fn fromJSClone(global: *JSGlobalObject, arg: JSValue) anyerror!Blob {
- return fromJSWithoutDeferGC(global, arg, false, true);
- }
-
- pub inline fn fromJSCloneOptionalArray(global: *JSGlobalObject, arg: JSValue) anyerror!Blob {
- return fromJSWithoutDeferGC(global, arg, false, false);
- }
-
- fn fromJSMovable(
- global: *JSGlobalObject,
- arg: JSValue,
- comptime move: bool,
- comptime require_array: bool,
- ) anyerror!Blob {
- const FromJSFunction = if (comptime move and !require_array)
- fromJSMove
- else if (!require_array)
- fromJSCloneOptionalArray
- else
- fromJSClone;
-
- return FromJSFunction(global, arg);
- }
-
- fn fromJSWithoutDeferGC(
- global: *JSGlobalObject,
- arg: JSValue,
- comptime move: bool,
- comptime require_array: bool,
- ) anyerror!Blob {
- var current = arg;
- if (current.isUndefinedOrNull()) {
- return Blob{ .globalThis = global };
- }
-
- var top_value = current;
- var might_only_be_one_thing = false;
- arg.ensureStillAlive();
- defer arg.ensureStillAlive();
- switch (current.jsTypeLoose()) {
- .Array, .DerivedArray => {
- var top_iter = JSC.JSArrayIterator.init(current, global);
- might_only_be_one_thing = top_iter.len == 1;
- if (top_iter.len == 0) {
- return Blob{ .globalThis = global };
- }
- if (might_only_be_one_thing) {
- top_value = top_iter.next().?;
- }
- },
- else => {
- might_only_be_one_thing = true;
- if (require_array) {
- return error.InvalidArguments;
- }
- },
- }
-
- if (might_only_be_one_thing or !move) {
-
- // Fast path: one item, we don't need to join
- switch (top_value.jsTypeLoose()) {
- .Cell,
- .NumberObject,
- JSC.JSValue.JSType.String,
- JSC.JSValue.JSType.StringObject,
- JSC.JSValue.JSType.DerivedStringObject,
- => {
- var sliced = top_value.toSlice(global, bun.default_allocator);
- const is_all_ascii = !sliced.isAllocated();
- if (!sliced.isAllocated() and sliced.len > 0) {
- sliced.ptr = @ptrCast([*]const u8, (try bun.default_allocator.dupe(u8, sliced.slice())).ptr);
- sliced.allocator = NullableAllocator.init(bun.default_allocator);
- }
-
- return Blob.initWithAllASCII(bun.constStrToU8(sliced.slice()), bun.default_allocator, global, is_all_ascii);
- },
-
- JSC.JSValue.JSType.ArrayBuffer,
- JSC.JSValue.JSType.Int8Array,
- JSC.JSValue.JSType.Uint8Array,
- JSC.JSValue.JSType.Uint8ClampedArray,
- JSC.JSValue.JSType.Int16Array,
- JSC.JSValue.JSType.Uint16Array,
- JSC.JSValue.JSType.Int32Array,
- JSC.JSValue.JSType.Uint32Array,
- JSC.JSValue.JSType.Float32Array,
- JSC.JSValue.JSType.Float64Array,
- JSC.JSValue.JSType.BigInt64Array,
- JSC.JSValue.JSType.BigUint64Array,
- JSC.JSValue.JSType.DataView,
- => {
- var buf = try bun.default_allocator.dupe(u8, top_value.asArrayBuffer(global).?.byteSlice());
-
- return Blob.init(buf, bun.default_allocator, global);
- },
-
- .DOMWrapper => {
- if (top_value.as(Blob)) |blob| {
- if (comptime move) {
- var _blob = blob.*;
- _blob.allocator = null;
- blob.transfer();
- return _blob;
- } else {
- return blob.dupe();
- }
- }
- },
-
- else => {},
- }
- }
-
- var stack_allocator = std.heap.stackFallback(1024, bun.default_allocator);
- var stack_mem_all = stack_allocator.get();
- var stack: std.ArrayList(JSValue) = std.ArrayList(JSValue).init(stack_mem_all);
- var joiner = StringJoiner{ .use_pool = false, .node_allocator = stack_mem_all };
- var could_have_non_ascii = false;
-
- defer if (stack_allocator.fixed_buffer_allocator.end_index >= 1024) stack.deinit();
-
- while (true) {
- switch (current.jsTypeLoose()) {
- .NumberObject,
- JSC.JSValue.JSType.String,
- JSC.JSValue.JSType.StringObject,
- JSC.JSValue.JSType.DerivedStringObject,
- => {
- var sliced = current.toSlice(global, bun.default_allocator);
- const allocator = sliced.allocator.get();
- could_have_non_ascii = could_have_non_ascii or allocator != null;
- joiner.append(
- sliced.slice(),
- 0,
- allocator,
- );
- },
-
- .Array, .DerivedArray => {
- var iter = JSC.JSArrayIterator.init(current, global);
- try stack.ensureUnusedCapacity(iter.len);
- var any_arrays = false;
- while (iter.next()) |item| {
- if (item.isUndefinedOrNull()) continue;
-
- // When it's a string or ArrayBuffer inside an array, we can avoid the extra push/pop
- // we only really want this for nested arrays
- // However, we must preserve the order
- // That means if there are any arrays
- // we have to restart the loop
- if (!any_arrays) {
- switch (item.jsTypeLoose()) {
- .NumberObject,
- .Cell,
- JSC.JSValue.JSType.String,
- JSC.JSValue.JSType.StringObject,
- JSC.JSValue.JSType.DerivedStringObject,
- => {
- var sliced = item.toSlice(global, bun.default_allocator);
- const allocator = sliced.allocator.get();
- could_have_non_ascii = could_have_non_ascii or allocator != null;
- joiner.append(
- sliced.slice(),
- 0,
- allocator,
- );
- continue;
- },
- JSC.JSValue.JSType.ArrayBuffer,
- JSC.JSValue.JSType.Int8Array,
- JSC.JSValue.JSType.Uint8Array,
- JSC.JSValue.JSType.Uint8ClampedArray,
- JSC.JSValue.JSType.Int16Array,
- JSC.JSValue.JSType.Uint16Array,
- JSC.JSValue.JSType.Int32Array,
- JSC.JSValue.JSType.Uint32Array,
- JSC.JSValue.JSType.Float32Array,
- JSC.JSValue.JSType.Float64Array,
- JSC.JSValue.JSType.BigInt64Array,
- JSC.JSValue.JSType.BigUint64Array,
- JSC.JSValue.JSType.DataView,
- => {
- could_have_non_ascii = true;
- var buf = item.asArrayBuffer(global).?;
- joiner.append(buf.byteSlice(), 0, null);
- continue;
- },
- .Array, .DerivedArray => {
- any_arrays = true;
- could_have_non_ascii = true;
- break;
- },
-
- .DOMWrapper => {
- if (item.as(Blob)) |blob| {
- could_have_non_ascii = could_have_non_ascii or !(blob.is_all_ascii orelse false);
- joiner.append(blob.sharedView(), 0, null);
- continue;
- }
- },
- else => {},
- }
- }
-
- stack.appendAssumeCapacity(item);
- }
- },
-
- .DOMWrapper => {
- if (current.as(Blob)) |blob| {
- could_have_non_ascii = could_have_non_ascii or !(blob.is_all_ascii orelse false);
- joiner.append(blob.sharedView(), 0, null);
- }
- },
-
- JSC.JSValue.JSType.ArrayBuffer,
- JSC.JSValue.JSType.Int8Array,
- JSC.JSValue.JSType.Uint8Array,
- JSC.JSValue.JSType.Uint8ClampedArray,
- JSC.JSValue.JSType.Int16Array,
- JSC.JSValue.JSType.Uint16Array,
- JSC.JSValue.JSType.Int32Array,
- JSC.JSValue.JSType.Uint32Array,
- JSC.JSValue.JSType.Float32Array,
- JSC.JSValue.JSType.Float64Array,
- JSC.JSValue.JSType.BigInt64Array,
- JSC.JSValue.JSType.BigUint64Array,
- JSC.JSValue.JSType.DataView,
- => {
- var buf = current.asArrayBuffer(global).?;
- joiner.append(buf.slice(), 0, null);
- could_have_non_ascii = true;
- },
-
- else => {
- var sliced = current.toSlice(global, bun.default_allocator);
- const allocator = sliced.allocator.get();
- could_have_non_ascii = could_have_non_ascii or allocator != null;
- joiner.append(
- sliced.slice(),
- 0,
- allocator,
- );
- },
- }
- current = stack.popOrNull() orelse break;
- }
-
- var joined = try joiner.done(bun.default_allocator);
-
- if (!could_have_non_ascii) {
- return Blob.initWithAllASCII(joined, bun.default_allocator, global, true);
- }
- return Blob.init(joined, bun.default_allocator, global);
- }
-};
-
-pub const AnyBlob = union(enum) {
- Blob: Blob,
- // InlineBlob: InlineBlob,
- InternalBlob: InternalBlob,
-
- pub fn toJSON(this: *AnyBlob, global: *JSGlobalObject, comptime lifetime: JSC.WebCore.Lifetime) JSValue {
- switch (this.*) {
- .Blob => return this.Blob.toJSON(global, lifetime),
- // .InlineBlob => {
- // if (this.InlineBlob.len == 0) {
- // return JSValue.jsNull();
- // }
- // var str = this.InlineBlob.toStringOwned(global);
- // return str.parseJSON(global);
- // },
- .InternalBlob => {
- if (this.InternalBlob.bytes.items.len == 0) {
- return JSValue.jsNull();
- }
-
- const str = this.InternalBlob.toJSON(global);
-
- // the GC will collect the string
- this.* = .{
- .Blob = .{},
- };
-
- return str;
- },
- }
- }
-
- pub fn toString(this: *AnyBlob, global: *JSGlobalObject, comptime lifetime: JSC.WebCore.Lifetime) JSValue {
- switch (this.*) {
- .Blob => return this.Blob.toString(global, lifetime),
- // .InlineBlob => {
- // const owned = this.InlineBlob.toStringOwned(global);
- // this.* = .{ .InlineBlob = .{ .len = 0 } };
- // return owned;
- // },
- .InternalBlob => {
- if (this.InternalBlob.bytes.items.len == 0) {
- return ZigString.Empty.toValue(global);
- }
-
- const owned = this.InternalBlob.toStringOwned(global);
- this.* = .{ .Blob = .{} };
- return owned;
- },
- }
- }
-
- pub fn toArrayBuffer(this: *AnyBlob, global: *JSGlobalObject, comptime lifetime: JSC.WebCore.Lifetime) JSValue {
- switch (this.*) {
- .Blob => return this.Blob.toArrayBuffer(global, lifetime),
- // .InlineBlob => {
- // if (this.InlineBlob.len == 0) {
- // return JSC.ArrayBuffer.empty.toJS(global, null);
- // }
- // var bytes = this.InlineBlob.sliceConst();
- // this.InlineBlob.len = 0;
- // const value = JSC.ArrayBuffer.create(
- // global,
- // bytes,
- // .ArrayBuffer,
- // );
- // return value;
- // },
- .InternalBlob => {
- if (this.InternalBlob.bytes.items.len == 0) {
- return JSC.ArrayBuffer.create(global, "", .ArrayBuffer);
- }
-
- var bytes = this.InternalBlob.toOwnedSlice();
- this.* = .{ .Blob = .{} };
- const value = JSC.ArrayBuffer.fromBytes(
- bytes,
- .ArrayBuffer,
- );
- return value.toJS(global, null);
- },
- }
- }
-
- pub inline fn size(this: *const AnyBlob) Blob.SizeType {
- return switch (this.*) {
- .Blob => this.Blob.size,
- else => @truncate(Blob.SizeType, this.slice().len),
- };
- }
-
- pub fn from(this: *AnyBlob, list: std.ArrayList(u8)) void {
- this.* = .{
- .InternalBlob = InternalBlob{
- .bytes = list,
- },
- };
- }
-
- pub fn isDetached(this: *const AnyBlob) bool {
- return switch (this.*) {
- .Blob => |blob| blob.isDetached(),
- else => this.slice().len == 0,
- };
- }
-
- pub fn store(this: *const @This()) ?*Blob.Store {
- if (this.* == .Blob) {
- return this.Blob.store;
- }
-
- return null;
- }
-
- pub fn contentType(self: *const @This()) []const u8 {
- return switch (self.*) {
- .Blob => self.Blob.content_type,
- // .InlineBlob => self.InlineBlob.contentType(),
- .InternalBlob => self.InternalBlob.contentType(),
- };
- }
-
- pub fn wasString(self: *const @This()) bool {
- return switch (self.*) {
- .Blob => self.Blob.is_all_ascii orelse false,
- // .InlineBlob => self.InlineBlob.was_string,
- .InternalBlob => self.InternalBlob.was_string,
- };
- }
-
- pub inline fn slice(self: *const @This()) []const u8 {
- return switch (self.*) {
- .Blob => self.Blob.sharedView(),
- // .InlineBlob => self.InlineBlob.sliceConst(),
- .InternalBlob => self.InternalBlob.sliceConst(),
- };
- }
-
- pub fn needsToReadFile(self: *const @This()) bool {
- return switch (self.*) {
- .Blob => self.Blob.needsToReadFile(),
- // .InlineBlob => false,
- .InternalBlob => false,
- };
- }
-
- pub fn detach(self: *@This()) void {
- return switch (self.*) {
- .Blob => {
- self.Blob.detach();
- self.* = .{
- .Blob = .{},
- };
- },
- // .InlineBlob => {
- // self.InlineBlob.len = 0;
- // },
- .InternalBlob => {
- self.InternalBlob.bytes.clearAndFree();
- self.* = .{
- .Blob = .{},
- };
- },
- };
- }
-};
-
-/// A single-use Blob
-pub const InternalBlob = struct {
- bytes: std.ArrayList(u8),
- was_string: bool = false,
-
- pub fn toStringOwned(this: *@This(), globalThis: *JSC.JSGlobalObject) JSValue {
- if (strings.toUTF16Alloc(globalThis.allocator(), this.bytes.items, false) catch &[_]u16{}) |out| {
- const return_value = ZigString.toExternalU16(out.ptr, out.len, globalThis);
- return_value.ensureStillAlive();
- this.deinit();
- return return_value;
- } else {
- var str = ZigString.init(this.toOwnedSlice());
- str.mark();
- return str.toExternalValue(globalThis);
- }
- }
-
- pub fn toJSON(this: *@This(), globalThis: *JSC.JSGlobalObject) JSValue {
- const str_bytes = ZigString.init(this.bytes.items).withEncoding();
- const json = str_bytes.toJSONObject(globalThis);
- this.deinit();
- return json;
- }
-
- pub inline fn sliceConst(this: *const @This()) []const u8 {
- return this.bytes.items;
- }
-
- pub fn deinit(this: *@This()) void {
- this.bytes.clearAndFree();
- }
-
- pub inline fn slice(this: @This()) []u8 {
- return this.bytes.items;
- }
-
- pub fn toOwnedSlice(this: *@This()) []u8 {
- var bytes = this.bytes.items;
- this.bytes.items = &.{};
- this.bytes.capacity = 0;
- return bytes;
- }
-
- pub fn clearAndFree(this: *@This()) void {
- this.bytes.clearAndFree();
- }
-
- pub fn contentType(self: *const @This()) []const u8 {
- if (self.was_string) {
- return MimeType.text.value;
- }
-
- return MimeType.other.value;
- }
-};
-
-/// A blob which stores all the data in the same space as a real Blob
-/// This is an optimization for small Response and Request bodies
-/// It means that we can avoid an additional heap allocation for a small response
-pub const InlineBlob = extern struct {
- const real_blob_size = @sizeOf(Blob);
- pub const IntSize = u8;
- pub const available_bytes = real_blob_size - @sizeOf(IntSize) - 1 - 1;
- bytes: [available_bytes]u8 align(1) = undefined,
- len: IntSize align(1) = 0,
- was_string: bool align(1) = false,
-
- pub fn concat(first: []const u8, second: []const u8) InlineBlob {
- const total = first.len + second.len;
- std.debug.assert(total <= available_bytes);
-
- var inline_blob: JSC.WebCore.InlineBlob = .{};
- var bytes_slice = inline_blob.bytes[0..total];
-
- if (first.len > 0)
- @memcpy(bytes_slice.ptr, first.ptr, first.len);
-
- if (second.len > 0)
- @memcpy(bytes_slice.ptr + first.len, second.ptr, second.len);
-
- inline_blob.len = @truncate(@TypeOf(inline_blob.len), total);
- return inline_blob;
- }
-
- fn internalInit(data: []const u8, was_string: bool) InlineBlob {
- std.debug.assert(data.len <= available_bytes);
-
- var blob = InlineBlob{
- .len = @intCast(IntSize, data.len),
- .was_string = was_string,
- };
-
- if (data.len > 0)
- @memcpy(&blob.bytes, data.ptr, data.len);
- return blob;
- }
-
- pub fn init(data: []const u8) InlineBlob {
- return internalInit(data, false);
- }
-
- pub fn initString(data: []const u8) InlineBlob {
- return internalInit(data, true);
- }
-
- pub fn toStringOwned(this: *@This(), globalThis: *JSC.JSGlobalObject) JSValue {
- if (this.len == 0)
- return ZigString.Empty.toValue(globalThis);
-
- var str = ZigString.init(this.sliceConst());
-
- if (!strings.isAllASCII(this.sliceConst())) {
- str.markUTF8();
- }
-
- const out = str.toValueGC(globalThis);
- out.ensureStillAlive();
- this.len = 0;
- return out;
- }
-
- pub fn contentType(self: *const @This()) []const u8 {
- if (self.was_string) {
- return MimeType.text.value;
- }
-
- return MimeType.other.value;
- }
-
- pub fn deinit(_: *@This()) void {}
-
- pub inline fn slice(this: *@This()) []u8 {
- return this.bytes[0..this.len];
- }
-
- pub inline fn sliceConst(this: *const @This()) []const u8 {
- return this.bytes[0..this.len];
- }
-
- pub fn toOwnedSlice(this: *@This()) []u8 {
- return this.slice();
- }
-
- pub fn clearAndFree(_: *@This()) void {}
-};
-
-// https://developer.mozilla.org/en-US/docs/Web/API/Body
-pub const Body = struct {
- init: Init = Init{ .headers = null, .status_code = 200 },
- value: Value, // = Value.empty,
-
- pub inline fn len(this: *const Body) Blob.SizeType {
- return this.value.size();
- }
-
- pub fn slice(this: *const Body) []const u8 {
- return this.value.slice();
- }
-
- pub fn use(this: *Body) Blob {
- return this.value.use();
- }
-
- pub fn clone(this: *Body, globalThis: *JSGlobalObject) Body {
- return Body{
- .init = this.init.clone(globalThis),
- .value = this.value.clone(globalThis),
- };
- }
-
- pub fn writeFormat(this: *const Body, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void {
- const Writer = @TypeOf(writer);
-
- try formatter.writeIndent(Writer, writer);
- try writer.writeAll("bodyUsed: ");
- formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.value == .Used), .BooleanObject, enable_ansi_colors);
- formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
- try writer.writeAll("\n");
-
- // if (this.init.headers) |headers| {
- // try formatter.writeIndent(Writer, writer);
- // try writer.writeAll("headers: ");
- // try headers.leak().writeFormat(formatter, writer, comptime enable_ansi_colors);
- // try writer.writeAll("\n");
- // }
-
- try formatter.writeIndent(Writer, writer);
- try writer.writeAll("status: ");
- formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(this.init.status_code), .NumberObject, enable_ansi_colors);
- if (this.value == .Blob) {
- try formatter.printComma(Writer, writer, enable_ansi_colors);
- try writer.writeAll("\n");
- try formatter.writeIndent(Writer, writer);
- try this.value.Blob.writeFormat(formatter, writer, enable_ansi_colors);
- } else if (this.value == .InternalBlob) {
- try formatter.printComma(Writer, writer, enable_ansi_colors);
- try writer.writeAll("\n");
- try formatter.writeIndent(Writer, writer);
- try Blob.writeFormatForSize(this.value.size(), writer, enable_ansi_colors);
- } else if (this.value == .Locked) {
- if (this.value.Locked.readable) |stream| {
- try formatter.printComma(Writer, writer, enable_ansi_colors);
- try writer.writeAll("\n");
- try formatter.writeIndent(Writer, writer);
- formatter.printAs(.Object, Writer, writer, stream.value, stream.value.jsType(), enable_ansi_colors);
- }
- }
- }
-
- pub fn deinit(this: *Body, _: std.mem.Allocator) void {
- if (this.init.headers) |headers| {
- this.init.headers = null;
-
- headers.deref();
- }
- this.value.deinit();
- }
-
- pub const Init = struct {
- headers: ?*FetchHeaders = null,
- status_code: u16,
- method: Method = Method.GET,
-
- pub fn clone(this: Init, _: *JSGlobalObject) Init {
- var that = this;
- var headers = this.headers;
- if (headers) |head| {
- that.headers = head.cloneThis();
- }
-
- return that;
- }
-
- pub fn init(allocator: std.mem.Allocator, ctx: *JSGlobalObject, response_init: JSC.JSValue, js_type: JSC.JSValue.JSType) !?Init {
- var result = Init{ .status_code = 200 };
-
- if (!response_init.isCell())
- return null;
-
- if (js_type == .DOMWrapper) {
- // fast path: it's a Request object or a Response object
- // we can skip calling JS getters
- if (response_init.as(Request)) |req| {
- if (req.headers) |headers| {
- result.headers = headers.cloneThis();
- }
-
- result.method = req.method;
- return result;
- }
-
- if (response_init.as(Response)) |req| {
- return req.body.init.clone(ctx);
- }
- }
-
- if (response_init.fastGet(ctx, .headers)) |headers| {
- if (headers.as(FetchHeaders)) |orig| {
- result.headers = orig.cloneThis();
- } else {
- result.headers = FetchHeaders.createFromJS(ctx.ptr(), headers);
- }
- }
-
- if (response_init.fastGet(ctx, .status)) |status_value| {
- const number = status_value.to(i32);
- if (number > 0)
- result.status_code = @truncate(u16, @intCast(u32, number));
- }
-
- if (response_init.fastGet(ctx, .method)) |method_value| {
- var method_str = method_value.toSlice(ctx, allocator);
- defer method_str.deinit();
- if (method_str.len > 0) {
- result.method = Method.which(method_str.slice()) orelse .GET;
- }
- }
-
- if (result.headers == null and result.status_code < 200) return null;
- return result;
- }
- };
-
- pub const PendingValue = struct {
- promise: ?JSValue = null,
- readable: ?JSC.WebCore.ReadableStream = null,
- // writable: JSC.WebCore.Sink
-
- global: *JSGlobalObject,
- task: ?*anyopaque = null,
-
- /// runs after the data is available.
- onReceiveValue: ?*const fn (ctx: *anyopaque, value: *Value) void = null,
-
- /// conditionally runs when requesting data
- /// used in HTTP server to ignore request bodies unless asked for it
- onStartBuffering: ?*const fn (ctx: *anyopaque) void = null,
-
- onStartStreaming: ?*const fn (ctx: *anyopaque) JSC.WebCore.DrainResult = null,
-
- deinit: bool = false,
- action: Action = Action.none,
-
- pub fn toAnyBlob(this: *PendingValue) ?AnyBlob {
- if (this.promise != null)
- return null;
-
- return this.toAnyBlobAllowPromise();
- }
-
- pub fn toAnyBlobAllowPromise(this: *PendingValue) ?AnyBlob {
- var stream = if (this.readable != null) &this.readable.? else return null;
-
- if (stream.toAnyBlob(this.global)) |blob| {
- this.readable = null;
- return blob;
- }
-
- return null;
- }
-
- pub fn setPromise(value: *PendingValue, globalThis: *JSC.JSGlobalObject, action: Action) JSValue {
- value.action = action;
-
- if (value.readable) |readable| {
- // switch (readable.ptr) {
- // .JavaScript
- // }
- switch (action) {
- .getText, .getJSON, .getBlob, .getArrayBuffer => {
- switch (readable.ptr) {
- .Blob => unreachable,
- else => {},
- }
- value.promise = switch (action) {
- .getJSON => globalThis.readableStreamToJSON(readable.value),
- .getArrayBuffer => globalThis.readableStreamToArrayBuffer(readable.value),
- .getText => globalThis.readableStreamToText(readable.value),
- .getBlob => globalThis.readableStreamToBlob(readable.value),
- else => unreachable,
- };
- value.promise.?.ensureStillAlive();
- readable.value.unprotect();
-
- // js now owns the memory
- value.readable = null;
-
- return value.promise.?;
- },
- .none => {},
- }
- }
-
- {
- var promise = JSC.JSPromise.create(globalThis);
- const promise_value = promise.asValue(globalThis);
- value.promise = promise_value;
-
- if (value.onStartBuffering) |onStartBuffering| {
- value.onStartBuffering = null;
- onStartBuffering(value.task.?);
- }
- return promise_value;
- }
- }
-
- pub const Action = enum {
- none,
- getText,
- getJSON,
- getArrayBuffer,
- getBlob,
- };
- };
-
- /// This is a duplex stream!
- pub const Value = union(Tag) {
- Blob: Blob,
- /// Single-use Blob
- /// Avoids a heap allocation.
- InternalBlob: InternalBlob,
- /// Single-use Blob that stores the bytes in the Value itself.
- // InlineBlob: InlineBlob,
- Locked: PendingValue,
- Used: void,
- Empty: void,
- Error: JSValue,
-
- pub fn toBlobIfPossible(this: *Value) void {
- if (this.* != .Locked)
- return;
-
- if (this.Locked.toAnyBlob()) |blob| {
- this.* = switch (blob) {
- .Blob => .{ .Blob = blob.Blob },
- .InternalBlob => .{ .InternalBlob = blob.InternalBlob },
- // .InlineBlob => .{ .InlineBlob = blob.InlineBlob },
- };
- }
- }
-
- pub fn size(this: *const Value) Blob.SizeType {
- return switch (this.*) {
- .Blob => this.Blob.size,
- .InternalBlob => @truncate(Blob.SizeType, this.InternalBlob.sliceConst().len),
- // .InlineBlob => @truncate(Blob.SizeType, this.InlineBlob.sliceConst().len),
- else => 0,
- };
- }
-
- pub fn estimatedSize(this: *const Value) usize {
- return switch (this.*) {
- .InternalBlob => this.InternalBlob.sliceConst().len,
- // .InlineBlob => this.InlineBlob.sliceConst().len,
- else => 0,
- };
- }
-
- pub fn createBlobValue(data: []u8, allocator: std.mem.Allocator, was_string: bool) Value {
- // if (data.len <= InlineBlob.available_bytes) {
- // var _blob = InlineBlob{
- // .bytes = undefined,
- // .was_string = was_string,
- // .len = @truncate(InlineBlob.IntSize, data.len),
- // };
- // @memcpy(&_blob.bytes, data.ptr, data.len);
- // allocator.free(data);
- // return Value{
- // .InlineBlob = _blob,
- // };
- // }
-
- return Value{
- .InternalBlob = InternalBlob{
- .bytes = std.ArrayList(u8).fromOwnedSlice(allocator, data),
- .was_string = was_string,
- },
- };
- }
-
- pub const Tag = enum {
- Blob,
- InternalBlob,
- // InlineBlob,
- Locked,
- Used,
- Empty,
- Error,
- };
-
- // pub const empty = Value{ .Empty = void{} };
-
- pub fn toReadableStream(this: *Value, globalThis: *JSGlobalObject) JSValue {
- JSC.markBinding(@src());
-
- switch (this.*) {
- .Used, .Empty => {
- return JSC.WebCore.ReadableStream.empty(globalThis);
- },
- .InternalBlob,
- .Blob,
- // .InlineBlob,
- => {
- var blob = this.use();
- defer blob.detach();
- blob.resolveSize();
- const value = JSC.WebCore.ReadableStream.fromBlob(globalThis, &blob, blob.size);
-
- this.* = .{
- .Locked = .{
- .readable = JSC.WebCore.ReadableStream.fromJS(value, globalThis).?,
- .global = globalThis,
- },
- };
- this.Locked.readable.?.value.protect();
-
- return value;
- },
- .Locked => {
- var locked = &this.Locked;
- if (locked.readable) |readable| {
- return readable.value;
- }
- var drain_result: JSC.WebCore.DrainResult = .{
- .estimated_size = 0,
- };
-
- if (locked.onStartStreaming) |drain| {
- locked.onStartStreaming = null;
- drain_result = drain(locked.task.?);
- }
-
- if (drain_result == .empty or drain_result == .aborted) {
- this.* = .{ .Empty = void{} };
- return JSC.WebCore.ReadableStream.empty(globalThis);
- }
-
- var reader = bun.default_allocator.create(JSC.WebCore.ByteStream.Source) catch unreachable;
- reader.* = .{
- .context = undefined,
- .globalThis = globalThis,
- };
-
- reader.context.setup();
-
- if (drain_result == .estimated_size) {
- reader.context.highWaterMark = @truncate(Blob.SizeType, drain_result.estimated_size);
- reader.context.size_hint = @truncate(Blob.SizeType, drain_result.estimated_size);
- } else if (drain_result == .owned) {
- reader.context.buffer = drain_result.owned.list;
- reader.context.size_hint = @truncate(Blob.SizeType, drain_result.owned.size_hint);
- }
-
- locked.readable = .{
- .ptr = .{ .Bytes = &reader.context },
- .value = reader.toJS(globalThis),
- };
-
- locked.readable.?.value.protect();
- return locked.readable.?.value;
- },
-
- else => unreachable,
- }
- }
-
- pub fn fromJS(globalThis: *JSGlobalObject, value: JSValue) ?Value {
- value.ensureStillAlive();
-
- if (value.isEmptyOrUndefinedOrNull()) {
- return Body.Value{
- .Empty = void{},
- };
- }
-
- const js_type = value.jsType();
-
- if (js_type.isStringLike()) {
- var str = value.getZigString(globalThis);
- if (str.len == 0) {
- return Body.Value{
- .Empty = {},
- };
- }
-
- // if (str.is16Bit()) {
- // if (str.maxUTF8ByteLength() < InlineBlob.available_bytes or
- // (str.len <= InlineBlob.available_bytes and str.utf8ByteLength() <= InlineBlob.available_bytes))
- // {
- // var blob = InlineBlob{
- // .was_string = true,
- // .bytes = undefined,
- // .len = 0,
- // };
- // if (comptime Environment.allow_assert) {
- // std.debug.assert(str.utf8ByteLength() <= InlineBlob.available_bytes);
- // }
-
- // const result = strings.copyUTF16IntoUTF8(
- // blob.bytes[0..blob.bytes.len],
- // []const u16,
- // str.utf16SliceAligned(),
- // );
- // blob.len = @intCast(InlineBlob.IntSize, result.written);
- // std.debug.assert(@as(usize, result.read) == str.len);
- // std.debug.assert(@as(usize, result.written) <= InlineBlob.available_bytes);
-
- // return Body.Value{
- // .InlineBlob = blob,
- // };
- // }
- // } else {
- // if (str.maxUTF8ByteLength() <= InlineBlob.available_bytes or
- // (str.len <= InlineBlob.available_bytes and str.utf8ByteLength() <= InlineBlob.available_bytes))
- // {
- // var blob = InlineBlob{
- // .was_string = true,
- // .bytes = undefined,
- // .len = 0,
- // };
- // if (comptime Environment.allow_assert) {
- // std.debug.assert(str.utf8ByteLength() <= InlineBlob.available_bytes);
- // }
- // const result = strings.copyLatin1IntoUTF8(
- // blob.bytes[0..blob.bytes.len],
- // []const u8,
- // str.slice(),
- // );
- // blob.len = @intCast(InlineBlob.IntSize, result.written);
- // std.debug.assert(@as(usize, result.read) == str.len);
- // std.debug.assert(@as(usize, result.written) <= InlineBlob.available_bytes);
- // return Body.Value{
- // .InlineBlob = blob,
- // };
- // }
- // }
-
- var buffer = str.toOwnedSlice(bun.default_allocator) catch {
- globalThis.vm().throwError(globalThis, ZigString.static("Failed to clone string").toErrorInstance(globalThis));
- return null;
- };
-
- return Body.Value{
- .InternalBlob = .{
- .bytes = std.ArrayList(u8).fromOwnedSlice(bun.default_allocator, buffer),
- .was_string = true,
- },
- };
- }
-
- if (js_type.isTypedArray()) {
- if (value.asArrayBuffer(globalThis)) |buffer| {
- var bytes = buffer.byteSlice();
-
- if (bytes.len == 0) {
- return Body.Value{
- .Empty = {},
- };
- }
-
- // if (bytes.len <= InlineBlob.available_bytes) {
- // return Body.Value{
- // .InlineBlob = InlineBlob.init(bytes),
- // };
- // }
-
- return Body.Value{
- .InternalBlob = .{
- .bytes = std.ArrayList(u8){
- .items = bun.default_allocator.dupe(u8, bytes) catch {
- globalThis.vm().throwError(globalThis, ZigString.static("Failed to clone ArrayBufferView").toErrorInstance(globalThis));
- return null;
- },
- .capacity = bytes.len,
- .allocator = bun.default_allocator,
- },
- .was_string = false,
- },
- };
- }
- }
-
- if (js_type == .DOMWrapper) {
- if (value.as(Blob)) |blob| {
- return Body.Value{
- .Blob = blob.dupe(),
- };
- }
- }
-
- value.ensureStillAlive();
-
- if (JSC.WebCore.ReadableStream.fromJS(value, globalThis)) |readable| {
- switch (readable.ptr) {
- .Blob => |blob| {
- var result: Value = .{
- .Blob = Blob.initWithStore(blob.store, globalThis),
- };
- blob.store.ref();
-
- readable.done();
-
- if (!blob.done) {
- blob.done = true;
- blob.deinit();
- }
- return result;
- },
- else => {},
- }
-
- return Body.Value.fromReadableStream(readable, globalThis);
- }
-
- return Body.Value{
- .Blob = Blob.get(globalThis, value, true, false) catch |err| {
- if (err == error.InvalidArguments) {
- globalThis.throwInvalidArguments("Expected an Array", .{});
- return null;
- }
-
- globalThis.throwInvalidArguments("Invalid Body object", .{});
- return null;
- },
- };
- }
-
- pub fn fromReadableStream(readable: JSC.WebCore.ReadableStream, globalThis: *JSGlobalObject) Value {
- if (readable.isLocked(globalThis)) {
- return .{ .Error = ZigString.init("Cannot use a locked ReadableStream").toErrorInstance(globalThis) };
- }
-
- readable.value.protect();
- return .{
- .Locked = .{
- .readable = readable,
- .global = globalThis,
- },
- };
- }
-
- pub fn resolve(to_resolve: *Value, new: *Value, global: *JSGlobalObject) void {
- if (to_resolve.* == .Locked) {
- var locked = &to_resolve.Locked;
- if (locked.readable) |readable| {
- readable.done();
- locked.readable = null;
- }
-
- if (locked.onReceiveValue) |callback| {
- locked.onReceiveValue = null;
- callback(locked.task.?, new);
- return;
- }
-
- if (locked.promise) |promise_| {
- const promise = promise_.asAnyPromise().?;
- locked.promise = null;
-
- switch (locked.action) {
- .getText => {
- switch (new.*) {
- .InternalBlob,
- // .InlineBlob,
- => {
- var blob = new.useAsAnyBlob();
- promise.resolve(global, blob.toString(global, .transfer));
- },
- else => {
- var blob = new.use();
- promise.resolve(global, blob.toString(global, .transfer));
- },
- }
- },
- .getJSON => {
- var blob = new.useAsAnyBlob();
- const json_value = blob.toJSON(global, .share);
- blob.detach();
-
- if (json_value.isAnyError()) {
- promise.reject(global, json_value);
- } else {
- promise.resolve(global, json_value);
- }
- },
- .getArrayBuffer => {
- var blob = new.useAsAnyBlob();
- promise.resolve(global, blob.toArrayBuffer(global, .transfer));
- },
- else => {
- var ptr = bun.default_allocator.create(Blob) catch unreachable;
- ptr.* = new.use();
- ptr.allocator = bun.default_allocator;
- promise.resolve(global, ptr.toJS(global));
- },
- }
- JSC.C.JSValueUnprotect(global, promise_.asObjectRef());
- }
- }
- }
- pub fn slice(this: *const Value) []const u8 {
- return switch (this.*) {
- .Blob => this.Blob.sharedView(),
- .InternalBlob => this.InternalBlob.sliceConst(),
- // .InlineBlob => this.InlineBlob.sliceConst(),
- else => "",
- };
- }
-
- pub fn use(this: *Value) Blob {
- this.toBlobIfPossible();
-
- switch (this.*) {
- .Blob => {
- var new_blob = this.Blob;
- std.debug.assert(new_blob.allocator == null); // owned by Body
- this.* = .{ .Used = {} };
- return new_blob;
- },
- .InternalBlob => {
- var new_blob = Blob.init(
- this.InternalBlob.toOwnedSlice(),
- // we will never resize it from here
- // we have to use the default allocator
- // even if it was actually allocated on a different thread
- bun.default_allocator,
- JSC.VirtualMachine.get().global,
- );
- if (this.InternalBlob.was_string) {
- new_blob.content_type = MimeType.text.value;
- }
-
- this.* = .{ .Used = {} };
- return new_blob;
- },
- // .InlineBlob => {
- // const cloned = this.InlineBlob.bytes;
- // const new_blob = Blob.create(
- // cloned[0..this.InlineBlob.len],
- // bun.default_allocator,
- // JSC.VirtualMachine.get().global,
- // this.InlineBlob.was_string,
- // );
-
- // this.* = .{ .Used = {} };
- // return new_blob;
- // },
- else => {
- return Blob.initEmpty(undefined);
- },
- }
- }
-
- pub fn tryUseAsAnyBlob(this: *Value) ?AnyBlob {
- const any_blob: AnyBlob = switch (this.*) {
- .Blob => AnyBlob{ .Blob = this.Blob },
- .InternalBlob => AnyBlob{ .InternalBlob = this.InternalBlob },
- // .InlineBlob => AnyBlob{ .InlineBlob = this.InlineBlob },
- .Locked => this.Locked.toAnyBlobAllowPromise() orelse return null,
- else => return null,
- };
-
- this.* = .{ .Used = {} };
- return any_blob;
- }
-
- pub fn useAsAnyBlob(this: *Value) AnyBlob {
- const any_blob: AnyBlob = switch (this.*) {
- .Blob => .{ .Blob = this.Blob },
- .InternalBlob => .{ .InternalBlob = this.InternalBlob },
- // .InlineBlob => .{ .InlineBlob = this.InlineBlob },
- .Locked => this.Locked.toAnyBlobAllowPromise() orelse AnyBlob{ .Blob = .{} },
- else => .{ .Blob = Blob.initEmpty(undefined) },
- };
-
- this.* = .{ .Used = {} };
- return any_blob;
- }
-
- pub fn toErrorInstance(this: *Value, error_instance: JSC.JSValue, global: *JSGlobalObject) void {
- if (this.* == .Locked) {
- var locked = this.Locked;
- locked.deinit = true;
- if (locked.promise) |promise| {
- if (promise.asAnyPromise()) |internal| {
- internal.reject(global, error_instance);
- }
- JSC.C.JSValueUnprotect(global, promise.asObjectRef());
- locked.promise = null;
- }
-
- if (locked.readable) |readable| {
- readable.done();
- locked.readable = null;
- }
-
- this.* = .{ .Error = error_instance };
- if (locked.onReceiveValue) |onReceiveValue| {
- locked.onReceiveValue = null;
- onReceiveValue(locked.task.?, this);
- }
- return;
- }
-
- this.* = .{ .Error = error_instance };
- }
-
- pub fn toErrorString(this: *Value, comptime err: string, global: *JSGlobalObject) void {
- var error_str = ZigString.init(err);
- var error_instance = error_str.toErrorInstance(global);
- return this.toErrorInstance(error_instance, global);
- }
-
- pub fn toError(this: *Value, err: anyerror, global: *JSGlobalObject) void {
- var error_str = ZigString.init(std.fmt.allocPrint(
- bun.default_allocator,
- "Error reading file {s}",
- .{@errorName(err)},
- ) catch unreachable);
- error_str.mark();
- var error_instance = error_str.toErrorInstance(global);
- return this.toErrorInstance(error_instance, global);
- }
-
- pub fn deinit(this: *Value) void {
- const tag = @as(Tag, this.*);
- if (tag == .Locked) {
- if (!this.Locked.deinit) {
- this.Locked.deinit = true;
-
- if (this.Locked.readable) |*readable| {
- readable.done();
- }
- }
-
- return;
- }
-
- if (tag == .InternalBlob) {
- this.InternalBlob.clearAndFree();
- this.* = Value{ .Empty = {} }; //Value.empty;
- }
-
- if (tag == .Blob) {
- this.Blob.deinit();
- this.* = Value{ .Empty = {} }; //Value.empty;
- }
-
- if (tag == .Error) {
- JSC.C.JSValueUnprotect(VirtualMachine.get().global, this.Error.asObjectRef());
- }
- }
-
- pub fn clone(this: *Value, globalThis: *JSC.JSGlobalObject) Value {
- if (this.* == .InternalBlob) {
- var internal_blob = this.InternalBlob;
- this.* = .{
- .Blob = Blob.init(
- internal_blob.toOwnedSlice(),
- internal_blob.bytes.allocator,
- globalThis,
- ),
- };
- }
-
- // if (this.* == .InlineBlob) {
- // return this.*;
- // }
-
- if (this.* == .Blob) {
- return Value{ .Blob = this.Blob.dupe() };
- }
-
- return Value{ .Empty = {} };
- }
- };
-
- pub fn @"404"(_: js.JSContextRef) Body {
- return Body{
- .init = Init{
- .headers = null,
- .status_code = 404,
- },
- .value = Value{ .Empty = {} }, //Value.empty,
- };
- }
-
- pub fn @"200"(_: js.JSContextRef) Body {
- return Body{
- .init = Init{
- .status_code = 200,
- },
- .value = Value{ .Empty = {} }, //Value.empty,
- };
- }
-
- pub fn extract(
- globalThis: *JSGlobalObject,
- value: JSValue,
- ) ?Body {
- return extractBody(
- globalThis,
- value,
- false,
- JSValue.zero,
- .Cell,
- );
- }
-
- pub fn extractWithInit(
- globalThis: *JSGlobalObject,
- value: JSValue,
- init: JSValue,
- init_type: JSValue.JSType,
- ) ?Body {
- return extractBody(
- globalThis,
- value,
- true,
- init,
- init_type,
- );
- }
-
- // https://github.com/WebKit/webkit/blob/main/Source/WebCore/Modules/fetch/FetchBody.cpp#L45
- inline fn extractBody(
- globalThis: *JSGlobalObject,
- value: JSValue,
- comptime has_init: bool,
- init: JSValue,
- init_type: JSC.JSValue.JSType,
- ) ?Body {
- var body = Body{
- .value = Value{ .Empty = {} },
- .init = Init{ .headers = null, .status_code = 200 },
- };
- var allocator = getAllocator(globalThis);
-
- if (comptime has_init) {
- if (Init.init(allocator, globalThis, init, init_type)) |maybeInit| {
- if (maybeInit) |init_| {
- body.init = init_;
- }
- } else |_| {}
- }
-
- body.value = Value.fromJS(globalThis, value) orelse return null;
- if (body.value == .Blob)
- std.debug.assert(body.value.Blob.allocator == null); // owned by Body
-
- return body;
- }
-};
-
-// https://developer.mozilla.org/en-US/docs/Web/API/Request
-pub const Request = struct {
- url: []const u8 = "",
- url_was_allocated: bool = false,
-
- headers: ?*FetchHeaders = null,
- body: Body.Value = Body.Value{ .Empty = {} },
- method: Method = Method.GET,
- uws_request: ?*uws.Request = null,
- https: bool = false,
- upgrader: ?*anyopaque = null,
-
- // We must report a consistent value for this
- reported_estimated_size: ?u63 = null,
-
- const RequestMixin = BodyMixin(@This());
- pub usingnamespace JSC.Codegen.JSRequest;
-
- pub const getText = RequestMixin.getText;
- pub const getBody = RequestMixin.getBody;
- pub const getBodyUsed = RequestMixin.getBodyUsed;
- pub const getJSON = RequestMixin.getJSON;
- pub const getArrayBuffer = RequestMixin.getArrayBuffer;
- pub const getBlob = RequestMixin.getBlob;
-
- pub fn estimatedSize(this: *Request) callconv(.C) usize {
- return this.reported_estimated_size orelse brk: {
- this.reported_estimated_size = @truncate(u63, this.body.estimatedSize() + this.sizeOfURL() + @sizeOf(Request));
- break :brk this.reported_estimated_size.?;
- };
- }
-
- pub fn writeFormat(this: *Request, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void {
- const Writer = @TypeOf(writer);
- try writer.print("Request ({}) {{\n", .{bun.fmt.size(this.body.slice().len)});
- {
- formatter.indent += 1;
- defer formatter.indent -|= 1;
-
- try formatter.writeIndent(Writer, writer);
- try writer.writeAll("method: \"");
- try writer.writeAll(std.mem.span(@tagName(this.method)));
- try writer.writeAll("\"");
- formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
- try writer.writeAll("\n");
-
- try formatter.writeIndent(Writer, writer);
- try writer.writeAll("url: \"");
- try this.ensureURL();
- try writer.print(comptime Output.prettyFmt("<r><b>{s}<r>", enable_ansi_colors), .{this.url});
-
- try writer.writeAll("\"");
- if (this.body == .Blob) {
- try writer.writeAll("\n");
- try formatter.writeIndent(Writer, writer);
- try this.body.Blob.writeFormat(formatter, writer, enable_ansi_colors);
- } else if (this.body == .InternalBlob) {
- try writer.writeAll("\n");
- try formatter.writeIndent(Writer, writer);
- if (this.body.size() == 0) {
- try Blob.initEmpty(undefined).writeFormat(formatter, writer, enable_ansi_colors);
- } else {
- try Blob.writeFormatForSize(this.body.size(), writer, enable_ansi_colors);
- }
- } else if (this.body == .Locked) {
- if (this.body.Locked.readable) |stream| {
- try writer.writeAll("\n");
- try formatter.writeIndent(Writer, writer);
- formatter.printAs(.Object, Writer, writer, stream.value, stream.value.jsType(), enable_ansi_colors);
- }
- }
- }
- try writer.writeAll("\n");
- try formatter.writeIndent(Writer, writer);
- try writer.writeAll("}");
- }
-
- pub fn fromRequestContext(ctx: *RequestContext) !Request {
- var req = Request{
- .url = std.mem.span(ctx.getFullURL()),
- .body = .{ .Empty = {} },
- .method = ctx.method,
- .headers = FetchHeaders.createFromPicoHeaders(ctx.request.headers),
- .url_was_allocated = true,
- };
- return req;
- }
-
- pub fn mimeType(this: *const Request) string {
- if (this.headers) |headers| {
- if (headers.fastGet(.ContentType)) |content_type| {
- return content_type.slice();
- }
- }
-
- switch (this.body) {
- .Blob => |blob| {
- if (blob.content_type.len > 0) {
- return blob.content_type;
- }
-
- return MimeType.other.value;
- },
- .InternalBlob => return this.body.InternalBlob.contentType(),
- // .InlineBlob => return this.body.InlineBlob.contentType(),
- .Error, .Used, .Locked, .Empty => return MimeType.other.value,
- }
- }
-
- pub fn getCache(
- _: *Request,
- globalThis: *JSC.JSGlobalObject,
- ) callconv(.C) JSC.JSValue {
- return ZigString.init(Properties.UTF8.default).toValueGC(globalThis);
- }
- pub fn getCredentials(
- _: *Request,
- globalThis: *JSC.JSGlobalObject,
- ) callconv(.C) JSC.JSValue {
- return ZigString.init(Properties.UTF8.include).toValueGC(globalThis);
- }
- pub fn getDestination(
- _: *Request,
- globalThis: *JSC.JSGlobalObject,
- ) callconv(.C) JSC.JSValue {
- return ZigString.init("").toValueGC(globalThis);
- }
-
- pub fn getIntegrity(
- _: *Request,
- globalThis: *JSC.JSGlobalObject,
- ) callconv(.C) JSC.JSValue {
- return ZigString.Empty.toValueGC(globalThis);
- }
-
- pub fn getMethod(
- this: *Request,
- globalThis: *JSC.JSGlobalObject,
- ) callconv(.C) JSC.JSValue {
- const string_contents: string = switch (this.method) {
- .GET => "GET",
- .HEAD => "HEAD",
- .PATCH => "PATCH",
- .PUT => "PUT",
- .POST => "POST",
- .OPTIONS => "OPTIONS",
- .CONNECT => "CONNECT",
- .TRACE => "TRACE",
- .DELETE => "DELETE",
- };
-
- return ZigString.init(string_contents).toValueGC(globalThis);
- }
-
- pub fn getMode(
- _: *Request,
- globalThis: *JSC.JSGlobalObject,
- ) callconv(.C) JSC.JSValue {
- return ZigString.init(Properties.UTF8.navigate).toValue(globalThis);
- }
-
- pub fn finalize(this: *Request) callconv(.C) void {
- if (this.headers) |headers| {
- headers.deref();
- this.headers = null;
- }
-
- if (this.url_was_allocated) {
- bun.default_allocator.free(bun.constStrToU8(this.url));
- }
-
- this.body.deinit();
-
- bun.default_allocator.destroy(this);
- }
-
- pub fn getRedirect(
- _: *Request,
- globalThis: *JSC.JSGlobalObject,
- ) callconv(.C) JSC.JSValue {
- return ZigString.init(Properties.UTF8.follow).toValueGC(globalThis);
- }
- pub fn getReferrer(
- this: *Request,
- globalObject: *JSC.JSGlobalObject,
- ) callconv(.C) JSC.JSValue {
- if (this.headers) |headers_ref| {
- if (headers_ref.get("referrer")) |referrer| {
- return ZigString.init(referrer).toValueGC(globalObject);
- }
- }
-
- return ZigString.init("").toValueGC(globalObject);
- }
- pub fn getReferrerPolicy(
- _: *Request,
- globalThis: *JSC.JSGlobalObject,
- ) callconv(.C) JSC.JSValue {
- return ZigString.init("").toValueGC(globalThis);
- }
- pub fn getUrl(
- this: *Request,
- globalObject: *JSC.JSGlobalObject,
- ) callconv(.C) JSC.JSValue {
- this.ensureURL() catch {
- globalObject.throw("Failed to join URL", .{});
- return .zero;
- };
-
- return ZigString.init(this.url).withEncoding().toValueGC(globalObject);
- }
-
- pub fn sizeOfURL(this: *const Request) usize {
- if (this.url.len > 0)
- return this.url.len;
-
- if (this.uws_request) |req| {
- const fmt = ZigURL.HostFormatter{
- .is_https = this.https,
- .host = req.header("host") orelse "",
- };
-
- return this.getProtocol().len + req.url().len + std.fmt.count("{any}", .{fmt});
- }
-
- return 0;
- }
-
- pub fn getProtocol(this: *const Request) []const u8 {
- if (this.https)
- return "https://";
-
- return "http://";
- }
-
- pub fn ensureURL(this: *Request) !void {
- if (this.url.len > 0) return;
-
- if (this.uws_request) |req| {
- const req_url = req.url();
- if (req.header("host")) |host| {
- const fmt = ZigURL.HostFormatter{
- .is_https = this.https,
- .host = host,
- };
- const url = try std.fmt.allocPrint(bun.default_allocator, "{s}{any}{s}", .{
- this.getProtocol(),
- fmt,
- req_url,
- });
- if (comptime Environment.allow_assert) {
- std.debug.assert(this.sizeOfURL() == url.len);
- }
- this.url = url;
- this.url_was_allocated = true;
- } else {
- if (comptime Environment.allow_assert) {
- std.debug.assert(this.sizeOfURL() == req_url.len);
- }
- this.url = try bun.default_allocator.dupe(u8, req_url);
- this.url_was_allocated = true;
- }
- }
- }
-
- pub fn constructInto(
- globalThis: *JSC.JSGlobalObject,
- arguments: []const JSC.JSValue,
- ) ?Request {
- var request = Request{};
-
- switch (arguments.len) {
- 0 => {},
- 1 => {
- const urlOrObject = arguments[0];
- const url_or_object_type = urlOrObject.jsType();
- if (url_or_object_type.isStringLike()) {
- request.url = (arguments[0].toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch {
- return null;
- }).slice();
- request.url_was_allocated = request.url.len > 0;
- } else {
- if (urlOrObject.fastGet(globalThis, .body)) |body_| {
- if (Body.Value.fromJS(globalThis, body_)) |body| {
- request.body = body;
- } else {
- return null;
- }
- }
-
- if (Body.Init.init(getAllocator(globalThis), globalThis, arguments[0], url_or_object_type) catch null) |req_init| {
- request.headers = req_init.headers;
- request.method = req_init.method;
- }
-
- if (urlOrObject.fastGet(globalThis, .url)) |url| {
- request.url = (url.toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch {
- return null;
- }).slice();
- request.url_was_allocated = request.url.len > 0;
- }
- }
- },
- else => {
- if (arguments[1].fastGet(globalThis, .body)) |body_| {
- if (Body.Value.fromJS(globalThis, body_)) |body| {
- request.body = body;
- } else {
- return null;
- }
- }
-
- if (Body.Init.init(getAllocator(globalThis), globalThis, arguments[1], arguments[1].jsType()) catch null) |req_init| {
- request.headers = req_init.headers;
- request.method = req_init.method;
- }
-
- request.url = (arguments[0].toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch {
- return null;
- }).slice();
- request.url_was_allocated = request.url.len > 0;
- },
- }
-
- return request;
- }
-
- pub fn constructor(
- globalThis: *JSC.JSGlobalObject,
- callframe: *JSC.CallFrame,
- ) callconv(.C) ?*Request {
- const arguments_ = callframe.arguments(2);
- const arguments = arguments_.ptr[0..arguments_.len];
-
- const request = constructInto(globalThis, arguments) orelse return null;
- var request_ = getAllocator(globalThis).create(Request) catch return null;
- request_.* = request;
- return request_;
- }
-
- pub fn getBodyValue(
- this: *Request,
- ) *Body.Value {
- return &this.body;
- }
-
- pub fn doClone(
- this: *Request,
- globalThis: *JSC.JSGlobalObject,
- _: *JSC.CallFrame,
- ) callconv(.C) JSC.JSValue {
- var cloned = this.clone(getAllocator(globalThis), globalThis);
- return cloned.toJS(globalThis);
- }
-
- pub fn getHeaders(
- this: *Request,
- globalThis: *JSC.JSGlobalObject,
- ) callconv(.C) JSC.JSValue {
- if (this.headers == null) {
- if (this.uws_request) |req| {
- this.headers = FetchHeaders.createFromUWS(globalThis, req);
- } else {
- this.headers = FetchHeaders.createEmpty();
- }
- }
-
- return this.headers.?.toJS(globalThis);
- }
-
- pub fn cloneInto(
- this: *Request,
- req: *Request,
- allocator: std.mem.Allocator,
- globalThis: *JSGlobalObject,
- ) void {
- this.ensureURL() catch {};
-
- req.* = Request{
- .body = this.body.clone(globalThis),
- .url = allocator.dupe(u8, this.url) catch {
- globalThis.throw("Failed to clone request", .{});
- return;
- },
- .method = this.method,
- };
-
- if (this.headers) |head| {
- req.headers = head.cloneThis();
- } else if (this.uws_request) |uws_req| {
- req.headers = FetchHeaders.createFromUWS(globalThis, uws_req);
- this.headers = req.headers.?.cloneThis().?;
- }
- }
-
- pub fn clone(this: *Request, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) *Request {
- var req = allocator.create(Request) catch unreachable;
- this.cloneInto(req, allocator, globalThis);
- return req;
- }
-};
-
-fn BodyMixin(comptime Type: type) type {
- return struct {
- pub fn getText(
- this: *Type,
- globalThis: *JSC.JSGlobalObject,
- _: *JSC.CallFrame,
- ) callconv(.C) JSC.JSValue {
- var value: *Body.Value = this.getBodyValue();
- if (value.* == .Used) {
- return handleBodyAlreadyUsed(globalThis);
- }
-
- if (value.* == .Locked) {
- return value.Locked.setPromise(globalThis, .getText);
- }
-
- var blob = value.useAsAnyBlob();
- return JSC.JSPromise.wrap(globalThis, blob.toString(globalThis, .transfer));
- }
-
- pub fn getBody(
- this: *Type,
- globalThis: *JSC.JSGlobalObject,
- ) callconv(.C) JSValue {
- var body: *Body.Value = this.getBodyValue();
-
- if (body.* == .Used) {
- // TODO: make this closed
- return JSC.WebCore.ReadableStream.empty(globalThis);
- }
-
- return body.toReadableStream(globalThis);
- }
-
- pub fn getBodyUsed(
- this: *Type,
- _: *JSC.JSGlobalObject,
- ) callconv(.C) JSValue {
- return JSValue.jsBoolean(this.getBodyValue().* == .Used);
- }
-
- pub fn getJSON(
- this: *Type,
- globalObject: *JSC.JSGlobalObject,
- _: *JSC.CallFrame,
- ) callconv(.C) JSC.JSValue {
- var value: *Body.Value = this.getBodyValue();
- if (value.* == .Used) {
- return handleBodyAlreadyUsed(globalObject);
- }
-
- if (value.* == .Locked) {
- return value.Locked.setPromise(globalObject, .getJSON);
- }
-
- var blob = value.useAsAnyBlob();
- return JSC.JSPromise.wrap(globalObject, blob.toJSON(globalObject, .share));
- }
-
- fn handleBodyAlreadyUsed(globalObject: *JSC.JSGlobalObject) JSValue {
- return JSC.JSPromise.rejectedPromiseValue(
- globalObject,
- ZigString.static("Body already used").toErrorInstance(globalObject),
- );
- }
-
- pub fn getArrayBuffer(
- this: *Type,
- globalObject: *JSC.JSGlobalObject,
- _: *JSC.CallFrame,
- ) callconv(.C) JSC.JSValue {
- var value: *Body.Value = this.getBodyValue();
-
- if (value.* == .Used) {
- return handleBodyAlreadyUsed(globalObject);
- }
-
- if (value.* == .Locked) {
- return value.Locked.setPromise(globalObject, .getArrayBuffer);
- }
-
- var blob: AnyBlob = value.useAsAnyBlob();
- return JSC.JSPromise.wrap(globalObject, blob.toArrayBuffer(globalObject, .transfer));
- }
-
- pub fn getBlob(
- this: *Type,
- globalObject: *JSC.JSGlobalObject,
- _: *JSC.CallFrame,
- ) callconv(.C) JSC.JSValue {
- var value: *Body.Value = this.getBodyValue();
-
- if (value.* == .Used) {
- return handleBodyAlreadyUsed(globalObject);
- }
-
- if (value.* == .Locked) {
- return value.Locked.setPromise(globalObject, .getBlob);
- }
-
- var blob = value.use();
- var ptr = getAllocator(globalObject).create(Blob) catch unreachable;
- ptr.* = blob;
- blob.allocator = getAllocator(globalObject);
- return JSC.JSPromise.resolvedPromiseValue(globalObject, ptr.toJS(globalObject));
- }
- };
-}
-
// https://github.com/WebKit/WebKit/blob/main/Source/WebCore/workers/service/FetchEvent.h
pub const FetchEvent = struct {
started_waiting_at: u64 = 0,
diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig
index f4f4ffb7c..3ba0da101 100644
--- a/src/bun.js/webcore/streams.zig
+++ b/src/bun.js/webcore/streams.zig
@@ -37,7 +37,7 @@ const JSValue = JSC.JSValue;
const JSError = JSC.JSError;
const JSGlobalObject = JSC.JSGlobalObject;
-const VirtualMachine = @import("../javascript.zig").VirtualMachine;
+const VirtualMachine = JSC.VirtualMachine;
const Task = JSC.Task;
const JSPrinter = @import("../../js_printer.zig");
const picohttp = @import("bun").picohttp;