[bun.js] Add stdout, stderr, stdin to Bun and support sendfile() + splice()

This commit is contained in:
Jarred SUmner
2022-04-06 01:52:15 -07:00
parent 57cf035a73
commit 81eb47de0e
25 changed files with 552 additions and 141 deletions

View File

@@ -255,6 +255,75 @@ pub fn getOrigin(
return ZigString.init(VirtualMachine.vm.origin.origin).toValue(ctx.ptr()).asRef();
}
pub fn getStdin(
_: void,
ctx: js.JSContextRef,
_: js.JSValueRef,
_: js.JSStringRef,
_: js.ExceptionRef,
) js.JSValueRef {
var existing = ctx.ptr().getCachedObject(&ZigString.init("BunSTDIN"));
if (existing.isEmpty()) {
var rare_data = JSC.VirtualMachine.vm.rareData();
var store = rare_data.stdin();
var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable;
blob.* = JSC.WebCore.Blob.initWithStore(store, ctx.ptr());
return ctx.ptr().putCachedObject(
&ZigString.init("BunSTDIN"),
JSC.JSValue.fromRef(JSC.WebCore.Blob.Class.make(ctx, blob)),
).asObjectRef();
}
return existing.asObjectRef();
}
pub fn getStderr(
_: void,
ctx: js.JSContextRef,
_: js.JSValueRef,
_: js.JSStringRef,
_: js.ExceptionRef,
) js.JSValueRef {
var existing = ctx.ptr().getCachedObject(&ZigString.init("BunSTDERR"));
if (existing.isEmpty()) {
var rare_data = JSC.VirtualMachine.vm.rareData();
var store = rare_data.stderr();
var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable;
blob.* = JSC.WebCore.Blob.initWithStore(store, ctx.ptr());
return ctx.ptr().putCachedObject(
&ZigString.init("BunSTDERR"),
JSC.JSValue.fromRef(JSC.WebCore.Blob.Class.make(ctx, blob)),
).asObjectRef();
}
return existing.asObjectRef();
}
pub fn getStdout(
_: void,
ctx: js.JSContextRef,
_: js.JSValueRef,
_: js.JSStringRef,
_: js.ExceptionRef,
) js.JSValueRef {
var existing = ctx.ptr().getCachedObject(&ZigString.init("BunSTDOUT"));
if (existing.isEmpty()) {
var rare_data = JSC.VirtualMachine.vm.rareData();
var store = rare_data.stdout();
var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable;
blob.* = JSC.WebCore.Blob.initWithStore(store, ctx.ptr());
return ctx.ptr().putCachedObject(
&ZigString.init("BunSTDOUT"),
JSC.JSValue.fromRef(JSC.WebCore.Blob.Class.make(ctx, blob)),
).asObjectRef();
}
return existing.asObjectRef();
}
pub fn enableANSIColors(
_: void,
ctx: js.JSContextRef,
@@ -1063,6 +1132,15 @@ pub const Class = NewClass(
.get = getOrigin,
.ts = d.ts{ .name = "origin", .@"return" = "string" },
},
.stdin = .{
.get = getStdin,
},
.stdout = .{
.get = getStdout,
},
.stderr = .{
.get = getStderr,
},
.routesDir = .{
.get = getRoutesDir,
.ts = d.ts{ .name = "routesDir", .@"return" = "string" },

View File

@@ -304,7 +304,7 @@ pub const HTMLRewriter = struct {
if (is_pending) {
input.doReadFileInternal(*BufferOutputSink, sink, onFinishedLoading, global);
} else if (sink.runOutputSink(input.sharedView(), false)) |error_value| {
} else if (sink.runOutputSink(input.sharedView(), false, false)) |error_value| {
return error_value;
}
@@ -337,15 +337,24 @@ pub const HTMLRewriter = struct {
return;
},
.result => |data| {
_ = sink.runOutputSink(data, true);
_ = sink.runOutputSink(data.buf, true, data.is_temporary);
},
}
}
pub fn runOutputSink(sink: *BufferOutputSink, bytes: []const u8, is_async: bool) ?JSValue {
pub fn runOutputSink(
sink: *BufferOutputSink,
bytes: []const u8,
is_async: bool,
free_bytes_on_end: bool,
) ?JSValue {
defer if (free_bytes_on_end)
bun.default_allocator.free(bun.constStrToU8(bytes));
sink.bytes.growBy(bytes.len) catch unreachable;
var global = sink.global;
var response = sink.response;
sink.rewriter.write(bytes) catch {
sink.deinit();
bun.default_allocator.destroy(sink);

View File

@@ -357,7 +357,9 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
sendfile: SendfileContext = undefined,
request_js_object: JSC.C.JSObjectRef = null,
request_body_buf: std.ArrayListUnmanaged(u8) = .{},
fallback_buf: std.ArrayListUnmanaged(u8) = .{},
/// Used either for temporary blob data or fallback
/// When the response body is a temporary value
response_buf_owned: std.ArrayListUnmanaged(u8) = .{},
pub const RequestContextStackAllocator = std.heap.StackFallbackAllocator(@sizeOf(RequestContext) * 2048 + 4096);
@@ -478,15 +480,19 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
return;
}
this.fallback_buf = std.ArrayListUnmanaged(u8){ .items = bb.items, .capacity = bb.capacity };
this.resp.onWritable(*RequestContext, onWritableFallback, this);
this.response_buf_owned = std.ArrayListUnmanaged(u8){ .items = bb.items, .capacity = bb.capacity };
this.renderResponseBuffer();
}
pub fn onWritableFallback(this: *RequestContext, write_offset: c_ulong, resp: *App.Response) callconv(.C) bool {
pub fn renderResponseBuffer(this: *RequestContext) void {
this.resp.onWritable(*RequestContext, onWritableResponseBuffer, this);
}
pub fn onWritableResponseBuffer(this: *RequestContext, write_offset: c_ulong, resp: *App.Response) callconv(.C) bool {
if (this.aborted) {
return false;
}
return this.sendWritableBytes(this.fallback_buf.items, write_offset, resp);
return this.sendWritableBytes(this.response_buf_owned.items, write_offset, resp);
}
pub fn create(this: *RequestContext, server: *ThisServer, req: *uws.Request, resp: *App.Response) void {
@@ -542,7 +548,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
this.response_headers = null;
}
this.fallback_buf.clearAndFree(bun.default_allocator);
this.response_buf_owned.clearAndFree(bun.default_allocator);
}
pub fn finalize(this: *RequestContext) void {
this.finalizeWithoutDeinit();
@@ -731,8 +737,15 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
return;
}
this.blob.resolveSize();
this.doRenderBlob();
const is_temporary = result.result.is_temporary;
if (!is_temporary) {
this.blob.resolveSize();
this.doRenderBlob();
} else {
this.blob.size = @truncate(Blob.SizeType, result.result.buf.len);
this.response_buf_owned = .{ .items = result.result.buf, .capacity = result.result.buf.len };
this.renderResponseBuffer();
}
}
pub fn doRenderWithBodyLocked(this: *anyopaque, value: *JSC.WebCore.Body.Value) void {

View File

@@ -2533,7 +2533,10 @@ pub const JSValue = enum(u64) {
pub fn asArrayBuffer(this: JSValue, global: *JSGlobalObject) ?ArrayBuffer {
var out: ArrayBuffer = undefined;
if (this.asArrayBuffer_(global, &out)) return out;
if (this.asArrayBuffer_(global, &out)) {
out.value = this;
return out;
}
return null;
}

View File

@@ -90,6 +90,8 @@ pub const Tag = enum(u8) {
fcopyfile,
recv,
send,
sendfile,
splice,
pub var strings = std.EnumMap(Tag, JSC.C.JSStringRef).initFull(null);
};

View File

@@ -1,3 +1,98 @@
const EditorContext = @import("../../open.zig").EditorContext;
const Blob = @import("./webcore/response.zig").Blob;
const default_allocator = @import("../../global.zig").default_allocator;
const Output = @import("../../global.zig").Output;
const RareData = @This();
const Syscall = @import("./node/syscall.zig");
const JSC = @import("javascript_core");
const std = @import("std");
editor_context: EditorContext = EditorContext{},
stderr_store: ?*Blob.Store = null,
stdin_store: ?*Blob.Store = null,
stdout_store: ?*Blob.Store = null,
pub fn stderr(rare: *RareData) *Blob.Store {
return rare.stderr_store orelse brk: {
var store = default_allocator.create(Blob.Store) catch unreachable;
var mode: JSC.Node.Mode = 0;
switch (Syscall.fstat(std.os.STDERR_FILENO)) {
.result => |stat| {
mode = stat.mode;
},
.err => {},
}
store.* = Blob.Store{
.ref_count = 2,
.allocator = default_allocator,
.data = .{
.file = Blob.FileStore{
.pathlike = .{
.fd = std.os.STDERR_FILENO,
},
.is_atty = Output.stderr_descriptor_type == .terminal,
.mode = mode,
},
},
};
rare.stderr_store = store;
break :brk store;
};
}
pub fn stdout(rare: *RareData) *Blob.Store {
return rare.stdout_store orelse brk: {
var store = default_allocator.create(Blob.Store) catch unreachable;
var mode: JSC.Node.Mode = 0;
switch (Syscall.fstat(std.os.STDOUT_FILENO)) {
.result => |stat| {
mode = stat.mode;
},
.err => {},
}
store.* = Blob.Store{
.ref_count = 2,
.allocator = default_allocator,
.data = .{
.file = Blob.FileStore{
.pathlike = .{
.fd = std.os.STDOUT_FILENO,
},
.is_atty = Output.stdout_descriptor_type == .terminal,
.mode = mode,
},
},
};
rare.stdout_store = store;
break :brk store;
};
}
pub fn stdin(rare: *RareData) *Blob.Store {
return rare.stdin_store orelse brk: {
var store = default_allocator.create(Blob.Store) catch unreachable;
var mode: JSC.Node.Mode = 0;
switch (Syscall.fstat(std.os.STDIN_FILENO)) {
.result => |stat| {
mode = stat.mode;
},
.err => {},
}
store.* = Blob.Store{
.allocator = default_allocator,
.ref_count = 2,
.data = .{
.file = Blob.FileStore{
.pathlike = .{
.fd = std.os.STDIN_FILENO,
},
.is_atty = std.os.isatty(std.os.STDIN_FILENO),
.mode = mode,
},
},
};
rare.stdin_store = store;
break :brk store;
};
}

View File

@@ -586,6 +586,8 @@ pub const Response = struct {
}
};
const null_fd = std.math.maxInt(JSC.Node.FileDescriptor);
pub const Fetch = struct {
const headers_string = "headers";
const method_string = "method";
@@ -1404,6 +1406,7 @@ pub const Blob = struct {
) js.JSObjectRef {
var args = JSC.Node.ArgumentsSlice.from(arguments);
defer args.deinit();
var path = JSC.Node.PathOrFileDescriptor.fromJS(ctx, &args, exception) orelse {
exception.* = JSC.toInvalidArguments("Expected file path string or file descriptor", .{}, ctx).asObjectRef();
return js.JSValueMakeUndefined(ctx);
@@ -1424,12 +1427,31 @@ pub const Blob = struct {
return Blob.initWithStore(blob, globalThis);
}
if (path == .path) {
path.path = .{
.string = bun.PathString.init(
(bun.default_allocator.dupeZ(u8, path.path.slice()) catch unreachable)[0..path.path.slice().len],
),
};
switch (path) {
.path => {
path.path = .{
.string = bun.PathString.init(
(bun.default_allocator.dupeZ(u8, path.path.slice()) catch unreachable)[0..path.path.slice().len],
),
};
},
.fd => {
switch (path.fd) {
std.os.STDIN_FILENO => return Blob.initWithStore(
VirtualMachine.vm.rareData().stdin(),
globalThis,
),
std.os.STDERR_FILENO => return Blob.initWithStore(
VirtualMachine.vm.rareData().stderr(),
globalThis,
),
std.os.STDOUT_FILENO => return Blob.initWithStore(
VirtualMachine.vm.rareData().stdout(),
globalThis,
),
else => {},
}
},
}
const result = Blob.initWithStore(Blob.Store.initFile(path, null, bun.default_allocator) catch unreachable, globalThis);
@@ -1567,7 +1589,7 @@ pub const Blob = struct {
}
pub fn getFd(this: *This) AsyncIO.OpenError!JSC.Node.FileDescriptor {
if (this.opened_fd != 0) {
if (this.opened_fd != null_fd) {
return this.opened_fd;
}
@@ -1641,7 +1663,7 @@ pub const Blob = struct {
&this.close_completion,
this.opened_fd,
);
this.opened_fd = 0;
this.opened_fd = null_fd;
suspend {
this.close_frame = @frame().*;
@@ -1678,7 +1700,7 @@ pub const Blob = struct {
errno: ?anyerror = null,
system_error: ?JSC.SystemError = null,
open_completion: HTTPClient.NetworkThread.Completion = undefined,
opened_fd: JSC.Node.FileDescriptor = 0,
opened_fd: JSC.Node.FileDescriptor = null_fd,
size: SizeType = 0,
store: *Store = undefined,
@@ -1749,12 +1771,12 @@ pub const Blob = struct {
}
fn _runAsync(this: *OpenAndStatFile) void {
this.opened_fd = 0;
this.opened_fd = null_fd;
if (this.file_store.pathlike == .fd) {
this.opened_fd = this.file_store.pathlike.fd;
}
const fd =
if (this.opened_fd == 0)
if (this.opened_fd == null_fd)
this.getFd() catch return
else
this.opened_fd;
@@ -1767,9 +1789,18 @@ pub const Blob = struct {
},
};
if (!std.os.S.ISREG(stat.mode)) {
this.errno = error.ENOTSUP;
return;
if (Environment.isMac) {
if (!std.os.S.ISREG(stat.mode)) {
this.errno = error.ENOTSUP;
return;
}
}
if (Environment.isLinux) {
if (!(std.os.S.ISREG(stat.mode) or std.os.S.ISFIFO(stat.mode))) {
this.errno = error.ENOTSUP;
return;
}
}
this.size = @truncate(SizeType, @intCast(u64, @maximum(@intCast(i64, stat.size), 0)));
@@ -1790,7 +1821,7 @@ pub const Blob = struct {
read_frame: @Frame(ReadFile.doRead) = undefined,
close_frame: @Frame(ReadFile.doClose) = undefined,
open_completion: HTTPClient.NetworkThread.Completion = undefined,
opened_fd: JSC.Node.FileDescriptor = 0,
opened_fd: JSC.Node.FileDescriptor = null_fd,
read_completion: HTTPClient.NetworkThread.Completion = undefined,
read_len: SizeType = 0,
read_off: SizeType = 0,
@@ -1804,7 +1835,13 @@ pub const Blob = struct {
onCompleteCtx: *anyopaque = undefined,
onCompleteCallback: OnReadFileCallback = undefined,
pub const ResultType = SystemError.Maybe([]u8);
convert_to_byte_blob: bool = false,
pub const Read = struct {
buf: []u8,
is_temporary: bool = false,
};
pub const ResultType = SystemError.Maybe(Read);
pub const OnReadFileCallback = fn (ctx: *anyopaque, bytes: ResultType) void;
@@ -1908,7 +1945,7 @@ pub const Blob = struct {
}
var store = this.store.?;
if (this.file_store.pathlike == .path) {
if (this.convert_to_byte_blob and this.file_store.pathlike == .path) {
VirtualMachine.vm.removeFileBlob(this.file_store.pathlike);
}
@@ -1919,17 +1956,20 @@ pub const Blob = struct {
return;
}
var bytes = this.buffer;
if (store.data == .bytes) {
bun.default_allocator.free(this.buffer);
bytes = store.data.bytes.slice();
} else if (store.data == .file) {
if (this.file_store.pathlike == .path) {
if (this.file_store.pathlike.path == .string) {
bun.default_allocator.free(this.file_store.pathlike.path.slice());
var buf = this.buffer;
const is_temporary = !this.convert_to_byte_blob;
if (this.convert_to_byte_blob) {
if (store.data == .bytes) {
bun.default_allocator.free(this.buffer);
buf = store.data.bytes.slice();
} else if (store.data == .file) {
if (this.file_store.pathlike == .path) {
if (this.file_store.pathlike.path == .string) {
bun.default_allocator.free(this.file_store.pathlike.path.slice());
}
}
store.data = .{ .bytes = ByteStore.init(buf, bun.default_allocator) };
}
store.data = .{ .bytes = ByteStore.init(bytes, bun.default_allocator) };
}
bun.default_allocator.destroy(this);
@@ -1937,9 +1977,9 @@ pub const Blob = struct {
// Attempt to free it as soon as possible
if (store.ref_count > 1) {
store.deref();
cb(cb_ctx, .{ .result = bytes });
cb(cb_ctx, .{ .result = .{ .buf = buf, .is_temporary = is_temporary } });
} else {
cb(cb_ctx, .{ .result = bytes });
cb(cb_ctx, .{ .result = .{ .buf = buf, .is_temporary = is_temporary } });
store.deref();
}
}
@@ -1983,7 +2023,7 @@ pub const Blob = struct {
}
const fd = this.getFd() catch return;
const needs_close = this.file_store.pathlike == .path and fd != 0;
const needs_close = this.file_store.pathlike == .path and fd != null_fd and fd > 2;
const stat: std.os.Stat = switch (JSC.Node.Syscall.fstat(fd)) {
.result => |result| result,
.err => |err| {
@@ -1992,24 +2032,34 @@ pub const Blob = struct {
return;
},
};
if (!std.os.S.ISREG(stat.mode)) {
this.errno = error.ENOTSUP;
if (std.os.S.ISDIR(stat.mode)) {
this.errno = error.EISDIR;
this.system_error = JSC.SystemError{
.code = ZigString.init(std.mem.span(@errorName(error.TODO))),
.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("Non-regular files are not supported yet"),
.message = ZigString.init("Directories cannot be read like files"),
.syscall = ZigString.init("read"),
};
return;
}
this.size = @minimum(
@truncate(SizeType, @intCast(SizeType, @maximum(@intCast(i64, stat.size), 0))),
this.max_length,
);
if (stat.size > 0 and std.os.S.ISREG(stat.mode)) {
this.size = @minimum(
@truncate(SizeType, @intCast(SizeType, @maximum(@intCast(i64, stat.size), 0))),
this.max_length,
);
// read up to 4k at a time if
// they didn't explicitly set a size and we're reading from something that's not a regular file
} else if (stat.size == 0 and !std.os.S.ISREG(stat.mode)) {
this.size = if (this.max_length == Blob.max_size)
4096
else
this.max_length;
}
if (this.size == 0) {
this.buffer = &[_]u8{};
this.byte_store = ByteStore.init(this.buffer, bun.default_allocator);
@@ -2019,6 +2069,7 @@ pub const Blob = struct {
}
return;
}
var bytes = bun.default_allocator.alloc(u8, this.size) catch |err| {
this.errno = err;
if (needs_close) {
@@ -2027,6 +2078,7 @@ pub const Blob = struct {
return;
};
this.buffer = bytes;
this.convert_to_byte_blob = std.os.S.ISREG(stat.mode) and this.file_store.pathlike == .path;
var remain = bytes;
while (remain.len > 0) {
@@ -2056,7 +2108,7 @@ pub const Blob = struct {
file_blob: Blob,
bytes_blob: Blob,
opened_fd: JSC.Node.FileDescriptor = 0,
opened_fd: JSC.Node.FileDescriptor = null_fd,
open_frame: OpenFrameType = undefined,
write_frame: @Frame(WriteFile.doWrite) = undefined,
close_frame: @Frame(WriteFile.doClose) = undefined,
@@ -2078,6 +2130,7 @@ pub const Blob = struct {
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(
@@ -2129,14 +2182,15 @@ pub const Blob = struct {
) AsyncIO.WriteError!SizeType {
var aio = &AsyncIO.global;
this.wrote = 0;
const fd = this.opened_fd;
aio.write(
*WriteFile,
this,
onWrite,
&this.write_completion,
this.opened_fd,
fd,
buffer,
file_offset,
if (fd > 2) file_offset else 0,
);
suspend {
@@ -2205,8 +2259,8 @@ pub const Blob = struct {
this.opened_fd = file.pathlike.fd;
}
_ = this.getFd() catch return;
const needs_close = file.pathlike == .path;
const fd = this.getFd() catch return;
const needs_close = file.pathlike == .path and fd > 2;
var remain = this.bytes_blob.sharedView();
@@ -2264,8 +2318,8 @@ pub const Blob = struct {
offset: SizeType = 0,
size: SizeType = 0,
max_length: SizeType = Blob.max_size,
destination_fd: JSC.Node.FileDescriptor = 0,
source_fd: JSC.Node.FileDescriptor = 0,
destination_fd: JSC.Node.FileDescriptor = null_fd,
source_fd: JSC.Node.FileDescriptor = null_fd,
system_error: ?SystemError = null,
@@ -2324,7 +2378,10 @@ pub const Blob = struct {
system_error.path = ZigString.init(this.source_file_store.pathlike.path.slice());
system_error.path.mark();
}
system_error.message = ZigString.init("Failed to copy file");
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| {
@@ -2349,9 +2406,8 @@ pub const Blob = struct {
}
pub fn doClose(this: *CopyFile) void {
// const repos = await fetch("https://api.github.com/users/octocat/repos")
const close_input = this.destination_file_store.pathlike != .fd and this.destination_fd != 0;
const close_output = this.source_file_store.pathlike != .fd and this.source_fd != 0;
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);
@@ -2420,41 +2476,86 @@ pub const Blob = struct {
}
}
pub fn doCopyFileRange(this: *CopyFile) anyerror!void {
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 == 0) {
if (remain == max_size or remain == 0) {
// sometimes stat lies
// let's give it 2048 and see how it goes
remain = 2048;
// 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_off = this.offset;
this.read_len = @truncate(SizeType, total_written);
}
while (remain > 0) {
// Linux Kernel 5.3 or later
const written = linux.copy_file_range(src_fd, null, dest_fd, null, remain, 0);
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 = .copy_file_range,
.syscall = TryWith.tag.get(use).?,
}).toSystemError();
return AsyncIO.asError(errno);
},
}
// wrote zero bytes means EOF
if (written == 0) break;
remain -|= written;
total_written += written;
if (written == 0 or remain == 0) break;
}
}
@@ -2462,6 +2563,7 @@ pub const Blob = struct {
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 => {},
@@ -2500,7 +2602,7 @@ pub const Blob = struct {
}
// Do we need to open both files?
if (this.destination_fd == 0 and this.source_fd == 0) {
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.
@@ -2555,12 +2657,12 @@ pub const Blob = struct {
this.doOpenFile(.both) catch return;
// Do we need to open only one file?
} else if (this.destination_fd == 0) {
} 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 == 0) {
} else if (this.source_fd == null_fd) {
this.destination_fd = this.destination_file_store.pathlike.fd;
this.doOpenFile(.source) catch return;
@@ -2570,8 +2672,10 @@ pub const Blob = struct {
return;
}
std.debug.assert(this.destination_fd != 0);
std.debug.assert(this.source_fd != 0);
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,
@@ -2595,35 +2699,70 @@ pub const Blob = struct {
return;
}
if (this.max_length > std.mem.page_size) {
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 (os.S.ISREG(stat.mode)) {
if (comptime Environment.isLinux) {
this.doCopyFileRange() catch {
this.doClose();
if (comptime Environment.isLinux) {
return;
};
} else 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));
// 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 {};
}
} else {
@compileError("TODO: implement copyfile");
this.doClose();
return;
}
} else {
// $ 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;
}
this.doClose();
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");
}
}
};
};
@@ -2631,6 +2770,8 @@ pub const Blob = struct {
pub const FileStore = struct {
pathlike: JSC.Node.PathOrFileDescriptor,
mime_type: HTTPClient.MimeType = HTTPClient.MimeType.other,
is_atty: ?bool = null,
mode: JSC.Node.Mode = 0,
pub fn init(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?HTTPClient.MimeType) FileStore {
return .{ .pathlike = pathlike, .mime_type = mime_type orelse HTTPClient.MimeType.other };
@@ -2754,7 +2895,7 @@ pub const Blob = struct {
_: []const js.JSValueRef,
_: js.ExceptionRef,
) JSC.C.JSObjectRef {
return promisified(this.toJSON(ctx.ptr()), ctx.ptr()).asObjectRef();
return promisified(this.toJSON(ctx.ptr(), .share), ctx.ptr()).asObjectRef();
}
pub fn getArrayBufferTransfer(
@@ -3082,6 +3223,8 @@ pub const Blob = struct {
clone,
transfer,
share,
/// When reading from a fifo like STDIN/STDERR
temporary,
};
pub fn setIsASCIIFlag(this: *Blob, is_all_ascii: bool) void {
@@ -3091,7 +3234,7 @@ pub const Blob = struct {
// we can update the store's is_all_ascii flag
// and any other Blob that points to the same store
// can skip checking the encoding
if (this.size > 0 and this.offset == 0) {
if (this.size > 0 and this.offset == 0 and this.store.?.data == .bytes) {
this.store.?.is_all_ascii = is_all_ascii;
}
}
@@ -3108,11 +3251,16 @@ pub const Blob = struct {
var globalThis = handler.globalThis;
bun.default_allocator.destroy(handler);
switch (bytes_) {
.result => |bytes| {
.result => |result| {
const bytes = result.buf;
const is_temporary = result.is_temporary;
if (blob.size > 0)
blob.size = @minimum(@truncate(u32, bytes.len), blob.size);
promise.resolve(globalThis, Function(&blob, globalThis, comptime lifetime));
if (!is_temporary) {
promise.resolve(globalThis, Function(&blob, globalThis, bytes, comptime lifetime));
} else {
promise.resolve(globalThis, Function(&blob, globalThis, bytes, .temporary));
}
},
.err => |err| {
promise.reject(globalThis, err.toErrorInstance(globalThis));
@@ -3202,18 +3350,7 @@ pub const Blob = struct {
return this.store != null and this.store.?.data == .file;
}
pub fn toString(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
if (this.needsToReadFile()) {
return this.doReadFile(toString, lifetime, global);
}
var view_: []const u8 =
this.sharedView();
if (view_.len == 0)
return ZigString.Empty.toValue(global);
var buf = view_;
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;
@@ -3222,15 +3359,21 @@ pub const Blob = struct {
// if toUTF16Alloc returns null, it means there are no non-ASCII characters
// instead of erroring, invalid characters will become a U+FFFD replacement character
if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch unreachable) |external| {
this.setIsASCIIFlag(false);
if (lifetime != .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);
}
this.setIsASCIIFlag(true);
if (lifetime != .temporary) this.setIsASCIIFlag(true);
}
switch (comptime lifetime) {
@@ -3251,16 +3394,29 @@ pub const Blob = struct {
this.store.?.ref();
return ZigString.init(buf).external(global, this.store.?, Store.external);
},
.temporary => {
return ZigString.init(buf).toExternalValue(global);
},
}
}
pub fn toJSONShare(this: *Blob, global: *JSGlobalObject, comptime _: Lifetime) JSValue {
return toJSON(this, global);
pub fn toString(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
if (this.needsToReadFile()) {
return this.doReadFile(toStringWithBytes, lifetime, global);
}
const view_: []u8 =
bun.constStrToU8(this.sharedView());
if (view_.len == 0)
return ZigString.Empty.toValue(global);
return toStringWithBytes(this, global, view_, lifetime);
}
pub fn toJSON(this: *Blob, global: *JSGlobalObject) JSValue {
pub fn toJSON(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
if (this.needsToReadFile()) {
return this.doReadFile(toJSONShare, .share, global);
return this.doReadFile(toJSONWithBytes, lifetime, global);
}
var view_ = this.sharedView();
@@ -3268,9 +3424,10 @@ pub const Blob = struct {
if (view_.len == 0)
return ZigString.Empty.toValue(global);
// TODO: use the index to make this one pass instead of two passes
var buf = view_;
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;
@@ -3278,37 +3435,35 @@ pub const Blob = struct {
if (could_be_all_ascii == null or !could_be_all_ascii.?) {
// if toUTF16Alloc returns null, it means there are no non-ASCII characters
if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch null) |external| {
this.setIsASCIIFlag(false);
if (comptime lifetime != .temporary) this.setIsASCIIFlag(false);
return ZigString.toExternalU16(external.ptr, external.len, global).parseJSON(global);
}
this.setIsASCIIFlag(true);
if (comptime lifetime != .temporary) this.setIsASCIIFlag(true);
}
return ZigString.init(buf).toValue(
global,
).parseJSON(global);
if (comptime lifetime == .temporary) {
return ZigString.init(buf).toExternalValue(
global,
).parseJSON(global);
} else {
return ZigString.init(buf).toValue(
global,
).parseJSON(global);
}
}
pub fn toArrayBuffer(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
if (this.needsToReadFile()) {
return this.doReadFile(toArrayBuffer, lifetime, global);
}
var view_ = this.sharedView();
if (view_.len == 0)
return JSC.ArrayBuffer.fromBytes(&[_]u8{}, .ArrayBuffer).toJS(global.ref(), null);
pub fn toArrayBufferWithBytes(this: *Blob, global: *JSGlobalObject, buf: []u8, comptime lifetime: Lifetime) JSValue {
switch (comptime lifetime) {
.clone => {
var clone = bun.default_allocator.alloc(u8, view_.len) catch unreachable;
@memcpy(clone.ptr, view_.ptr, view_.len);
var clone = bun.default_allocator.alloc(u8, buf.len) catch unreachable;
@memcpy(clone.ptr, buf.ptr, buf.len);
return JSC.ArrayBuffer.fromBytes(clone, .ArrayBuffer).toJS(global.ref(), null);
},
.share => {
this.store.?.ref();
return JSC.ArrayBuffer.fromBytes(bun.constStrToU8(view_), .ArrayBuffer).toJSWithContext(
return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJSWithContext(
global.ref(),
this.store.?,
JSC.BlobArrayBuffer_deallocator,
@@ -3318,16 +3473,35 @@ pub const Blob = struct {
.transfer => {
var store = this.store.?;
this.transfer();
return JSC.ArrayBuffer.fromBytes(bun.constStrToU8(view_), .ArrayBuffer).toJSWithContext(
return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJSWithContext(
global.ref(),
store,
JSC.BlobArrayBuffer_deallocator,
null,
);
},
.temporary => {
return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJS(
global.ref(),
null,
);
},
}
}
pub fn toArrayBuffer(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
if (this.needsToReadFile()) {
return this.doReadFile(toArrayBufferWithBytes, lifetime, global);
}
var view_ = this.sharedView();
if (view_.len == 0)
return JSC.ArrayBuffer.fromBytes(&[_]u8{}, .ArrayBuffer).toJS(global.ref(), null);
return toArrayBufferWithBytes(this, global, bun.constStrToU8(view_), lifetime);
}
pub inline fn fromJS(
global: *JSGlobalObject,
arg: JSValue,
@@ -3796,7 +3970,7 @@ pub const Body = struct {
promise.asPromise().?.resolve(global, JSValue.fromRef(blob.getTextTransfer(global.ref())));
},
.getJSON => {
promise.asPromise().?.resolve(global, blob.toJSON(global));
promise.asPromise().?.resolve(global, blob.toJSON(global, .share));
blob.detach();
},
.getArrayBuffer => {