Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 10 additions & 4 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2664,11 +2664,14 @@

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. Stripping the 2-byte BOM
// does not change parity, so an odd-length input would make
// `bytemuck::cast_slice` `panic!` (uncatchable). Drop the trailing
// odd byte first, mirroring Zig's `@divTrunc(bytes.len, 2)`.
let buf = &buf[..buf.len() & !1];
// +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)));

Check failure on line 2674 in src/runtime/webcore/Blob.rs

View check run for this annotation

Claude / Claude Code Review

bytemuck::cast_slice still panics on misaligned buf from sliced Blobs

`bytemuck::cast_slice::<u8, u16>` panics on two conditions — odd *length* and odd *alignment* — and this PR only fixes the first. `shared_view_raw()` returns `base + self.offset`, so e.g. `new Blob([Buffer.concat([Buffer.from([0x00, 0xFF, 0xFE]), Buffer.from('hi', 'utf16le')])]).slice(1).text()` hands an odd-address `buf` to `cast_slice`, which still aborts the process with `PodCastError::TargetAlignmentGreaterAndInputNotAligned` (the deleted comment even said "bytemuck checks align + even-len")
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
return out.to_js(global);
}

Expand Down Expand Up @@ -2847,8 +2850,11 @@
}

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. Stripping the 2-byte BOM
// does not change parity, so an odd-length input would make
// `bytemuck::cast_slice` `panic!` (uncatchable). Drop the trailing
// odd byte first, mirroring Zig's `@divTrunc(bytes.len, 2)`.
let buf = &buf[..buf.len() & !1];
// +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)));
Expand Down
25 changes: 25 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,28 @@
expect(originalType).toStartWith("multipart/form-data; boundary=");
expect(clonedType).toBe(originalType);
});

test("Blob.json()/.text() on odd-length UTF-16LE+BOM does not abort", async () => {
// Stripping the 2-byte BOM keeps the length odd, which used to make the
// u8->u16 cast `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 like Zig and parses the valid prefix.
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")]);
const j = await new Blob([oddJson]).json();
const t = await new Blob([oddText]).text();
const e = await new Blob([evenJson]).json();
process.stdout.write(JSON.stringify(j) + "|" + JSON.stringify(t) + "|" + JSON.stringify(e));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", src],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout).toBe(`{"a":1}|"hi"|{"a":1}`);
expect(exitCode).toBe(0);

Check warning on line 350 in test/js/web/fetch/blob.test.ts

View check run for this annotation

Claude / Claude Code Review

Test pipes stderr but never reads or asserts on it

nit: `stderr: "pipe"` is set but `proc.stderr` is never read. Consider adding `proc.stderr.text()` to the `Promise.all` and asserting `expect(stderr).toBe("")` first (matching the neighboring `dupeWithContentType` test and the pattern in test/CLAUDE.md) so the panic message isn't swallowed if this ever regresses.
Comment thread
claude[bot] marked this conversation as resolved.
});
Loading