Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4755,11 +4755,30 @@ pub(crate) fn write_file_with_source_destination(
}
#[cfg(not(windows))]
{
let slice_len = |blob: &Blob, store: &StoreRef, is_source: bool| {
let size = blob.size.get();
let cached = store.data.as_file().max_size;
let from_stat = if cached == MAX_SIZE {
is_source && size == 0
} else {
is_source && size <= cached
};
if size == MAX_SIZE || (blob.offset.get() == 0 && (size == cached || from_stat)) {
Comment thread
robobun marked this conversation as resolved.
Resolved
MAX_SIZE
} else {
size
}
};
let max_len = slice_len(source_blob, &source_store, true).min(slice_len(
destination_blob,
&destination_store,
false,
));
let mut file_copier = copy_file::CopyFile::create(
destination_store,
source_store,
destination_blob.offset.get(),
destination_blob.size.get(),
source_blob.offset.get(),
max_len,
ctx,
options.mkdirp_if_not_exists.unwrap_or(true),
options.mode,
Expand Down
60 changes: 37 additions & 23 deletions src/runtime/webcore/blob/copy_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -793,12 +793,30 @@ impl<'a> CopyFile<'a> {

// BSD fstat on a pipe reports bytes currently buffered in st_size;
// only a regular-file st_size is a length.
if stat.st_size != 0 && bun_sys::S::ISREG(stat.st_mode as _) {
self.max_length = (SizeType::try_from(stat.st_size)
.expect("int cast")
.min(self.max_length))
.max(self.offset)
- self.offset;
let stat_size = if stat.st_size > 0 && bun_sys::S::ISREG(stat.st_mode as _) {
stat.st_size as SizeType
} else {
0
};

if matches!(
self.destination_file_store.pathlike,
PathOrFileDescriptor::Path(_)
) {
if let bun_sys::Result::Ok(dest_stat) = bun_sys::fstat(self.destination_fd) {
if stat.st_dev == dest_stat.st_dev && stat.st_ino == dest_stat.st_ino {
self.read_len = stat_size;
self.do_close();
return;
Comment thread
robobun marked this conversation as resolved.
Resolved
}
if bun_sys::S::ISREG(dest_stat.st_mode as _) {
let _ = bun_sys::ftruncate(self.destination_fd, 0);
}
}
}

if stat_size != 0 {
self.max_length = stat_size.saturating_sub(self.offset).min(self.max_length);
if self.max_length == 0 {
self.do_close();
return;
Expand All @@ -820,6 +838,10 @@ impl<'a> CopyFile<'a> {
}
}

if self.offset > 0 && bun_sys::S::ISREG(stat.st_mode as _) {
let _ = bun_sys::set_file_offset(self.source_fd, self.offset as u64);
}

Comment thread
robobun marked this conversation as resolved.
Resolved
#[cfg(any(target_os = "linux", target_os = "android"))]
{
// Bun.write(Bun.file("a"), Bun.file("b"))
Expand Down Expand Up @@ -872,29 +894,22 @@ impl<'a> CopyFile<'a> {

#[cfg(target_os = "macos")]
{
// fcopyfile rewrites dest from offset 0 and the slice trim is
// ftruncate; both are only safe for a dest Bun opened O_TRUNC.
if matches!(
let is_sliced = self.offset > 0 || (stat_size > 0 && self.max_length < stat_size);
let is_path_dest = matches!(
self.destination_file_store.pathlike,
PathOrFileDescriptor::Path(_)
) {
if self.do_fcopy_file_with_read_write_loop_fallback().is_err() {
);
if is_sliced || !is_path_dest {
if self.do_read_write_loop_capped(self.max_length).is_err() {
self.do_close();
return;
}
if stat.st_size != 0
&& SizeType::try_from(stat.st_size).expect("int cast") > self.max_length
{
let _ = bun_sys::ftruncate(
self.destination_fd,
i64::try_from(self.max_length).expect("int cast"),
);
}
} else if self.do_read_write_loop_capped(self.max_length).is_err() {
} else if self.do_fcopy_file_with_read_write_loop_fallback().is_err() {
self.do_close();
return;
} else {
self.read_len = stat_size;
}

self.do_close();
return;
}
Expand Down Expand Up @@ -1025,8 +1040,7 @@ const PREALLOCATE_SUPPORTED: bool = cfg!(any(target_os = "linux", target_os = "a
const PREALLOCATE_LENGTH: SizeType = 2048 * 1024;

#[cfg(not(windows))]
const OPEN_DESTINATION_FLAGS: i32 =
bun_sys::O::CLOEXEC | bun_sys::O::CREAT | bun_sys::O::WRONLY | bun_sys::O::TRUNC;
const OPEN_DESTINATION_FLAGS: i32 = bun_sys::O::CLOEXEC | bun_sys::O::CREAT | bun_sys::O::WRONLY;
#[cfg(not(windows))]
const OPEN_SOURCE_FLAGS: i32 = bun_sys::O::CLOEXEC | bun_sys::O::RDONLY;

Expand Down
84 changes: 84 additions & 0 deletions test/js/bun/io/bun-write.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -943,4 +943,88 @@ int posix_fadvise(int fd, off_t offset, off_t len, int advice) {

expect(f.name).toBe(filePath);
});

describe.skipIf(isWindows)("file to file", () => {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

// https://github.com/oven-sh/bun/issues/20462
it("Bun.write(path, Bun.file(path)) does not truncate the file", async () => {
using dir = tempDir("bun-write-self-copy", { "f.txt": alphabet });
const p = join(String(dir), "f.txt");
const written = await Bun.write(p, Bun.file(p));
expect({ written, content: fs.readFileSync(p, "utf8") }).toEqual({ written: 26, content: alphabet });
});

it("Bun.write to the same inode via a hardlink does not truncate the file", async () => {
using dir = tempDir("bun-write-self-copy-hardlink", { "a.txt": alphabet });
const a = join(String(dir), "a.txt");
const b = join(String(dir), "b.txt");
fs.linkSync(a, b);
await Bun.write(b, Bun.file(a));
expect(fs.readFileSync(a, "utf8")).toBe(alphabet);
});

it.each([
["slice(a, b)", 10, 20, "KLMNOPQRST"],
["slice(0, n)", 0, 8, "ABCDEFGH"],
["slice(a)", 20, undefined, "UVWXYZ"],
])("Bun.write(dest, Bun.file(src).%s) writes only the sliced window", async (_, a, b, expected) => {
using dir = tempDir("bun-write-file-slice", {
"src.txt": alphabet,
"dst.txt": Buffer.alloc(100, "x").toString(),
});
const src = join(String(dir), "src.txt");
const dst = join(String(dir), "dst.txt");
const written = await Bun.write(dst, Bun.file(src).slice(a, b));
expect({ written, content: fs.readFileSync(dst, "utf8") }).toEqual({
written: expected.length,
content: expected,
});
});

// https://github.com/oven-sh/bun/issues/22456
it("reusing a BunFile as a destination does not cap the write at its cached size", async () => {
using dir = tempDir("bun-write-reused-dest", {});
const file1 = Bun.file(join(String(dir), "file1.txt"));
const file2 = Bun.file(join(String(dir), "file2.txt"));
await Bun.write(file1, "this is a long long long long line");
await Bun.write(file2, "short line");
await file2.exists();
await Bun.write(join(String(dir), "file3.txt"), file2);
await Bun.write(file2, file1);
expect(fs.readFileSync(join(String(dir), "file2.txt"), "utf8")).toBe("this is a long long long long line");
});

it("a cached stat size on an unsliced source BunFile does not cap the write", async () => {
using dir = tempDir("bun-write-stale-src-stat", {});
const a = join(String(dir), "a.txt");
const f = Bun.file(a);
fs.writeFileSync(a, "0123456789");
await f.exists();
fs.writeFileSync(a, "0123456789ABCDEFGHIJ");
await Bun.write(join(String(dir), "b.txt"), f);
expect(fs.readFileSync(join(String(dir), "b.txt"), "utf8")).toBe("0123456789ABCDEFGHIJ");
});
Comment thread
robobun marked this conversation as resolved.
Resolved

it("a source BunFile whose .exists() ran before the file existed copies the whole file", async () => {
using dir = tempDir("bun-write-enoent-stat", {});
const a = join(String(dir), "a.txt");
const f = Bun.file(a);
expect(await f.exists()).toBe(false);
await Bun.write(f, "hello world");
await Bun.write(join(String(dir), "b.txt"), f);
expect(fs.readFileSync(join(String(dir), "b.txt"), "utf8")).toBe("hello world");
});

it("a source BunFile whose store was re-stat'd via .lastModified does not cap the write", async () => {
using dir = tempDir("bun-write-lastmodified-desync", { "a.txt": "01234" });
const a = join(String(dir), "a.txt");
const f = Bun.file(a);
await f.exists();
await Bun.write(f, "0123456789ABCDEFGHIJ");
void f.lastModified;
await Bun.write(join(String(dir), "b.txt"), f);
expect(fs.readFileSync(join(String(dir), "b.txt"), "utf8")).toBe("0123456789ABCDEFGHIJ");
});
});
});
Loading