Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2664,11 +2664,19 @@ impl BlobExt for Blob {

if bom == Some(strings::BOM::Utf16Le) {
let _free = (LIFETIME == Lifetime::Temporary).then(|| TemporaryBytes(raw_bytes));
// BOM::Utf16Le ⇒ buf is UTF-16LE bytes; len is even after BOM strip.
// Mirrors Zig `bun.reinterpretSlice(u16, buf)`; bytemuck checks align + even-len.
// BOM::Utf16Le ⇒ buf is UTF-16LE bytes. `buf` may be odd-length
// (truncated input) or odd-address (`.slice(odd)` of a shared
// store) — either makes `bytemuck::cast_slice` `panic!`
// (uncatchable). `chunks_exact(2)` + `from_le_bytes` handles both:
// it drops any trailing odd byte (Zig's `@divTrunc(len, 2)`) and
// reads unaligned. `clone_utf16` copies anyway, so the extra Vec
// is one allocation either way.
let utf16: Vec<u16> = buf
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
// +1 WTF ref; `OwnedString` releases it on scope exit (Zig: `defer out.deref()`).
let out =
OwnedString::new(BunString::clone_utf16(bytemuck::cast_slice::<u8, u16>(buf)));
let out = OwnedString::new(BunString::clone_utf16(&utf16));
return out.to_js(global);
}

Expand Down Expand Up @@ -2847,11 +2855,19 @@ impl BlobExt for Blob {
}

if bom == Some(strings::BOM::Utf16Le) {
// BOM::Utf16Le ⇒ buf is UTF-16LE bytes; len is even after BOM strip.
// Mirrors Zig `bun.reinterpretSlice(u16, buf)`; bytemuck checks align + even-len.
// BOM::Utf16Le ⇒ buf is UTF-16LE bytes. `buf` may be odd-length
// (truncated input) or odd-address (`.slice(odd)` of a shared
// store) — either makes `bytemuck::cast_slice` `panic!`
// (uncatchable). `chunks_exact(2)` + `from_le_bytes` handles both:
// it drops any trailing odd byte (Zig's `@divTrunc(len, 2)`) and
// reads unaligned. `clone_utf16` copies anyway, so the extra Vec
// is one allocation either way.
let utf16: Vec<u16> = buf
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
// +1 WTF ref; `OwnedString` releases it on scope exit (Zig: `defer out.deref()`).
let mut out =
OwnedString::new(BunString::clone_utf16(bytemuck::cast_slice::<u8, u16>(buf)));
let mut out = OwnedString::new(BunString::clone_utf16(&utf16));
// PORT NOTE: Zig used `defer { free; detach }`. Reshaped to compute the
// result first, then perform the deferred work explicitly — capturing
// `&mut self` in a scopeguard closure conflicts with later uses below.
Expand Down
30 changes: 30 additions & 0 deletions test/js/web/fetch/blob.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,33 @@ test("dupe() preserves allocated content_type for Body clone", () => {
expect(originalType).toStartWith("multipart/form-data; boundary=");
expect(clonedType).toBe(originalType);
});

test("Blob.json()/.text() on odd-length/odd-aligned UTF-16LE+BOM does not abort", async () => {
// Odd length: stripping the 2-byte BOM keeps the length odd. Odd address:
// `.slice(1)` of a shared byte store hands an odd pointer straight to the
// u8->u16 cast. Either used to `panic!` and abort the whole process
// (uncatchable). Run in a subprocess: pre-fix it exits 133 with no output;
// fixed it drops the trailing odd byte and reads unaligned like Zig.
const src = `
const oddJson = Buffer.concat([Buffer.from([0xFF, 0xFE]), Buffer.from(JSON.stringify({ a: 1 }), "utf16le"), Buffer.from([0x20])]);
const oddText = Buffer.concat([Buffer.from([0xFF, 0xFE]), Buffer.from("hi", "utf16le"), Buffer.from([0x20])]);
const evenJson = Buffer.concat([Buffer.from([0xFF, 0xFE]), Buffer.from(JSON.stringify({ a: 1 }), "utf16le")]);
// Odd address: pad 1 byte then .slice(1) so the view starts at base+1.
const misaligned = Buffer.concat([Buffer.from([0x00, 0xFF, 0xFE]), Buffer.from("hi", "utf16le")]);
const j = await new Blob([oddJson]).json();
const t = await new Blob([oddText]).text();
const e = await new Blob([evenJson]).json();
const m = await new Blob([misaligned]).slice(1).text();
Comment thread
robobun marked this conversation as resolved.
process.stdout.write(JSON.stringify(j) + "|" + JSON.stringify(t) + "|" + JSON.stringify(e) + "|" + JSON.stringify(m));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", src],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toBe(`{"a":1}|"hi"|{"a":1}|"hi"`);
expect(exitCode).toBe(0);
Comment thread
claude[bot] marked this conversation as resolved.
});
Loading