Skip to content
Open
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
10 changes: 7 additions & 3 deletions src/runtime/webcore/FileSink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ impl FileSink {

pub fn write(&self, data: &streams::Result) -> streams::Writable {
if self.done.get() {
return streams::Writable::Done;
return streams::Writable::Owned(0);
}
// SAFETY(JsCell): `IOWriter::write` buffers/writes to fd; does not call JS.
let rc = self.writer.with_mut(|w| w.write(data.slice()));
Expand All @@ -992,7 +992,7 @@ impl FileSink {

pub fn write_latin1(&self, data: &streams::Result) -> streams::Writable {
if self.done.get() {
return streams::Writable::Done;
return streams::Writable::Owned(0);
}
// SAFETY(JsCell): `IOWriter::write_latin1` buffers/writes; no JS.
let rc = self.writer.with_mut(|w| w.write_latin1(data.slice()));
Expand All @@ -1001,7 +1001,7 @@ impl FileSink {

pub fn write_utf16(&self, data: &streams::Result) -> streams::Writable {
if self.done.get() {
return streams::Writable::Done;
return streams::Writable::Owned(0);
}
// SAFETY(JsCell): `IOWriter::write_utf16` buffers/writes; no JS.
let rc = self.writer.with_mut(|w| w.write_utf16(data.slice16()));
Expand All @@ -1017,6 +1017,7 @@ impl FileSink {
// goes via the stored `*mut FileSink` backref, not this borrow.
match self.writer.with_mut(|w| w.flush()) {
WriteResult::Done(written) => {
self.done.set(true);
self.written.set(self.written.get() + written as usize); // @truncate
self.writer.with_mut(|w| w.end());
sys::Result::Ok(())
Expand All @@ -1036,6 +1037,7 @@ impl FileSink {
sys::Result::Ok(())
}
WriteResult::Wrote(written) => {
self.done.set(true);
self.written.set(self.written.get() + written as usize); // @truncate
self.writer.with_mut(|w| w.end());
sys::Result::Ok(())
Expand Down Expand Up @@ -1097,6 +1099,7 @@ impl FileSink {

match flush_result {
WriteResult::Done(written) => {
self.done.set(true);
self.update_ref(false);
self.writer.with_mut(|w| w.end());
sys::Result::Ok(JSValue::js_number(written as f64))
Expand Down Expand Up @@ -1125,6 +1128,7 @@ impl FileSink {
sys::Result::Ok(unsafe { (*promise_result).to_js() })
}
WriteResult::Wrote(written) => {
self.done.set(true);
self.writer.with_mut(|w| w.end());
sys::Result::Ok(JSValue::js_number(written as f64))
}
Expand Down
40 changes: 40 additions & 0 deletions test/js/bun/util/filesink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,46 @@ it.skipIf(!isPosix)("writing after end() fails during flush does not crash", asy
await 1;
});

it("write() and flush() after end() return 0 and undefined, not boolean true", async () => {
const path = join(tmpdirSync(), "filesink-write-after-end.txt");
const writer = Bun.file(path).writer();

// write() may return a number synchronously (POSIX regular file) or a
// Promise<number> (Windows regular-file uv_fs_write); await either.
expect(await writer.write("hello")).toBe(5);
await Promise.resolve(writer.end());

// write()/flush() are typed `number | Promise<number>`. After end() they
// returned the boolean `true` because end_from_js() never set `done` on the
// synchronous success paths, so the call fell through to the already-ended
// IOWriter. Now they return 0 / undefined like every other sink.
expect(writer.write("world")).toBe(0);
expect(writer.flush()).toBeUndefined();

// The dropped write must not appear in the file.
expect(await Bun.file(path).text()).toBe("hello");

// Calling end() again is idempotent and must not throw.
await Promise.resolve(writer.end()).catch(() => {});
});

it("write() to an exited subprocess's stdin returns 0, not boolean true", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", "process.exit(0)"],
env: bunEnv,
stdin: "pipe",
stdout: "ignore",
stderr: "ignore",
});
await proc.exited;

// The stdin FileSink's `done` is set by on_attached_process_exit. Writing
// to it returns 0 (nothing written), matching the HTTP/network sinks and
// the declared `number | Promise<number>` type.
expect(proc.stdin.write("late\n")).toBe(0);
await Promise.resolve(proc.stdin.end()).catch(() => {});
});

// On Windows the libuv write completion path re-enters JS (promise resolution)
// while a `&mut WindowsStreamingWriter` is live, so without raw-ptr laundering
// LLVM `noalias` lets release builds cache stale `is_done`/`parent` and
Expand Down
Loading