Bun.write: resolve to the byte count on the macOS fcopyfile path

Merged
RO
2mo ago
+58-13

Problem

  • On macOS, await Bun.write(dest, Bun.file(src)) resolves to 0 when the destination already exists or is on another volume. The file on disk is complete. Only the resolved number is wrong. Fixes #42060.
  • The cause is do_fcopy_file_with_read_write_loop_fallback in src/runtime/webcore/blob/copy_file.rs:523. It never writes self.read_len, and then() resolves the promise with self.read_len (line 163). The clonefile path sets it, so a fresh same-volume destination was correct.

Fix

  • The helper now returns the number of bytes copied: the source size after a successful fcopyfile, or total_written from the read/write fallback.
  • run_async stores that count in read_len. When the destination is a slice and the source is longer, it clamps the count to max_length, which is also the size ftruncate trims to. This mirrors the FreeBSD branch at lines 917-927.
  • Verified: test/js/bun/io/bun-write.test.js (new test: existing destination and a sliced destination). The test only fails before the fix on macOS. On Linux the copy_file_range path already records the count, so the test passes before and after there. cargo check -p bun_runtime --target aarch64-apple-darwin passes.

Background

  • CopyFile is the work pool job behind Bun.write(fileA, fileB) on POSIX. It picks a copy strategy per platform and stores the byte count in read_len. The JS promise resolves to that field.
  • On macOS, clonefile is tried first when both sides are paths and the destination does not exist. It fails with EEXIST for an existing destination and EXDEV across volumes. Both cases fall back to fcopyfile, which copies the whole source through the two open fds.
  • max_length is the destination window. run_async clamps it to the source size before the copy, so min(copied, max_length) is the number of bytes that remain after the trim.
Notes
  • The issue also asks about cloning over an existing destination. That is a copy strategy change and is out of scope here. This PR only fixes the resolved value.
  • While writing the test I saw that a slice on the source side (Bun.write(dest, Bun.file(src).slice(0, n))) is ignored on the file to file path on Linux too. Blob.rs:4489 passes the destination blob's offset and size to CopyFile. The test uses a destination slice, like the existing Bun.file -> Bun.file test. That is a separate bug.
  • Bun.write > should work when copyFileRange is not available > on large files times out under the debug ASAN build in this container on main as well. It does not touch the macOS code.

no test proof · iteration 2 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/io/bun-write.test.js