diff options
-rw-r--r-- | bench/snippets/cat.bun.js | 5 | ||||
-rw-r--r-- | bench/snippets/cat.mjs | 1 | ||||
-rw-r--r-- | bench/snippets/cat.node.js | 4 | ||||
-rw-r--r-- | bench/snippets/copy.bun.js | 4 | ||||
-rw-r--r-- | examples/cat.ts | 5 | ||||
-rw-r--r-- | examples/hashing.js (renamed from examples/bun/hashing.js) | 0 | ||||
-rw-r--r-- | examples/html-rewriter.ts (renamed from examples/bun/html-rewriter.ts) | 0 | ||||
-rw-r--r-- | examples/http-file.ts (renamed from examples/bun/http-file.ts) | 0 | ||||
-rw-r--r-- | examples/http.ts (renamed from examples/bun/http.ts) | 0 | ||||
-rw-r--r-- | examples/mmap/1.js (renamed from examples/bun/mmap/1.js) | 0 | ||||
-rw-r--r-- | examples/mmap/2.js (renamed from examples/bun/mmap/2.js) | 0 | ||||
-rw-r--r-- | examples/mmap/mmap.txt (renamed from examples/bun/mmap/mmap.txt) | 0 | ||||
-rw-r--r-- | examples/openInEditor.js (renamed from examples/bun/openInEditor.js) | 0 | ||||
-rw-r--r-- | examples/ssl.ts (renamed from examples/bun/ssl.ts) | 0 | ||||
-rw-r--r-- | examples/tsconfig.json (renamed from examples/bun/tsconfig.json) | 3 | ||||
m--------- | src/deps/mimalloc | 0 | ||||
-rw-r--r-- | src/javascript/jsc/api/bun.zig | 78 | ||||
-rw-r--r-- | src/javascript/jsc/api/html_rewriter.zig | 15 | ||||
-rw-r--r-- | src/javascript/jsc/api/server.zig | 29 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/bindings.zig | 5 | ||||
-rw-r--r-- | src/javascript/jsc/node/syscall.zig | 2 | ||||
-rw-r--r-- | src/javascript/jsc/rare_data.zig | 95 | ||||
-rw-r--r-- | src/javascript/jsc/webcore/response.zig | 424 | ||||
-rw-r--r-- | src/linux_c.zig | 17 | ||||
-rw-r--r-- | src/string_immutable.zig | 2 |
25 files changed, 550 insertions, 139 deletions
diff --git a/bench/snippets/cat.bun.js b/bench/snippets/cat.bun.js new file mode 100644 index 000000000..1bb1c809a --- /dev/null +++ b/bench/snippets/cat.bun.js @@ -0,0 +1,5 @@ +import { resolve } from "path"; +const { write, stdout, file } = Bun; +const input = resolve(process.argv[process.argv.length - 1]); + +await write(stdout, file(input)); diff --git a/bench/snippets/cat.mjs b/bench/snippets/cat.mjs index 7e4f3da54..ca6dfe838 100644 --- a/bench/snippets/cat.mjs +++ b/bench/snippets/cat.mjs @@ -1,3 +1,4 @@ +// works in both bun & node import { readFileSync } from "node:fs"; const count = parseInt(process.env.ITERATIONS || "1", 10) || 1; const arg = process.argv.slice(1); diff --git a/bench/snippets/cat.node.js b/bench/snippets/cat.node.js new file mode 100644 index 000000000..d38d7c537 --- /dev/null +++ b/bench/snippets/cat.node.js @@ -0,0 +1,4 @@ +const path = require("path"); +const fs = require("fs"); +const input = path.resolve(process.argv[process.argv.length - 1]); +fs.createReadStream(input).pipe(process.stdout); diff --git a/bench/snippets/copy.bun.js b/bench/snippets/copy.bun.js new file mode 100644 index 000000000..20269212a --- /dev/null +++ b/bench/snippets/copy.bun.js @@ -0,0 +1,4 @@ +import path from "path"; +const input = path.resolve(process.argv[process.argv.length - 2]); +const output = path.resolve(process.argv[process.argv.length - 1]); +await Bun.write(Bun.file(output), Bun.file(input)); diff --git a/examples/cat.ts b/examples/cat.ts new file mode 100644 index 000000000..1bb1c809a --- /dev/null +++ b/examples/cat.ts @@ -0,0 +1,5 @@ +import { resolve } from "path"; +const { write, stdout, file } = Bun; +const input = resolve(process.argv[process.argv.length - 1]); + +await write(stdout, file(input)); diff --git a/examples/bun/hashing.js b/examples/hashing.js index cf4772ffe..cf4772ffe 100644 --- a/examples/bun/hashing.js +++ b/examples/hashing.js diff --git a/examples/bun/html-rewriter.ts b/examples/html-rewriter.ts index 2b370c5ed..2b370c5ed 100644 --- a/examples/bun/html-rewriter.ts +++ b/examples/html-rewriter.ts diff --git a/examples/bun/http-file.ts b/examples/http-file.ts index f2c0773e8..f2c0773e8 100644 --- a/examples/bun/http-file.ts +++ b/examples/http-file.ts diff --git a/examples/bun/http.ts b/examples/http.ts index 4d58f270f..4d58f270f 100644 --- a/examples/bun/http.ts +++ b/examples/http.ts diff --git a/examples/bun/mmap/1.js b/examples/mmap/1.js index 1c4f2c969..1c4f2c969 100644 --- a/examples/bun/mmap/1.js +++ b/examples/mmap/1.js diff --git a/examples/bun/mmap/2.js b/examples/mmap/2.js index c4b68bd9a..c4b68bd9a 100644 --- a/examples/bun/mmap/2.js +++ b/examples/mmap/2.js diff --git a/examples/bun/mmap/mmap.txt b/examples/mmap/mmap.txt index 6931040dd..6931040dd 100644 --- a/examples/bun/mmap/mmap.txt +++ b/examples/mmap/mmap.txt diff --git a/examples/bun/openInEditor.js b/examples/openInEditor.js index 59282c098..59282c098 100644 --- a/examples/bun/openInEditor.js +++ b/examples/openInEditor.js diff --git a/examples/bun/ssl.ts b/examples/ssl.ts index df30a0bd6..df30a0bd6 100644 --- a/examples/bun/ssl.ts +++ b/examples/ssl.ts diff --git a/examples/bun/tsconfig.json b/examples/tsconfig.json index ab7f30fc9..4e99331ee 100644 --- a/examples/bun/tsconfig.json +++ b/examples/tsconfig.json @@ -3,7 +3,6 @@ "lib": ["ESNext"], "module": "esnext", "target": "esnext", - "typeRoots": ["../../types"], - "types": ["bun"] + "typeRoots": ["~/.bun/types"] } } diff --git a/src/deps/mimalloc b/src/deps/mimalloc -Subproject 9e41263d39041aee3b647eff64d5ef4918a60ce +Subproject 817569dfad79732233fb86649c89e04387ce02e diff --git a/src/javascript/jsc/api/bun.zig b/src/javascript/jsc/api/bun.zig index 26faf75d7..8826acc2e 100644 --- a/src/javascript/jsc/api/bun.zig +++ b/src/javascript/jsc/api/bun.zig @@ -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" }, diff --git a/src/javascript/jsc/api/html_rewriter.zig b/src/javascript/jsc/api/html_rewriter.zig index f75829418..70406ace5 100644 --- a/src/javascript/jsc/api/html_rewriter.zig +++ b/src/javascript/jsc/api/html_rewriter.zig @@ -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); diff --git a/src/javascript/jsc/api/server.zig b/src/javascript/jsc/api/server.zig index 01854eecb..57fbe6d34 100644 --- a/src/javascript/jsc/api/server.zig +++ b/src/javascript/jsc/api/server.zig @@ -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 { diff --git a/src/javascript/jsc/bindings/bindings.zig b/src/javascript/jsc/bindings/bindings.zig index d4fcdaef0..7797ae4c5 100644 --- a/src/javascript/jsc/bindings/bindings.zig +++ b/src/javascript/jsc/bindings/bindings.zig @@ -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; } diff --git a/src/javascript/jsc/node/syscall.zig b/src/javascript/jsc/node/syscall.zig index cd7a56157..ac7d51d8b 100644 --- a/src/javascript/jsc/node/syscall.zig +++ b/src/javascript/jsc/node/syscall.zig @@ -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); }; diff --git a/src/javascript/jsc/rare_data.zig b/src/javascript/jsc/rare_data.zig index 72bf45fbd..efd602085 100644 --- a/src/javascript/jsc/rare_data.zig +++ b/src/javascript/jsc/rare_data.zig @@ -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; + }; +} diff --git a/src/javascript/jsc/webcore/response.zig b/src/javascript/jsc/webcore/response.zig index c18dd1431..f1fa6223b 100644 --- a/src/javascript/jsc/webcore/response.zig +++ b/src/javascript/jsc/webcore/response.zig @@ -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(); + // 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 {}; + } - 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(); + 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 {}; } - } else { - @compileError("TODO: implement copyfile"); + + this.doClose(); + return; } - } else { + + 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); - } - pub fn toArrayBuffer(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue { - if (this.needsToReadFile()) { - return this.doReadFile(toArrayBuffer, lifetime, global); + if (comptime lifetime == .temporary) { + return ZigString.init(buf).toExternalValue( + global, + ).parseJSON(global); + } else { + return ZigString.init(buf).toValue( + global, + ).parseJSON(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,14 +3473,33 @@ 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( @@ -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 => { diff --git a/src/linux_c.zig b/src/linux_c.zig index 6247df61d..ae5e9a3cb 100644 --- a/src/linux_c.zig +++ b/src/linux_c.zig @@ -280,3 +280,20 @@ pub const SystemErrno = enum(u8) { pub fn preallocate_file(fd: std.os.fd_t, offset: std.os.off_t, len: std.os.off_t) anyerror!void { _ = std.os.linux.fallocate(fd, 0, @intCast(i64, offset), len); } + +/// splice() moves data between two file descriptors without copying +/// between kernel address space and user address space. It +/// transfers up to len bytes of data from the file descriptor fd_in +/// to the file descriptor fd_out, where one of the file descriptors +/// must refer to a pipe. +pub fn splice(fd_in: std.os.fd_t, off_in: ?*i64, fd_out: std.os.fd_t, off_out: ?*i64, len: usize, flags: u32) usize { + return std.os.linux.syscall6( + .splice, + @bitCast(usize, @as(isize, fd_in)), + @ptrToInt(off_in), + @bitCast(usize, @as(isize, fd_out)), + @ptrToInt(off_out), + len, + flags, + ); +} diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 4c7c07eae..1483729fa 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1106,6 +1106,8 @@ pub fn convertUTF8BytesIntoUTF16(sequence: *const [4]u8) UTF16Replacement { (@as(u32, sequence[2]) << 6) + @as(u32, sequence[3])) - 0x03C82080, }; }, + // invalid unicode sequence + 0 => return UTF16Replacement{ .len = 1 }, else => unreachable, } } |