diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 5a75ce9ca2..7ddab4f345 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -93,10 +93,7 @@ jobs: CCACHE_DIR: ccache run: | .\scripts\env.ps1 ${{ contains(inputs.tag, '-baseline') && '-Baseline' || '' }} - Invoke-WebRequest -Uri "https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/win64/nasm-2.16.01-win64.zip" -OutFile nasm.zip - Expand-Archive nasm.zip (mkdir -Force "nasm") - $Nasm = (Get-ChildItem "nasm") - $env:Path += ";${Nasm}" + choco install -y nasm --version=2.16.01 $env:BUN_DEPS_OUT_DIR = (mkdir -Force "./bun-deps") .\scripts\all-dependencies.ps1 - name: Save Cache diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 72a7373519..e6aa8cecd5 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -117,8 +117,11 @@ pub const Blob = struct { pub const SizeType = u52; pub const max_size = std.math.maxInt(SizeType); - const serialization_version: u8 = 1; - const reserved_space_for_serialization: u32 = 128; + /// 1: Initial + /// 2: Added byte for whether it's a dom file, length and bytes for `stored_name`, + /// and f64 for `last_modified`. Removed reserved bytes, it's handled by version + /// number. + const serialization_version: u8 = 2; pub fn getFormDataEncoding(this: *Blob) ?*bun.FormData.AsyncFormData { var content_type_slice: ZigString.Slice = this.getContentType() orelse return null; @@ -319,10 +322,10 @@ pub const Blob = struct { ) !void { try writer.writeInt(u8, serialization_version, .little); - try writer.writeInt(u64, @as(u64, @intCast(this.offset)), .little); + try writer.writeInt(u64, @intCast(this.offset), .little); - try writer.writeInt(u32, @as(u32, @truncate(this.content_type.len)), .little); - _ = try writer.write(this.content_type); + try writer.writeInt(u32, @truncate(this.content_type.len), .little); + try writer.writeAll(this.content_type); try writer.writeInt(u8, @intFromBool(this.content_type_was_set), .little); const store_tag: Store.SerializeTag = if (this.store) |store| @@ -337,8 +340,8 @@ pub const Blob = struct { try store.serialize(Writer, writer); } - // reserved space for future use - _ = try writer.write(&[_]u8{0} ** reserved_space_for_serialization); + try writer.writeInt(u8, @intFromBool(this.is_jsdom_file), .little); + try writeFloat(f64, this.last_modified, Writer, writer); } pub fn onStructuredCloneSerialize( @@ -372,6 +375,25 @@ pub const Blob = struct { _ = globalThis; } + fn writeFloat( + comptime FloatType: type, + value: FloatType, + comptime Writer: type, + writer: Writer, + ) !void { + const bytes: [@sizeOf(FloatType)]u8 = @bitCast(value); + try writer.writeAll(&bytes); + } + + fn readFloat( + comptime FloatType: type, + comptime Reader: type, + reader: Reader, + ) !FloatType { + const bytes = try reader.readBoundedBytes(@sizeOf(FloatType)); + return @bitCast(bytes.slice()[0..@sizeOf(FloatType)].*); + } + fn readSlice( reader: anytype, len: usize, @@ -391,7 +413,6 @@ pub const Blob = struct { const allocator = bun.default_allocator; const version = try reader.readInt(u8, .little); - _ = version; const offset = try reader.readInt(u64, .little); @@ -404,16 +425,29 @@ pub const Blob = struct { const store_tag = try reader.readEnum(Store.SerializeTag, .little); const blob: *Blob = switch (store_tag) { - .bytes => brk: { + .bytes => bytes: { const bytes_len = try reader.readInt(u32, .little); const bytes = try readSlice(reader, bytes_len, allocator); const blob = Blob.init(bytes, allocator, globalThis); - const blob_ = Blob.new(blob); - break :brk blob_; + versions: { + if (version == 1) break :versions; + + const name_len = try reader.readInt(u32, .little); + const name = try readSlice(reader, name_len, allocator); + + if (blob.store) |store| switch (store.data) { + .bytes => |*bytes_store| bytes_store.stored_name = bun.PathString.init(name), + else => {}, + }; + + if (version == 2) break :versions; + } + + break :bytes Blob.new(blob); }, - .file => brk: { + .file => file: { const pathlike_tag = try reader.readEnum(JSC.Node.PathOrFileDescriptor.SerializeTag, .little); switch (pathlike_tag) { @@ -428,7 +462,7 @@ pub const Blob = struct { globalThis, )); - break :brk blob; + break :file blob; }, .path => { const path_len = try reader.readInt(u32, .little); @@ -444,16 +478,24 @@ pub const Blob = struct { globalThis, )); - break :brk blob; + break :file blob; }, } return .zero; }, - .empty => brk: { - break :brk Blob.new(Blob.initEmpty(globalThis)); - }, + .empty => Blob.new(Blob.initEmpty(globalThis)), }; + + versions: { + if (version == 1) break :versions; + + blob.is_jsdom_file = try reader.readInt(u8, .little) != 0; + blob.last_modified = try readFloat(f64, Reader, reader); + + if (version == 2) break :versions; + } + blob.allocator = allocator; blob.offset = @as(u52, @intCast(offset)); if (content_type.len > 0) { @@ -474,13 +516,7 @@ pub const Blob = struct { var buffer_stream = std.io.fixedBufferStream(ptr[0..total_length]); const reader = buffer_stream.reader(); - const blob = _onStructuredCloneDeserialize(globalThis, @TypeOf(reader), reader) catch return .zero; - - if (Environment.allow_assert) { - assert(total_length - reader.context.pos == reserved_space_for_serialization); - } - - return blob; + return _onStructuredCloneDeserialize(globalThis, @TypeOf(reader), reader) catch return .zero; } const URLSearchParamsConverter = struct { @@ -1380,6 +1416,8 @@ pub const Blob = struct { } } + var set_last_modified = false; + if (args.len > 2) { const options = args[2]; if (options.isObject()) { @@ -1410,11 +1448,18 @@ pub const Blob = struct { } if (options.getTruthy(globalThis, "lastModified")) |last_modified| { + set_last_modified = true; blob.last_modified = last_modified.coerce(f64, globalThis); } } } + if (!set_last_modified) { + // `lastModified` should be the current date in milliseconds if unspecified. + // https://developer.mozilla.org/en-US/docs/Web/API/File/lastModified + blob.last_modified = @floatFromInt(std.time.milliTimestamp()); + } + if (blob.content_type.len == 0) { blob.content_type = ""; blob.content_type_was_set = false; @@ -1707,14 +1752,17 @@ pub const Blob = struct { .path => |path| { const path_slice = path.slice(); try writer.writeInt(u32, @as(u32, @truncate(path_slice.len)), .little); - _ = try writer.write(path_slice); + try writer.writeAll(path_slice); }, } }, .bytes => |bytes| { const slice = bytes.slice(); - try writer.writeInt(u32, @as(u32, @truncate(slice.len)), .little); - _ = try writer.write(slice); + try writer.writeInt(u32, @truncate(slice.len), .little); + try writer.writeAll(slice); + + try writer.writeInt(u32, @truncate(bytes.stored_name.slice().len), .little); + try writer.writeAll(bytes.stored_name.slice()); }, } } diff --git a/test/js/bun/globals.test.js b/test/js/bun/globals.test.js index 2cc73fb999..1a0f7b2ef9 100644 --- a/test/js/bun/globals.test.js +++ b/test/js/bun/globals.test.js @@ -61,7 +61,7 @@ describe("File", () => { expect(file.name).toBe("bar.txt"); expect(file.type).toBe("text/plain;charset=utf-8"); expect(file.size).toBe(3); - expect(file.lastModified).toBe(0); + expect(file.lastModified).toBeGreaterThan(0); }); it("constructor with lastModified", () => { @@ -77,7 +77,7 @@ describe("File", () => { expect(file.name).toBe("undefined"); expect(file.type).toBe(""); expect(file.size).toBe(3); - expect(file.lastModified).toBe(0); + expect(file.lastModified).toBeGreaterThan(0); }); it("constructor throws invalid args", () => { @@ -129,7 +129,7 @@ describe("File", () => { expect(foo.name).toBe("bar.txt"); expect(foo.type).toBe("text/plain;charset=utf-8"); expect(foo.size).toBe(3); - expect(foo.lastModified).toBe(0); + expect(foo.lastModified).toBeGreaterThanOrEqual(0); expect(await foo.text()).toBe("foo"); }); }); diff --git a/test/js/web/workers/structured-clone.test.ts b/test/js/web/workers/structured-clone.test.ts index a35d82cd28..459bd872ad 100644 --- a/test/js/web/workers/structured-clone.test.ts +++ b/test/js/web/workers/structured-clone.test.ts @@ -166,6 +166,28 @@ describe("structured clone", () => { expect(cloned.lastModified).toBe(blob.lastModified); expect(cloned.name).toBe(blob.name); }); + describe("dom file", async () => { + test("without lastModified", async () => { + const file = new File(["hi"], "example.txt", { type: "text/plain" }); + expect(file.lastModified).toBeGreaterThan(0); + expect(file.name).toBe("example.txt"); + expect(file.size).toBe(2); + const cloned = structuredClone(file); + expect(cloned.lastModified).toBe(file.lastModified); + expect(cloned.name).toBe(file.name); + expect(cloned.size).toBe(file.size); + }); + test("with lastModified", async () => { + const file = new File(["hi"], "example.txt", { type: "text/plain", lastModified: 123 }); + expect(file.lastModified).toBe(123); + expect(file.name).toBe("example.txt"); + expect(file.size).toBe(2); + const cloned = structuredClone(file); + expect(cloned.lastModified).toBe(123); + expect(cloned.name).toBe(file.name); + expect(cloned.size).toBe(file.size); + }); + }); test("unpaired high surrogate (invalid utf-8)", async () => { const blob = createBlob(encode_cesu8([0xd800])); const cloned = structuredClone(blob);