import { deserialize, serialize } from "bun:jsc"; import { openSync } from "fs"; import { bunEnv } from "harness"; import { bunExe } from "js/bun/shell/test_builder"; import { join } from "path"; function jscSerializeRoundtrip(value: any) { const serialized = serialize(value); const cloned = deserialize(serialized); return cloned; } function jscSerializeRoundtripCrossProcess(original: any) { const serialized = serialize(original); const result = Bun.spawnSync({ cmd: [ bunExe(), "-e", ` import {deserialize, serialize} from "bun:jsc"; const serialized = deserialize(await Bun.stdin.bytes()); const cloned = serialize(serialized); process.stdout.write(cloned); `, ], env: bunEnv, stdin: serialized, stdout: "pipe", stderr: "inherit", }); return deserialize(result.stdout); } for (const structuredCloneFn of [structuredClone, jscSerializeRoundtrip, jscSerializeRoundtripCrossProcess]) { describe(structuredCloneFn.name, () => { let primitives_tests = [ { description: "primitive undefined", value: undefined }, { description: "primitive null", value: null }, { description: "primitive true", value: true }, { description: "primitive false", value: false }, { description: "primitive string, empty string", value: "" }, { description: "primitive string, lone high surrogate", value: "\uD800" }, { description: "primitive string, lone low surrogate", value: "\uDC00" }, { description: "primitive string, NUL", value: "\u0000" }, { description: "primitive string, astral character", value: "\uDBFF\uDFFD" }, { description: "primitive number, 0.2", value: 0.2 }, { description: "primitive number, 0", value: 0 }, { description: "primitive number, -0", value: -0 }, { description: "primitive number, NaN", value: NaN }, { description: "primitive number, Infinity", value: Infinity }, { description: "primitive number, -Infinity", value: -Infinity }, { description: "primitive number, 9007199254740992", value: 9007199254740992 }, { description: "primitive number, -9007199254740992", value: -9007199254740992 }, { description: "primitive number, 9007199254740994", value: 9007199254740994 }, { description: "primitive number, -9007199254740994", value: -9007199254740994 }, { description: "primitive BigInt, 0n", value: 0n }, { description: "primitive BigInt, -0n", value: -0n }, { description: "primitive BigInt, -9007199254740994000n", value: -9007199254740994000n }, { description: "primitive BigInt, -9007199254740994000900719925474099400090071992547409940009007199254740994000n", value: -9007199254740994000900719925474099400090071992547409940009007199254740994000n, }, ]; for (let { description, value } of primitives_tests) { test(description, () => { const cloned = structuredCloneFn(value); expect(cloned).toBe(value); }); } test("Array with primitives", () => { const input = [ undefined, null, true, false, "", "\uD800", "\uDC00", "\u0000", "\uDBFF\uDFFD", 0.2, 0, -0, NaN, Infinity, -Infinity, 9007199254740992, -9007199254740992, 9007199254740994, -9007199254740994, -12n, -0n, 0n, ]; const cloned = structuredCloneFn(input); expect(cloned).toBeInstanceOf(Array); expect(cloned).not.toBe(input); expect(cloned.length).toEqual(input.length); for (const x in input) { expect(cloned[x]).toBe(input[x]); } }); test("Object with primitives", () => { const input: any = { undefined: undefined, null: null, true: true, false: false, empty: "", "high surrogate": "\uD800", "low surrogate": "\uDC00", nul: "\u0000", astral: "\uDBFF\uDFFD", "0.2": 0.2, "0": 0, "-0": -0, NaN: NaN, Infinity: Infinity, "-Infinity": -Infinity, "9007199254740992": 9007199254740992, "-9007199254740992": -9007199254740992, "9007199254740994": 9007199254740994, "-9007199254740994": -9007199254740994, "-12n": -12n, "-0n": -0n, "0n": 0n, }; const cloned = structuredCloneFn(input); expect(cloned).toBeInstanceOf(Object); expect(cloned).not.toBeInstanceOf(Array); expect(cloned).not.toBe(input); for (const x in input) { expect(cloned[x]).toBe(input[x]); } }); test("map", () => { const input = new Map(); input.set("a", 1); input.set("b", 2); input.set("c", 3); const cloned = structuredCloneFn(input); expect(cloned).toBeInstanceOf(Map); expect(cloned).not.toBe(input); expect(cloned.size).toEqual(input.size); for (const [key, value] of input) { expect(cloned.get(key)).toBe(value); } }); test("set", () => { const input = new Set(); input.add("a"); input.add("b"); input.add("c"); const cloned = structuredCloneFn(input); expect(cloned).toBeInstanceOf(Set); expect(cloned).not.toBe(input); expect(cloned.size).toEqual(input.size); for (const value of input) { expect(cloned.has(value)).toBe(true); } }); describe("bun blobs work", () => { test("simple", async () => { const blob = new Blob(["hello"], { type: "application/octet-stream" }); const cloned = structuredCloneFn(blob); await compareBlobs(blob, cloned); }); test("empty", async () => { const emptyBlob = new Blob([], { type: "" }); const clonedEmpty = structuredCloneFn(emptyBlob); await compareBlobs(emptyBlob, clonedEmpty); }); test("empty with type", async () => { const emptyBlob = new Blob([], { type: "application/octet-stream" }); const clonedEmpty = structuredCloneFn(emptyBlob); await compareBlobs(emptyBlob, clonedEmpty); }); test("unknown type", async () => { const blob = new Blob(["hello type"], { type: "this is type" }); const cloned = structuredCloneFn(blob); await compareBlobs(blob, cloned); }); test("file from path", async () => { const blob = Bun.file(join(import.meta.dir, "example.txt")); const cloned = structuredCloneFn(blob); expect(cloned.lastModified).toBe(blob.lastModified); expect(cloned.name).toBe(blob.name); expect(cloned.size).toBe(blob.size); }); test("file from fd", async () => { const fd = openSync(join(import.meta.dir, "example.txt"), "r"); const blob = Bun.file(fd); const cloned = structuredCloneFn(blob); expect(cloned.lastModified).toBe(blob.lastModified); expect(cloned.name).toBe(blob.name); expect(cloned.size).toBe(blob.size); }); 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 = structuredCloneFn(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 = structuredCloneFn(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 = structuredCloneFn(blob); await compareBlobs(blob, cloned); }); test("unpaired low surrogate (invalid utf-8)", async () => { const blob = createBlob(encode_cesu8([0xdc00])); const cloned = structuredCloneFn(blob); await compareBlobs(blob, cloned); }); test("paired surrogates (invalid utf-8)", async () => { const blob = createBlob(encode_cesu8([0xd800, 0xdc00])); const cloned = structuredCloneFn(blob); await compareBlobs(blob, cloned); }); }); if (structuredCloneFn === structuredClone) { describe("net.BlockList works", () => { test("simple", () => { const net = require("node:net"); const blocklist = new net.BlockList(); blocklist.addAddress("123.123.123.123"); const newlist = structuredCloneFn(blocklist); expect(newlist.check("123.123.123.123")).toBeTrue(); expect(!newlist.check("123.123.123.124")).toBeTrue(); newlist.addAddress("123.123.123.124"); expect(blocklist.check("123.123.123.124")).toBeTrue(); expect(newlist.check("123.123.123.124")).toBeTrue(); }); }); describe("transferables", () => { test("ArrayBuffer", () => { const buffer = Uint8Array.from([1]).buffer; const cloned = structuredCloneFn(buffer, { transfer: [buffer] }); expect(buffer.byteLength).toBe(0); expect(cloned.byteLength).toBe(1); }); test("A detached ArrayBuffer cannot be transferred", () => { const buffer = new ArrayBuffer(2); structuredCloneFn(buffer, { transfer: [buffer] }); expect(() => { structuredCloneFn(buffer, { transfer: [buffer] }); }).toThrow(DOMException); }); test("Transferring a non-transferable platform object fails", () => { const blob = new Blob(); expect(() => { structuredCloneFn(blob, { transfer: [blob] }); }).toThrow(DOMException); }); }); } }); } async function compareBlobs(original: Blob, cloned: Blob) { expect(cloned).toBeInstanceOf(Blob); expect(cloned).not.toBe(original); expect(cloned.size).toBe(original.size); expect(cloned.type).toBe(original.type); const ab1 = await new Response(cloned).arrayBuffer(); const ab2 = await new Response(original).arrayBuffer(); expect(ab1.byteLength).toBe(ab2.byteLength); const ta1 = new Uint8Array(ab1); const ta2 = new Uint8Array(ab2); for (let i = 0; i < ta1.length; i++) { expect(ta1[i]).toBe(ta2[i]); } } function encode_cesu8(codeunits: number[]): number[] { // http://www.unicode.org/reports/tr26/ section 2.2 // only the 3-byte form is supported const rv: number[] = []; codeunits.forEach(function (codeunit) { rv.push(b("11100000") + ((codeunit & b("1111000000000000")) >> 12)); rv.push(b("10000000") + ((codeunit & b("0000111111000000")) >> 6)); rv.push(b("10000000") + (codeunit & b("0000000000111111"))); }); return rv; } function b(s: string): number { return parseInt(s, 2); } function createBlob(arr: number[]): Blob { const buffer = new ArrayBuffer(arr.length); const view = new DataView(buffer); for (let i = 0; i < arr.length; i++) { view.setUint8(i, arr[i]); } return new Blob([view]); }