Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion src/jsc/bindings/wtf-bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ extern "C" int uv_tty_reset_mode(void)

static void uv__tty_make_raw(struct termios* tio)
{
assert(tio != NULL);
ASSERT(tio != NULL);

#if defined __sun || defined __MVS__
/*
Expand Down
50 changes: 42 additions & 8 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2664,11 +2664,28 @@ 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). Drop the trailing odd byte (Zig's
// `@divTrunc(len, 2)`), then borrow as `&[u16]` when already
// 2-aligned (the common case: file reads, whole stores) and only
// copy into a fresh `Vec<u16>` when `try_cast_slice` rejects on
// alignment.
let buf = &buf[..buf.len() & !1];
let unaligned: Vec<u16>;
let utf16: &[u16] = match bytemuck::try_cast_slice(buf) {
Ok(s) => s,
Err(_) => {
unaligned = buf
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
&unaligned
}
};
// +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 +2864,28 @@ 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). Drop the trailing odd byte (Zig's
// `@divTrunc(len, 2)`), then borrow as `&[u16]` when already
// 2-aligned (the common case: file reads, whole stores) and only
// copy into a fresh `Vec<u16>` when `try_cast_slice` rejects on
// alignment.
let buf = &buf[..buf.len() & !1];
let unaligned: Vec<u16>;
let utf16: &[u16] = match bytemuck::try_cast_slice(buf) {
Ok(s) => s,
Err(_) => {
unaligned = buf
.chunks_exact(2)
Comment thread
robobun marked this conversation as resolved.
Outdated
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
&unaligned
}
};
// +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.
});