diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 19b46d60176bd9..37f4b72a085359 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -620,8 +620,26 @@ declare module "bun" { * * @param begin - start offset in bytes * @param end - absolute offset in bytes (relative to 0) + * @param contentType - MIME type for the new FileBlob */ - slice(begin?: number, end?: number): FileBlob; + slice(begin?: number, end?: number, contentType?: string): FileBlob; + + /** + * Offset any operation on the file starting at `begin` + * + * Similar to [`TypedArray.subarray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray). Does not copy the file, open the file, or modify the file. + * + * If `begin` > 0, {@link Bun.write()} will be slower on macOS + * + * @param begin - start offset in bytes + * @param contentType - MIME type for the new FileBlob + */ + slice(begin?: number, contentType?: string): FileBlob; + + /** + * @param contentType - MIME type for the new FileBlob + */ + slice(contentType?: string): FileBlob; /** * Incremental writer for files and pipes. diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index 2dd86f91129ec8..aab4921bdff1bd 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -565,7 +565,29 @@ declare class Blob implements BlobInterface { * @param end The index that sets the end of the view. * */ - slice(begin?: number, end?: number): Blob; + slice(begin?: number, end?: number, contentType?: string): Blob; + + /** + * Create a new view **without 🚫 copying** the underlying data. + * + * Similar to [`BufferSource.subarray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BufferSource/subarray) + * + * @param begin The index that sets the beginning of the view. + * @param end The index that sets the end of the view. + * + */ + slice(begin?: number, contentType?: string): Blob; + + /** + * Create a new view **without 🚫 copying** the underlying data. + * + * Similar to [`BufferSource.subarray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BufferSource/subarray) + * + * @param begin The index that sets the beginning of the view. + * @param end The index that sets the end of the view. + * + */ + slice(contentType?: string): Blob; /** * Read the data from the blob as a string. It will be decoded from UTF-8. diff --git a/packages/bun-types/tests/array.test-d.ts b/packages/bun-types/tests/array.test-d.ts index 65295b3b154ade..f7ba9a6513d11f 100644 --- a/packages/bun-types/tests/array.test-d.ts +++ b/packages/bun-types/tests/array.test-d.ts @@ -13,6 +13,4 @@ async function* listReleases() { } } -const releases = await Array.fromAsync(listReleases()); - -export {}; +export const releases = await Array.fromAsync(listReleases()); diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 5da1b360f54b6c..3514ab599fffaa 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -376,7 +376,9 @@ pub const ServerConfig = struct { } if (arg.getTruthy(global, "maxRequestBodySize")) |max_request_body_size| { - args.max_request_body_size = @intCast(u64, @max(0, max_request_body_size.toInt64())); + if (max_request_body_size.isNumber()) { + args.max_request_body_size = @intCast(u64, @max(0, max_request_body_size.toInt64())); + } } if (arg.getTruthy(global, "error")) |onError| { diff --git a/src/bun.js/base.zig b/src/bun.js/base.zig index f9a52b7145a997..a411beac2b85ce 100644 --- a/src/bun.js/base.zig +++ b/src/bun.js/base.zig @@ -1773,6 +1773,8 @@ pub const ArrayBuffer = extern struct { } pub fn createEmpty(globalThis: *JSC.JSGlobalObject, comptime kind: JSC.JSValue.JSType) JSValue { + JSC.markBinding(@src()); + return switch (comptime kind) { .Uint8Array => Bun__createUint8ArrayForCopy(globalThis, null, 0, false), .ArrayBuffer => Bun__createArrayBufferForCopy(globalThis, null, 0), diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 62c1848938d79a..5c9cba7f0f5dce 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -2455,10 +2455,6 @@ bool JSC__JSValue__isPrimitive(JSC__JSValue JSValue0) { return JSC::JSValue::decode(JSValue0).isPrimitive(); } -bool JSC__JSValue__isString(JSC__JSValue JSValue0) -{ - return JSC::JSValue::decode(JSValue0).isString(); -} bool JSC__JSValue__isSymbol(JSC__JSValue JSValue0) { return JSC::JSValue::decode(JSValue0).isSymbol(); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 49ff393a205e94..40fc43091a2ee5 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -159,6 +159,9 @@ pub const ZigString = extern struct { } pub fn eql(this: ZigString, other: ZigString) bool { + if (this.len == 0 or other.len == 0) + return this.len == other.len; + const left_utf16 = this.is16Bit(); const right_utf16 = other.is16Bit(); @@ -2276,6 +2279,10 @@ pub const JSGlobalObject = extern struct { return this.bunVM().allocator; } + pub fn throwOutOfMemory(this: *JSGlobalObject) void { + this.throwValue(this.createErrorInstance("Out of memory", .{})); + } + pub fn throwInvalidArguments( this: *JSGlobalObject, comptime fmt: string, @@ -3454,8 +3461,11 @@ pub const JSValue = enum(JSValueReprInt) { return res; } - pub fn isString(this: JSValue) bool { - return cppFn("isString", .{this}); + pub inline fn isString(this: JSValue) bool { + if (!this.isCell()) + return false; + + return jsType(this).isStringLike(); } pub fn isBigInt(this: JSValue) bool { return cppFn("isBigInt", .{this}); @@ -3968,7 +3978,6 @@ pub const JSValue = enum(JSValueReprInt) { "isObject", "isPrimitive", "isSameValue", - "isString", "isSymbol", "isTerminationException", "isUInt32AsAnyInt", diff --git a/src/bun.js/bindings/headers-cpp.h b/src/bun.js/bindings/headers-cpp.h index 4b195d7565d267..ae4ecaf5d6da96 100644 --- a/src/bun.js/bindings/headers-cpp.h +++ b/src/bun.js/bindings/headers-cpp.h @@ -1,4 +1,4 @@ -//-- AUTOGENERATED FILE -- 1677776166 +//-- AUTOGENERATED FILE -- 1678254453 // clang-format off #pragma once diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 422a71a7014419..20b7fbedbb2586 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -1,5 +1,5 @@ // clang-format off -//-- AUTOGENERATED FILE -- 1677776166 +//-- AUTOGENERATED FILE -- 1678254453 #pragma once #include @@ -331,7 +331,6 @@ CPP_DECL bool JSC__JSValue__isNumber(JSC__JSValue JSValue0); CPP_DECL bool JSC__JSValue__isObject(JSC__JSValue JSValue0); CPP_DECL bool JSC__JSValue__isPrimitive(JSC__JSValue JSValue0); CPP_DECL bool JSC__JSValue__isSameValue(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* arg2); -CPP_DECL bool JSC__JSValue__isString(JSC__JSValue JSValue0); CPP_DECL bool JSC__JSValue__isSymbol(JSC__JSValue JSValue0); CPP_DECL bool JSC__JSValue__isTerminationException(JSC__JSValue JSValue0, JSC__VM* arg1); CPP_DECL bool JSC__JSValue__isUInt32AsAnyInt(JSC__JSValue JSValue0); diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index 0c968d362728e8..ce84d1e730a596 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -244,7 +244,6 @@ pub extern fn JSC__JSValue__isNumber(JSValue0: JSC__JSValue) bool; pub extern fn JSC__JSValue__isObject(JSValue0: JSC__JSValue) bool; pub extern fn JSC__JSValue__isPrimitive(JSValue0: JSC__JSValue) bool; pub extern fn JSC__JSValue__isSameValue(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue, arg2: *bindings.JSGlobalObject) bool; -pub extern fn JSC__JSValue__isString(JSValue0: JSC__JSValue) bool; pub extern fn JSC__JSValue__isSymbol(JSValue0: JSC__JSValue) bool; pub extern fn JSC__JSValue__isTerminationException(JSValue0: JSC__JSValue, arg1: *bindings.VM) bool; pub extern fn JSC__JSValue__isUInt32AsAnyInt(JSValue0: JSC__JSValue) bool; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 46a52289478d94..c89c07583a1312 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -450,6 +450,10 @@ pub const VirtualMachine = struct { return VMHolder.vm.?; } + pub fn mimeType(this: *VirtualMachine, str: []const u8) ?bun.HTTP.MimeType { + return this.rareData().mimeTypeFromString(this.allocator, str); + } + pub const GCLevel = enum(u3) { none = 0, mild = 1, diff --git a/src/bun.js/rare_data.zig b/src/bun.js/rare_data.zig index 253eb2cf2d962e..238d447003d83e 100644 --- a/src/bun.js/rare_data.zig +++ b/src/bun.js/rare_data.zig @@ -27,6 +27,18 @@ file_polls_: ?*JSC.FilePoll.HiveArray = null, global_dns_data: ?*JSC.DNS.GlobalData = null, +mime_types: ?bun.HTTP.MimeType.Map = null, + +pub fn mimeTypeFromString(this: *RareData, allocator: std.mem.Allocator, str: []const u8) ?bun.HTTP.MimeType { + if (this.mime_types == null) { + this.mime_types = bun.HTTP.MimeType.createHashTable( + allocator, + ) catch @panic("Out of memory"); + } + + return this.mime_types.?.get(str); +} + pub fn filePolls(this: *RareData, vm: *JSC.VirtualMachine) *JSC.FilePoll.HiveArray { return this.file_polls_ orelse { this.file_polls_ = vm.allocator.create(JSC.FilePoll.HiveArray) catch unreachable; diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index c610b199dc8fca..5a41f5f8ceaea5 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -2391,7 +2391,7 @@ pub const Blob = struct { callframe: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { var allocator = globalThis.allocator(); - var arguments_ = callframe.arguments(2); + var arguments_ = callframe.arguments(3); var args = arguments_.ptr[0..arguments_.len]; if (this.size == 0) { @@ -2410,51 +2410,84 @@ pub const Blob = struct { // 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); + if (args.ptr[0].isString()) { + args.ptr[2] = args.ptr[0]; + args.ptr[1] = .zero; + args.ptr[0] = .zero; + args.len = 3; + } else if (args.ptr[1].isString()) { + args.ptr[2] = args.ptr[1]; + args.ptr[1] = .zero; + args.len = 3; + } + 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 (start_.isNumber()) { + 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)); + if (end_.isNumber()) { + 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 = ""; + var content_type_was_allocated = false; 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); + inner: { + 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(); + if (!strings.isAllASCII(slice)) { + break :inner; + } + + if (globalThis.bunVM().mimeType(slice)) |mime| { + content_type = mime.value; + break :inner; + } + + content_type_was_allocated = slice.len > 0; + 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)); + 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; + + // infer the content type if it was not specified + if (content_type.len == 0 and this.content_type.len > 0 and !this.content_type_allocated) + content_type = this.content_type; + blob.content_type = content_type; - blob.content_type_allocated = content_type.len > 0; + blob.content_type_allocated = content_type_was_allocated; var blob_ = allocator.create(Blob) catch unreachable; blob_.* = blob; @@ -2474,19 +2507,35 @@ pub const Blob = struct { globalThis: *JSC.JSGlobalObject, value: JSC.JSValue, ) callconv(.C) bool { - var zig_str = value.getZigString(globalThis); - if (zig_str.is16Bit()) - return false; + var zig_str = if (value.isString()) + value.getZigString(globalThis) + else + ZigString.Empty; + + if (!zig_str.isAllASCII()) { + zig_str = ZigString.Empty; + } - var slice = zig_str.trimmedSlice(); - if (strings.eql(slice, this.content_type)) + if (zig_str.eql(ZigString.init(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); + var slicer = zig_str.toSlice(bun.default_allocator); + defer slicer.deinit(); + const allocated = this.content_type_allocated; + defer if (allocated) bun.default_allocator.free(prev_content_type); + if (globalThis.bunVM().mimeType(slicer.slice())) |mime| { + this.content_type = mime.value; + this.content_type_allocated = false; + return true; + } + var content_type_buf = globalThis.allocator().alloc(u8, slicer.len) catch { + globalThis.throwOutOfMemory(); + return false; + }; + this.content_type = strings.copyLowercase(slicer.slice(), content_type_buf); } this.content_type_allocated = true; @@ -2602,13 +2651,22 @@ pub const Blob = struct { // 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.toSlice(globalThis, bun.default_allocator); - defer content_type_str.deinit(); - var slice = content_type_str.slice(); - 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; + inner: { + if (content_type.isString()) { + var content_type_str = content_type.toSlice(globalThis, bun.default_allocator); + defer content_type_str.deinit(); + var slice = content_type_str.slice(); + if (!strings.isAllASCII(slice)) { + break :inner; + } + if (globalThis.bunVM().mimeType(slice)) |mime| { + blob.content_type = mime.value; + break :inner; + } + 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; + } } } } diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 748b4583c69eb9..b5f690a1df28ed 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -399,7 +399,8 @@ pub const StreamStart = union(Tag) { } if (value.get(globalThis, "chunkSize")) |chunkSize| { - return .{ .chunk_size = @intCast(Blob.SizeType, @truncate(i52, chunkSize.toInt64())) }; + if (chunkSize.isNumber()) + return .{ .chunk_size = @intCast(Blob.SizeType, @truncate(i52, chunkSize.toInt64())) }; } return .{ .empty = {} }; @@ -432,8 +433,10 @@ pub const StreamStart = union(Tag) { } if (value.get(globalThis, "highWaterMark")) |chunkSize| { - empty = false; - chunk_size = @intCast(JSC.WebCore.Blob.SizeType, @max(0, @truncate(i51, chunkSize.toInt64()))); + if (chunkSize.isNumber()) { + empty = false; + chunk_size = @intCast(JSC.WebCore.Blob.SizeType, @max(0, @truncate(i51, chunkSize.toInt64()))); + } } if (!empty) { @@ -450,7 +453,8 @@ pub const StreamStart = union(Tag) { var chunk_size: JSC.WebCore.Blob.SizeType = 0; if (value.get(globalThis, "highWaterMark")) |chunkSize| { - chunk_size = @intCast(JSC.WebCore.Blob.SizeType, @max(0, @truncate(i51, chunkSize.toInt64()))); + if (chunkSize.isNumber()) + chunk_size = @intCast(JSC.WebCore.Blob.SizeType, @max(0, @truncate(i51, chunkSize.toInt64()))); } if (value.get(globalThis, "path")) |path| { @@ -485,8 +489,10 @@ pub const StreamStart = union(Tag) { var chunk_size: JSC.WebCore.Blob.SizeType = 2048; if (value.get(globalThis, "highWaterMark")) |chunkSize| { - empty = false; - chunk_size = @intCast(JSC.WebCore.Blob.SizeType, @max(256, @truncate(i51, chunkSize.toInt64()))); + if (chunkSize.isNumber()) { + empty = false; + chunk_size = @intCast(JSC.WebCore.Blob.SizeType, @max(256, @truncate(i51, chunkSize.toInt64()))); + } } if (!empty) { diff --git a/src/http/mime_type.zig b/src/http/mime_type.zig index 3b3d8b9615917d..a79b5ebddc7fe5 100644 --- a/src/http/mime_type.zig +++ b/src/http/mime_type.zig @@ -18,6 +18,23 @@ const MimeType = @This(); value: string, category: Category, +pub const Map = bun.StringHashMap(MimeType); + +pub fn createHashTable(allocator: std.mem.Allocator) !Map { + @setCold(true); + + const decls = comptime std.meta.declarations(all); + + var map = Map.init(allocator); + try map.ensureTotalCapacity(@truncate(u32, decls.len)); + @setEvalBranchQuota(4000); + inline for (decls) |decl| { + map.putAssumeCapacityNoClobber(decl.name, @field(all, decl.name)); + } + + return map; +} + pub fn canOpenInEditor(this: MimeType) bool { if (this.category == .text or this.category.isCode()) return true; diff --git a/test/js/web/fetch/blob.test.ts b/test/js/web/fetch/blob.test.ts new file mode 100644 index 00000000000000..51b9c0ea8e806f --- /dev/null +++ b/test/js/web/fetch/blob.test.ts @@ -0,0 +1,56 @@ +import { test, expect } from "bun:test"; + +test("Blob.slice", () => { + const blob = new Blob(["Bun", "Foo"]); + const b1 = blob.slice(0, 3, "Text/HTML"); + expect(b1 instanceof Blob).toBeTruthy(); + expect(b1.size).toBe(3); + expect(b1.type).toBe("text/html"); + const b2 = blob.slice(-1, 3); + expect(b2.size).toBe(0); + const b3 = blob.slice(100, 3); + expect(b3.size).toBe(0); + const b4 = blob.slice(0, 10); + expect(b4.size).toBe(blob.size); + + expect(blob.slice().size).toBe(blob.size); + expect(blob.slice(0).size).toBe(blob.size); + expect(blob.slice(NaN).size).toBe(blob.size); + expect(blob.slice(0, Infinity).size).toBe(blob.size); + expect(blob.slice(-Infinity).size).toBe(blob.size); + expect(blob.slice(0, NaN).size).toBe(0); + // @ts-expect-error + expect(blob.slice(Symbol(), "-123").size).toBe(6); + expect(blob.slice(Object.create(null), "-123").size).toBe(6); + // @ts-expect-error + expect(blob.slice(null, "-123").size).toBe(6); + expect(blob.slice(0, 10).size).toBe(blob.size); + expect(blob.slice("text/plain;charset=utf-8").type).toBe("text/plain;charset=utf-8"); +}); + +test("Blob.prototype.type setter", () => { + var blob = new Blob(["Bun", "Foo"], { type: "text/foo" }); + expect(blob.type).toBe("text/foo"); + blob.type = "text/bar"; + expect(blob.type).toBe("text/bar"); + blob.type = "text/baz"; + expect(blob.type).toBe("text/baz"); + blob.type = "text/baz; charset=utf-8"; + expect(blob.type).toBe("text/baz; charset=utf-8"); + // @ts-expect-error + blob.type = NaN; + expect(blob.type).toBe(""); + // @ts-expect-error + blob.type = Symbol(); + expect(blob.type).toBe(""); +}); + +test("new Blob", () => { + var blob = new Blob(["Bun", "Foo"], { type: "text/foo" }); + expect(blob.size).toBe(6); + expect(blob.type).toBe("text/foo"); + + blob = new Blob(["Bun", "Foo"], { type: "\u1234" }); + expect(blob.size).toBe(6); + expect(blob.type).toBe(""); +});