blob: copy the source slice window in Bun.write(file, file.slice()), not the whole file

Closed
AL
2mo ago
+486-67

Bun.write(dst, Bun.file(src).slice(a, b)) wrote the entire source file instead of the slice. The file-to-file copy passed the destination blob's offset/size as the copy window, but the copier applies that window to the source read, and a plain-path destination has offset 0 and size MAX_SIZE, so the whole source was copied. The data itself was fine (slice(a, b).bytes() already worked); only the file-to-file copy dropped the window. Present in Bun 1.3.14 too, so this is not a port regression.

await Bun.write(dst, Bun.file(src).slice(100, 164));
// returned 4096 (the whole source size) and dst held the whole file

Fix

Pass the source blob's window to the copier and make every platform honor it.

  • POSIX: seek the source fd to the slice start before the per-OS copy. copy_file_range, sendfile, fcopyfile, and the read/write loop all read from the fd's current position. Only regular files are seeked; pipes, FIFOs, sockets, and character devices are copied from their current position (per review).
  • macOS: clonefile stays gated on offset == 0 and truncates to the slice length for head slices. The byte count Bun.write resolves with on the fcopyfile path is now the copied length (read_len was never set on that path, so it resolved with 0).
  • Windows: CopyFileWindows learns the source offset. A sliced source goes through a positioned, window-clamped uv_fs_read loop; uv_fs_copyfile is only used when there is no offset. A slice past EOF is clamped from the opened fd so the destination is never zero-padded.
  • copy_file_using_read_write_loop: a non-zero stat_size is now the exact number of bytes to copy rather than a lower bound, so the fallback stops at the slice end instead of reading the backing file to EOF. 0 still means unknown (read to EOF). fs.copyFile and cp pass the full file size, for which both behaviors are identical.
  • The destination blob's size is no longer consulted. It never carried user intent: reading .size on a BunFile caches the stat into the blob, so const f = Bun.file(dst); f.size; await Bun.write(f, biggerFile) silently truncated the copy to the destination's old length.

Tests

All in test/js/bun/io/bun-write.test.js and running on every platform: mid, head, and tail slices, a slice past EOF, an empty slice, an fd-backed source, a larger-than-1MB middle slice through both the fast path and the forced read/write loop, a pre-existing larger destination, a non-seekable FIFO source, a destination with a resolved .size, and whole-file regression guards.