Skip to content

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

Closed
robobun wants to merge 8 commits into
mainfrom
farm/59d5c8ff/bun-file-slice-negative-end
Closed

Bun.file().slice(): resolve the file size before applying the W3C relative clamp#33601
robobun wants to merge 8 commits into
mainfrom
farm/59d5c8ff/bun-file-slice-negative-end

Conversation

@robobun

@robobun robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

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.

…e-end clamp

Negative start/end (and positive overshoot) on a lazy Bun.file() blob were
clamped against the MAX_SIZE sentinel instead of the actual file size, so
slice(1, -1) on a 10-byte file reported .size == 2^52-ish and read through
to EOF. Resolve the size first, same as the .size getter does.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9787bce8-3aba-48d7-8d1d-d5127897c2c3

📥 Commits

Reviewing files that changed from the base of the PR and between 245685b and a1ce5dc.

📒 Files selected for processing (4)
  • src/runtime/webcore/Blob.rs
  • src/runtime/webcore/FileReader.rs
  • test/js/bun/http/serve.test.ts
  • test/js/web/fetch/blob.test.ts

Walkthrough

Modifies BlobExt::get_slice in Blob.rs to resolve the actual size of lazy, file-backed blobs before computing relative start/end slice bounds, removing an early-return path for zero-size blobs. Test coverage in blob.test.ts is updated and expanded to validate slice behavior consistently across in-memory and file-backed blobs, including new relative-bounds test cases.

Changes

Blob slice fix

Layer / File(s) Summary
Resolve lazy size before slice clamping
src/runtime/webcore/Blob.rs
get_slice resolves size for lazy file-backed blobs when size equals the MAX_SIZE sentinel and reading is required, and removes the previous size==0 early-return that bypassed clamping logic and store creation.
Test coverage for slice behavior
test/js/web/fetch/blob.test.ts
Removes per-case is_file branching, unifies the slice(0, 10) size assertion across cases, and adds a new describe block testing relative start/end slicing, slice-of-slice with negative end, size/content agreement, ENOENT on missing files, and contentType preservation for empty sources.

Possibly related PRs

  • oven-sh/bun#31210: Also modifies Blob.rs slice size/bounds handling so Blob.slice uses correct resolved lengths, with matching test expansions in blob.test.ts.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: resolving file size before W3C clamping in Bun.file().slice().
Description check ✅ Passed The description covers the bug, root cause, fix, and verification, though it doesn't use the repository's exact template headings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the claude label Jul 7, 2026
@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 6:15 AM PT - Jul 7th, 2026

@robobun, your commit a1ce5dc has 4 failures in Build #69751 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33601

That installs a local version of the PR into your bun-33601 executable, so you can run:

bun-33601 --bun

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix Bun.file().slice() being treated as the rest of the file #32794 - Fixes the same Bun.file().slice() bug where the file-backed slice size is overwritten in resolve_size/resolved_size, same root cause and code area in Blob.rs
  2. Bun.file: stop a file's stat size from capping reads of the whole file #33360 - Also fixes the resolve_size File-arm overwrite of a slice's concrete size using a size_is_explicit flag approach; its description acknowledges overlap with Fix Bun.file().slice() being treated as the rest of the file #32794 on the same lines

🤖 Generated with Claude Code

@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate of #32794 or #33360. Those fix resolve_size() overwriting an already-correct slice size on a later read (slice(0, 5) computes size=5 correctly, then gets stomped to file_size - offset). This PR fixes get_slice() computing the bounds wrong at creation time when start/end are negative or overshoot, because the W3C clamp runs against the MAX_SIZE sentinel instead of the file's real size:

Bun.file(p).slice(1, -1).size  // 4503599627370493, should be 8

Neither of those PRs touches get_slice and neither makes this repro pass. They're complementary, same file different function.

