Bun.file().slice(): resolve the file size before applying the W3C relative clamp

Closed
RO
3mo ago
+122-20

Repro

import * as fs from "node:fs";
fs.writeFileSync("/tmp/f", "0123456789");          // 10 bytes
const b = Bun.file("/tmp/f").slice(1, -1);
console.log(b.size, JSON.stringify(await b.text()));
// Bun:  4503599627370493 "123456789"
// Node: 8 "12345678"

slice(start, -n) is the idiom for "everything but the trailing N bytes". On a file-backed blob Bun reads through to EOF, returning exactly the bytes the caller excluded, and .size reports the MAX_SIZE sentinel. .text(), .bytes(), .arrayBuffer() and .stream() all over-read. Negative start is wrong in the other direction (slice(-3) reads from byte 0), and slice(0, 100) on a 10-byte file reports .size == 100.

Cause

Blob::get_slice applies the W3C relative-start/end clamp against self.size. For a lazy Bun.file() the size is still the MAX_SIZE sentinel (the file has not been statted), so end < 0 ? max(size + end, 0) becomes a 2^52-scale number instead of fileSize + end. The in-memory Blob path is correct because its size is always known.

Fix

Resolve the file size at the top of get_slice (same call .size already makes) when the store is file-backed and the size is still the sentinel, so the clamp and the resulting .size match the in-memory Blob.slice path. S3-backed blobs are left unchanged.

Verification

USE_SYSTEM_BUN=1 bun test test/js/web/fetch/blob.test.ts   # 14 fail
bun bd test test/js/web/fetch/blob.test.ts                 # 40 pass

The existing Bun.file().slice test had an if (!is_file) guard with a comment noting "file will lazy read until EOF if the size is wrong"; that guard is removed.

Related: #32794 fixes adjacent resolve_size overwrites on an already-sliced file blob; this PR fixes slice creation on a not-yet-statted one.