Repository navigation
Conversation
slice(start) of a Bun.file() that nothing has stat'd subtracted the start from MAX_SIZE, the marker for a size that is not known, and stored the result as the length of the slice. `.size` was 4503599627370485 for slice(10). get_slice now gives get_slice_from the size of the slice. For a slice of a file or a pipe that runs to an unknown end, that size stays MAX_SIZE. The readers that already test for MAX_SIZE then handle the slice as they handle the whole file: `.size` stats on the first read, structuredClone writes an unknown size, HEAD of a FIFO slice answers as the whole FIFO. A start that counts back from the end and an S3 slice keep the length they had.
Collaborator
Author
|
Updated 2:38 AM PT - Oct 18th, 2026
❌ @robobun, your commit f2b6673 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 45859That installs a local version of the PR into your bun-45859 --bun |
Collaborator
Author
|
Status: draft. This rule must land in one unit with #43897 and #43910. The description says why (Downsides, and "Why this is a draft" in Notes). Reproduction on 1.3.13, 1.4.2 and a release build of main (017b243), Linux x64: printf '0123456789abcdef' > /tmp/f.txt
bun -e 'console.log(Bun.file("/tmp/f.txt").slice(10).size)'
# 1.3.13, 1.4.2, main: 4503599627370485. This branch: 6.Fail-before and pass-after: |
This was referenced Oct 18, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related to #1311
Problem
Bun.file(p).slice(10).sizeis 4503599627370485 for a 16-byte file that nothing has stat'd.text()still gives the 6 bytes.Blob::get_slice(src/runtime/webcore/Blob.rs) takesself.sizeas the default end. For such a file that isMAX_SIZE("not known"), andget_slice_fromstoresend - startas a length.Fix
get_slicenow givesget_slice_fromthe size. For a file slice that runs to an unknown end it staysMAX_SIZE, so.sizeresolves as for the whole file.slice()does no I/O, as before. The.sizegetter is unchanged.test/js/bun/util/bun-file.test.ts(13 new tests, 10 fail on main),test/js/bun/http/bun-serve-file.test.ts(1 new).Background
Blobis a window (offset,size) on a store. ABun.file()store holds a path and one cached stat, shared with its slices.resolve_size()writes the size from that stat into the Blob that asks. Reads then stop there.slice()(the types promise no I/O) and a range check in the.sizegetter (a hot path).Downsides
.sizeorexists()the slice keeps that stat: a file created or grown later reads""or too few bytes. Blob: resolve_size applies resolved_size #43897 with Bun.file: do not use a cached stat size as the byte budget of a read or a copy #43910 repairs this and needs this rule..sizeof such a slice fills its parent's stat: after an append,f.sizeis 16 (was 19).slice()costs 6 or 7 more instructions (391 to 397 in memory) and 1 more branch, no allocation or syscall.Notes
Reproduction
The same output on 1.3.13, 1.4.2 and a release build of main (017b243). A slice of a handle that was stat'd before (
f.size; f.slice(10).size) already gives 6. #1311 reported this in 2022 (2251799813685215 forslice(32)then). It was closed in 2023. Its repro readsblob.sizebefore the slice, which now hides the bug.What changes
Release builds of main (017b243) and of this branch, Linux x64, 16-byte file.
MAXisMAX_SIZE(2^52 - 1).slice(10).size, also forBun.file(fd),Bun.file(url),fs.openAsBlob(p), aBuildArtifacton diskslice(10).slice(2).sizeslice(10, Infinity),slice(10, undefined),slice(10, "text/plain").sizeslice(16).size,slice(20).size.sizeaftertext(),bytes(),stream()orexists()on the same slicestructuredClone(slice).size,deserialize(serialize(slice)).size(bun:jsc,node:v8)f5ffffffffff0f00ffffffffffff0f00Response.blob(),Request.blob(),new Blob([slice]),new File([slice]),formData.get().sizeexpect(slice).toHaveLength(6)Bun.file(missing).slice(10).sizeBun.stdin.slice(10).sizewith a pipenew Response(Bun.file(fifo).slice(10)):Content-LengthstructuredClone(Bun.file(p).slice(10)).sizewas 6 on 1.3.13. It is MAX - 10 since the size field went on the wire (1.4.2 has it).Not changed:
slice(),slice(0),slice(10, 12), every slice of an in-memory Blob, every slice of a handle that was stat'd before, every S3 slice. The bytes thattext(),bytes(),stream(),fetchuploads and Bun.serve GET send for a slice that nothing asked are the same as on main.Why this is a draft
.sizeof an open-ended slice was a field read. It now goes throughresolve_size(), as the wholeBun.file()does.resolve_size()writes the size of the cached stat (or 0 after a failed stat) into the Blob that asks, and every later read of that Blob stops at that size. On main an open-ended slice holdsMAX_SIZE - n, so nothing resolves it and its reads run to the end of the file. The whole handle has this problem today (#4930, #23902). This PR alone gives it to open-ended slices.f = Bun.file(p)over0123456789abcdef,s = f.slice(10), nothing stat'd. Main and this PR are release builds. The two right columns are debug builds of this branch with the named PRs merged in.s.sizes.sizeorawait s.exists(), create the file,s.text()abcdef""abcdefabcdefs.size, write 16 bytes,s.text()abcdef""""abcdefs.sizeorawait s.exists(), appendXYZ,s.text()abcdefXYZabcdefabcdefabcdefXYZt = f.slice(off); t.size; await t.text(), append""after round 1""after round 1/proc/version:t = f.slice(6); t.size, thent.text()orf.text()""""d = Bun.file(dest).slice(2); d.size; await Bun.write(d, Bun.file(src))if (t.size > 0)s.size, appendXYZ,f.sizes.size, unlink,await f.exists()Bun.inspect(new Response(s)), appendXYZ,f.sizeResponse {, 19Response (6 bytes) {, 16FormDatawiths, appendXYZ,f.sizef.size; structuredClone(f.slice(10)).sizeis 4503599627370485 (main: 6). With this rule it is 6..size,exists(), print, matcher or multipart body of an open-ended slice now fills it, as the same calls on the whole handle do. On mainf.slice(0).size,slice.exists()andslice.lastModifiedalready fill it. The gated tail loop works on main only becauset.sizeis always 4.5e15. Bun.file: do not use a cached stat size as the byte budget of a read or a copy #43910 alone also ends it.Question for a maintainer: may a
.sizeread, a print or a multipart body of an open-ended slice fill the stat that its parent keeps? #44504 removed that effect fromclone(). If the answer is no, this rule needs a size for file views that writes nothing to the store.Left for other changes
slice(-4),slice(0, -4),slice(10, -2)). These keep the results of main here. Bun.file().slice(): count a negative index back from the file's real end #41257 counts them from the real end. One wrong number moves:slice(10).slice(0, -2).sizeis MAX - 2 (was MAX - 12)..sizeof such a slice is now 0.slice(2 ** 52 - 2).text()returns the whole file as the other far starts do (main: 1 byte).slice(10, 1000).sizestays 990.resolve_size()stores 0 for an S3 Blob of unknown size (s3: leave the size of an S3 blob unknown in resolve_size #43934), so an S3 slice of unknown size would read 1 byte after a matcher asked for its length.slice()leaves S3 as on main:.sizestays MAX - 10 and theRangeheader staysbytes=10-4503599627370494.serialize()carriesMAX_SIZE - n. The receiver keeps it as a length, as on main (checked with a payload from 1.4.2).Bun.write(dest, slice)copies the whole source file (Honor Bun.file().slice() bounds in Bun.write #41209).fstatof a pipe reports the buffered bytes, andresolve_file_stattakes a size above 0 as a length. So.sizeof an open-ended pipe slice can be a number there. Not run: this comes from reading the code.Cost, measured
Release builds of the merge base (017b243) and of this branch, Linux x64.
perf,valgrind,straceandbloatyare not in the container. The counts come from gdb: single-step from the entry of the function to its return (callees included),catch syscall, and the callees seen while stepping..sizegetter, known size (WebCore::BlobPrototype__sizeGetterWrap): 38 -> 38 instructions for an in-memory Blob, a stat'dBun.file(), a slice of a stat'd file andslice(0, 5)of an un-stat'd file. TheBuildArtifactgetter: 38 -> 38.objdumpofBlobPrototype__sizeGetterWrap,BuildArtifactPrototype__sizeGetterWrapandS3ClientPrototype__size: 0 lines differ..sizeof an open-ended slice of an un-stat'd file: first read 39 -> 488 instructions and 0 -> 1 stat syscalls. The first.sizeof a wholeBun.file()costs 488 and 1 on both builds. Store already stat'd: 39 -> 88 instructions, 0 syscalls. Later reads: 39 -> 38.slice()(WebCore::BlobPrototype__sliceCallback, calls 200 to 202 of a loop): in-memory open-ended 391 -> 397, in-memory bounded 399 -> 406, stat'd file 405 -> 411, un-stat'd bounded 413 -> 420 instructions. Each has 1 more executed conditional branch (43 -> 44 for the first). Un-stat'd open-ended, the path of the fix: 405 -> 417 instructions, 46 -> 50 branches.slice(): 1 -> 1mi_mallocof 104 bytes (the Blob), 1 -> 1 JS cell (heapStatsover 100000 slices).size_of::<Blob>()stays 104: no field is added..sizeon one slice 0 -> 1..sizeof 50 slices of one handle 0 -> 1. First.sizeof 50 new handles 0 -> 50 (50 whole handles: 50 on both).f.sizethen 50 slices 1 -> 1. A multipart body with one open-ended slice 0 -> 2 (with a whole file: 2 on both).get_slice1548 -> 1713 bytes,get_slice_from647 -> 651 bytes. No other symbol changes size. The.textsection (0x4d993e5 bytes) and the stripped file (106911456 bytes) have the same size on both builds, because the padding between functions takes the 169 bytes.Serialization version
SERIALIZATION_VERSIONstays 4. The layout is the same. Version 4 already definesMAX_SIZEin the size field as "unknown", and the receiver then stats the file. An open-ended slice now writes that value.Other open PRs on these lines
Each merge below was built and run here (debug builds of this branch with the PR merged in).
get_slice. Keep itslengthfor indexes from the end, and passlength.is_some()tosize_to_unknown_endwhere this PR passesstart_counts_from_end. With that, the 19 tests ofbun-file.test.tsand the new HEAD test pass on the merged tree.bun-file.test.tshas a cell"slice(10).size"that expects 4503599627370485. The cell becomes 6 in whichever of the two PRs rebases second. With the cell at 6, the merged file passes (34 tests).get_slice. With Bun.file().slice(): count a negative index back from the file's real end #41257 merged in (its arithmetic, andstart.is_some_and(|i| i < 0)for the flag),blob.test.tspasses with its negative-index tests (163), andbun-file.test.tspasses.Suites
Debug build of this branch, no failure:
bun-file,bun-serve-file,blob,blob-cow,body,body-clone,structured-clone-blob-file,bun-file-read,bun-file-fd-read,bun-file-exists,fetch-file-upload,inspect,FormData,websocket-blob.On a release build of main, 10 of the 13 new tests in
bun-file.test.tsfail. The other 3 are guards:slice(-n), the pipe read, S3. On a debug build of main'ssrc/, the new HEAD test fails withContent-Length: 4503599627370485.Timeouts of the debug build in this container, none of them in a test with an open-ended slice:
serve-file-slice-read-error(also on a debug build of main), one GC test ofbun-write, three long tests ofbun-build-api, and two concurrent tests ofbun-stdin-slicein 1 run of 6 (the same with main's binary).Not run: Windows and macOS. The source change has no platform-gated line. The pipe read test skips Windows, as the test it mirrors does. The pipe size test runs there and compares the slice with the whole pipe.
no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/util/bun-file.test.ts, test/js/bun/http/bun-serve-file.test.ts