Comment thread src/runtime/webcore/Blob.rs
robobun and others added 2 commits July 7, 2026 03:55
It discarded the File store (so a nonexistent-file slice resolved to empty
instead of ENOENT) and the contentType argument. The clamp already yields
(0, 0) for an empty source and get_slice_from preserves both.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/web/fetch/blob.test.ts`:
- Around line 111-153: The file-writing blob slice tests are running
sequentially even though each case uses its own tempDir and isolated file I/O.
Update the parameterized slice coverage in blob.test.ts to use
test.concurrent.each for the cases table, and make the standalone ENOENT test
concurrent as well so these filesystem-heavy tests can run in parallel without
shared state. Keep the existing assertions and the tempDir/Bun.file/p-based
setup unchanged, just switch the test definitions to the concurrent variants.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1a91ecb5-6388-41a9-9227-5ac41f42c7ea

📥 Commits

Reviewing files that changed from the base of the PR and between 3f67971 and 245685b.

📒 Files selected for processing (2)
  • src/runtime/webcore/Blob.rs
  • test/js/web/fetch/blob.test.ts

Comment thread test/js/web/fetch/blob.test.ts
robobun added 5 commits July 7, 2026 04:03
on_read_chunk returned false without closing the reader or clearing its
buffer when total_readed >= max_size. On Windows the async uv read loop
kept going and the populated _buffer was handed out at EOF via the
empty-buf / on_reader_done path, so a size-0 file slice streamed the
whole file from offset to EOF. Mark done, clear the buffer, and close
instead; also hoist the cap check outside the non-empty-buf guard so the
Windows EOF-with-empty-buf call is covered.
…d one

The test asserted this range yields an empty body, which only held while
Bun.file().slice() clamped negative start against the MAX_SIZE sentinel.
With the clamp fixed it correctly addresses the last half of the file,
matching ArrayBuffer.prototype.slice. Move the case out of badRanges and
assert the bytes match full.buffer.slice(start, Infinity).
On macOS fstat on a pipe reports the currently-buffered byte count in
st_size, so resolve_size() picked that up as the blob's size and the
slice clamp then capped against a transient value. A pipe has no
meaningful size for the W3C clamp; restore the sentinel after resolving
so Bun.stdin.slice(n) keeps its prior behaviour.
@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

The diff is green: blob.test.ts, serve.test.ts, and bun-stdin-slice.test.ts pass on every lane that ran (Linux x64/aarch64/asan, macOS x64/aarch64, Windows x64/aarch64).

The remaining red is one :darwin: 26 aarch64 lane failing on buildkite-agent artifact download timed out after 120s before any tests run (build 69731, build 69751). That is a CI artifact-download infra timeout, not a test failure. Ready for review.

alii pushed a commit that referenced this pull request Aug 16, 2026
… the stream when it is used up (#39201)

### Problem
- `new Response(Bun.file(path).slice(0, 5)).body` and
`Bun.file(path).slice(0, 5).stream()` deliver the whole file from the
slice offset: 100 bytes for a 100-byte file, 1 MiB for a 1 MiB file. A
zero-length slice streams to EOF. `test/js/web/fetch/blob.test.ts`
"streams only the slice" fails on main (`Expected: 5, Received: 100`).
Regressed in #38886.
- Cause: the slice window (`FileReader.max_size` / `total_readed`) is
only applied in `FileReader::on_read_chunk`
(`src/runtime/webcore/FileReader.rs:637`). #38886 made
`FileReader::on_pull` read straight into the pull buffer with
`IOReader::read_into` (`FileReader.rs:832`), which does not go through
`on_read_chunk`, and on POSIX that is the path every pull of a regular
file takes.
- Pre-existing, same mechanism: when the window was used up,
`on_read_chunk` returned `false` without closing the reader, so a slice
of a file that continues past it never finished on the paths that still
go through `on_read_chunk` (native sinks such as HTMLRewriter, pollable
fds, Windows), and before #38886 on every path (#18192, #31675).

### Fix
- Both delivery paths share one window (`window_remaining` /
`consume_window`): `on_pull` cuts the `read_into` destination to what is
left of the window and charges what was read; `on_read_chunk` truncates
the chunk to it, as before.
- Whichever path uses the window up closes the reader (`end_at_window`),
after the bytes have been handed over. This is the same sequence as a
real EOF on that path (the final chunk, then `on_reader_done`), so
sinks, parked reads and the JS adapter end the stream the way they
already do at EOF. A zero-length window closes on the first pull without
a read (`read_into` reads nothing into an empty destination).
- Correct because the window is the blob's contract:
`ReadableStream::from_blob_copy_ref` sets `start_offset`/`max_size` from
the slice's offset and size, and `.text()` / `.arrayBuffer()` on the
same slice already return exactly that window. The reader's offset was
still honored (`pread` from `start_offset`); only the end of the window
was lost.
- Fixes #18192 and #31675 as a consequence: the window end now ends the
stream instead of leaving the reader open.
- Verified: `bun bd test test/js/web/fetch/blob.test.ts
test/js/bun/util/bun-stdin-slice.test.ts` (108 pass). The new
`blob.test.ts` cases under "a slice of a file that continues past it"
cover `.stream()` + `for await`, `.stream().bytes()`,
`Response(...).body` (all `read_into` pulls on POSIX, `on_read_chunk` on
Windows) and `HTMLRewriter.transform(new Response(slice))` (native sink,
`on_read_chunk`), each with a window inside the first read, of exactly
the first pull buffer, spanning several pulls, ending at EOF, running
past EOF, and empty, plus an unsliced file whose resolved size gives it
a window ending at EOF. The new `bun-stdin-slice.test.ts` cases stream
`Bun.stdin.slice(0, N)` over a pipe that is never closed, in one write
and in two, which is the parked-read branch of `on_read_chunk`. Against
a build with main's `FileReader.rs`, 20 of the 29 `blob.test.ts` cases
fail (wrong byte counts, or a timeout where the stream never ends; the
nine that pass are the EOF-bounded windows and the resolved-size guard)
and both stdin cases time out.
- Also run with the fix: `test/js/web/streams/streams.test.js`,
`test/js/bun/util/bun-file*.test.ts`, `bun-stdin-slice.test.ts`,
`test/js/workerd/html-rewriter.test.js`, the `spawn` stdio stream tests,
`child_process.test.ts`, `process-stdin.test.ts`,
`fetch-file-upload.test.ts`, `bun-serve-file.test.ts`; manual checks of
`Bun.stdin.stream()` / `process.stdin` over a pipe and a file redirect,
a FIFO slice whose writer stays open, and `/dev/zero` / `/dev/urandom`
slices (now deliver exactly the slice; previously unbounded on main,
hung before #38886). `cargo check -p bun_runtime` for the Windows and
macOS targets.
- Not a replacement for #31680 or #33601: both predate #38886 and carry
a patch of the old `on_read_chunk` block for the window-end hang, which
this PR makes unnecessary, but the `read_into` path this PR is about did
not exist when they were written. Their main changes (#31680: buffered
reads of character devices in `read_file.rs`; #33601: negative `slice()`
indices in `Blob::get_slice`) are independent of this.

### Background
- `FileReader` is the native source behind a file-backed
`ReadableStream` (`Bun.file().stream()`, `new Response(file).body`, and
the stream HTMLRewriter or fetch wire up for a file body). A file Blob
carries an `offset` and a `size`; for a slice these describe the window,
and `from_blob_copy_ref` copies them into the reader as `start_offset`
and `max_size`.
- It gets bytes two ways. A JS pull (`on_pull`) may read synchronously
straight into the pull buffer via `BufferedReader::read_into`.
Everything else comes from the `BufferedReader` read loop, which
delivers through `on_read_chunk`: native sinks (`pull_into_sink`),
pollable fds whose poll fired, and all reads on Windows, where reads
complete through libuv.
- The stream only ends when the reader reports done: `reader().close()`
runs `on_reader_done`, which ends an attached sink or settles a parked
read and tells the JS adapter to close; `on_pull` returns `Done` once
`reader().is_done()`. A reader that is merely no longer being read from
leaves the stream open forever, which is what the old window-exhausted
`return false` did.
@robobun

robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up for rebasing: #39201 (merged as eec9c8b) rewrote the FileReader.rs window block this PR also patches, and closing the reader at the end of the window is handled there now, so the FileReader.rs hunk here can be dropped. The Blob::get_slice change (resolving the file size before the relative clamp) is unaffected and still needed.

@alii alii closed this Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants