diff --git a/builtins/web/structured-clone.cpp b/builtins/web/structured-clone.cpp index e2439648..c7b9f28e 100644 --- a/builtins/web/structured-clone.cpp +++ b/builtins/web/structured-clone.cpp @@ -1,5 +1,7 @@ #include "structured-clone.h" #include "blob.h" +#include "decode.h" +#include "encode.h" #include "dom-exception.h" #include "mozilla/Assertions.h" @@ -51,9 +53,33 @@ JSObject *ReadStructuredClone(JSContext *cx, JSStructuredCloneReader *r, } case SCTAG_DOM_BLOB: { - JS::RootedString contentType(cx, JS_GetEmptyString(cx)); UniqueChars chars(reinterpret_cast(bytes)); + // Read back the Blob's `type` written after the byte data. + uint32_t type_len = 0; + uint32_t unused = 0; + if (!JS_ReadUint32Pair(r, &type_len, &unused)) { + return nullptr; + } + + JS::RootedString contentType(cx); + if (type_len > 0) { + JS::UniqueChars type_bytes(static_cast(JS_malloc(cx, type_len))); + if (!type_bytes) { + JS_ReportOutOfMemory(cx); + return nullptr; + } + if (!JS_ReadBytes(r, type_bytes.get(), type_len)) { + return nullptr; + } + contentType = core::decode(cx, std::string_view(type_bytes.get(), type_len)); + } else { + contentType = JS_GetEmptyString(cx); + } + if (!contentType) { + return nullptr; + } + RootedObject blob(cx, blob::Blob::create(cx, std::move(chars), len, contentType)); return blob; } @@ -82,8 +108,17 @@ bool WriteStructuredClone(JSContext *cx, JSStructuredCloneWriter *w, JS::HandleO } } else if (blob::Blob::is_instance(obj)) { auto *data = blob::Blob::blob(obj); + // A Blob's `type` is part of its serialized state and must be preserved, so + // it is written after the byte data as a length-prefixed UTF-8 string. + RootedString type_str(cx, blob::Blob::type(obj)); + auto type = core::encode(cx, type_str); + if (!type) { + return false; + } if (!JS_WriteUint32Pair(w, SCTAG_DOM_BLOB, data->length()) || - !JS_WriteBytes(w, (void *)data->begin(), data->length())) { + !JS_WriteBytes(w, (void *)data->begin(), data->length()) || + !JS_WriteUint32Pair(w, static_cast(type.len), 0) || + !JS_WriteBytes(w, (void *)type.begin(), type.len)) { return false; } } else { diff --git a/tests/integration/handlers.js b/tests/integration/handlers.js index fc07035a..ea45d6c9 100644 --- a/tests/integration/handlers.js +++ b/tests/integration/handlers.js @@ -5,3 +5,4 @@ export { handler as crypto } from './crypto/crypto.js'; export { handler as timers } from './timers/timers.js'; export { handler as fetch } from './fetch/fetch.js'; export { handler as event } from './event/event.js'; +export { handler as structuredclone } from './structuredclone/structuredclone.js'; diff --git a/tests/integration/structuredclone/structuredclone.js b/tests/integration/structuredclone/structuredclone.js new file mode 100644 index 00000000..0fe129a4 --- /dev/null +++ b/tests/integration/structuredclone/structuredclone.js @@ -0,0 +1,25 @@ +import { serveTest } from "../test-server.js"; +import { strictEqual } from "../../assert.js"; + +export const handler = serveTest(async (t) => { + await t.test("structuredClone-blob-preserves-type", () => { + const b = new Blob(["hello"], { type: "text/plain" }); + const c = structuredClone(b); + strictEqual(c.size, 5, "size is preserved"); + strictEqual(c.type, "text/plain", "type is preserved"); + }); + + await t.test("structuredClone-blob-empty-type", () => { + const b = new Blob(["x"]); + const c = structuredClone(b); + strictEqual(c.size, 1, "size is preserved"); + strictEqual(c.type, "", "empty type is preserved"); + }); + + await t.test("structuredClone-blob-content-and-type", async () => { + const b = new Blob(["hello world"], { type: "text/markdown" }); + const c = structuredClone(b); + strictEqual(await c.text(), "hello world", "content is preserved"); + strictEqual(c.type, "text/markdown", "type is preserved"); + }); +}); diff --git a/tests/tests.cmake b/tests/tests.cmake index 7b7840f3..fd312ccd 100644 --- a/tests/tests.cmake +++ b/tests/tests.cmake @@ -68,5 +68,6 @@ integration_tests( event fetch performance + structuredclone timers )