Skip to content
Merged
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
41 changes: 36 additions & 5 deletions crates/perry-runtime/src/fs/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,18 @@ pub extern "C" fn js_fs_write_callback(fd_value: f64, data_value: f64, callback:
crate::closure::js_closure_call3(cb, err_val, 0.0, data_value);
return f64::from_bits(TAG_UNDEFINED);
}
let bytes = js_fs_write_sync(fd_value, data_value);
let bytes = match crate::fs::write_string_sync_result(
fd_value as i32,
data_value,
f64::from_bits(crate::value::TAG_UNDEFINED),
) {
Ok(bytes) => bytes,
Err(err) => {
let err_val = unsafe { build_fs_error_value_no_path(&err, "write") };
crate::closure::js_closure_call3(cb, err_val, 0.0, data_value);
return f64::from_bits(TAG_UNDEFINED);
}
};
if !cb.is_null() {
crate::closure::js_closure_call3(cb, f64::from_bits(TAG_NULL), bytes, data_value);
}
Expand Down Expand Up @@ -1081,7 +1092,20 @@ pub extern "C" fn js_fs_write_buffer_callback_options(
.unwrap_or_else(|| (buffer_len - offset).max(0.0));
let position = unsafe { options_number_field(options_value, b"position") }
.unwrap_or(f64::from_bits(crate::value::TAG_NULL));
let bytes = js_fs_write_buffer_sync(fd_value, buffer_value, offset, length, position);
let bytes = match crate::fs::write_buffer_sync_result(
fd_value as i32,
buffer_value,
offset,
length,
position,
) {
Ok(bytes) => bytes,
Err(err) => {
let err_val = unsafe { build_fs_error_value_no_path(&err, "write") };
crate::closure::js_closure_call3(cb, err_val, 0.0, buffer_value);
return f64::from_bits(TAG_UNDEFINED);
}
};
if !cb.is_null() {
crate::closure::js_closure_call3(cb, f64::from_bits(TAG_NULL), bytes, buffer_value);
}
Expand All @@ -1106,13 +1130,20 @@ pub extern "C" fn js_fs_write_buffer_callback(
crate::closure::js_closure_call3(cb, err_val, 0.0, buffer_value);
return f64::from_bits(TAG_UNDEFINED);
}
let bytes = js_fs_write_buffer_sync(
fd_value,
let bytes = match crate::fs::write_buffer_sync_result(
fd_value as i32,
buffer_value,
offset_value,
length_value,
position_value,
);
) {
Ok(bytes) => bytes,
Err(err) => {
let err_val = unsafe { build_fs_error_value_no_path(&err, "write") };
crate::closure::js_closure_call3(cb, err_val, 0.0, buffer_value);
return f64::from_bits(TAG_UNDEFINED);
}
};
if !cb.is_null() {
crate::closure::js_closure_call3(cb, f64::from_bits(TAG_NULL), bytes, buffer_value);
}
Expand Down
24 changes: 24 additions & 0 deletions crates/perry-runtime/src/fs/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ pub(crate) fn io_error_code(err: &std::io::Error) -> &'static str {
code if code == libc::ENOSPC => return "ENOSPC",
code if code == libc::ETIMEDOUT => return "ETIMEDOUT",
code if code == libc::EAGAIN => return "EAGAIN",
// Descriptor- and write-side errnos. Rust has no `ErrorKind` for
// these, so without an arm here they fall through to the
// `ErrorKind` match below and come back as the catch-all "EIO" —
// `fs.write()` to a closed fd reported `EIO` where Node reports
// `EBADF`. `io_error_errno` already returns the raw errno, so only
// the code string was wrong.
code if code == libc::EBADF => return "EBADF",
code if code == libc::EPIPE => return "EPIPE",
code if code == libc::EROFS => return "EROFS",
code if code == libc::EFBIG => return "EFBIG",
code if code == libc::ESPIPE => return "ESPIPE",
code if code == libc::EBUSY => return "EBUSY",
code if code == libc::EMFILE => return "EMFILE",
code if code == libc::ENFILE => return "ENFILE",
code if code == libc::EXDEV => return "EXDEV",
_ => {}
}
}
Expand Down Expand Up @@ -59,6 +74,15 @@ pub(crate) fn io_error_errno(err: &std::io::Error) -> i32 {
"ENOSPC" => -libc::ENOSPC,
"ETIMEDOUT" => -libc::ETIMEDOUT,
"EAGAIN" => -libc::EAGAIN,
"EBADF" => -libc::EBADF,
"EPIPE" => -libc::EPIPE,
"EROFS" => -libc::EROFS,
"EFBIG" => -libc::EFBIG,
"ESPIPE" => -libc::ESPIPE,
"EBUSY" => -libc::EBUSY,
"EMFILE" => -libc::EMFILE,
"ENFILE" => -libc::ENFILE,
"EXDEV" => -libc::EXDEV,
_ => -libc::EIO,
}
#[cfg(not(unix))]
Expand Down
55 changes: 40 additions & 15 deletions crates/perry-runtime/src/fs/fd_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,23 @@ pub extern "C" fn js_fs_write_string_sync_options(
position_value: f64,
) -> f64 {
crate::fs::validate::validate_fd_open(fd_value, "write");
write_string_sync_inner(fd_value as i32, data_value, position_value)
match write_string_sync_result(fd_value as i32, data_value, position_value) {
Ok(bytes) => bytes,
Err(err) => unsafe {
crate::exception::js_throw(build_fs_error_value_no_path(&err, "write"))
},
}
}

pub(crate) fn write_string_sync_inner(fd: i32, data_value: f64, position_value: f64) -> f64 {
write_string_sync_result(fd, data_value, position_value).unwrap_or(0.0)
}

pub(crate) fn write_string_sync_result(
fd: i32,
data_value: f64,
position_value: f64,
) -> Result<f64, std::io::Error> {
let bytes = bytes_from_value(data_value);
let position = if position_value.is_finite() && position_value >= 0.0 {
Some(position_value as u64)
Expand All @@ -326,16 +339,13 @@ pub(crate) fn write_string_sync_inner(fd: i32, data_value: f64, position_value:
FD_REGISTRY.with(|r| {
let mut reg = r.borrow_mut();
let Some(file) = reg.get_mut(&fd) else {
return 0.0;
return Ok(0.0);
};
let restore_pos = position.and_then(|_| file.stream_position().ok());
if let Some(pos) = position {
let _ = file.seek(SeekFrom::Start(pos));
}
let result = match file.write(&bytes) {
Ok(n) => n as f64,
Err(_) => 0.0,
};
let result = file.write(&bytes).map(|written| written as f64);
if let Some(pos) = restore_pos {
let _ = file.seek(SeekFrom::Start(pos));
}
Expand All @@ -353,13 +363,18 @@ pub extern "C" fn js_fs_write_buffer_sync(
position_value: f64,
) -> f64 {
crate::fs::validate::validate_fd_open(fd_value, "write");
write_buffer_sync_inner(
match write_buffer_sync_result(
fd_value as i32,
buffer_value,
offset_value,
length_value,
position_value,
)
) {
Ok(bytes) => bytes,
Err(err) => unsafe {
crate::exception::js_throw(build_fs_error_value_no_path(&err, "write"))
},
}
}

pub(crate) fn write_buffer_sync_inner(
Expand All @@ -369,6 +384,17 @@ pub(crate) fn write_buffer_sync_inner(
length_value: f64,
position_value: f64,
) -> f64 {
write_buffer_sync_result(fd, buffer_value, offset_value, length_value, position_value)
.unwrap_or(0.0)
}

pub(crate) fn write_buffer_sync_result(
fd: i32,
buffer_value: f64,
offset_value: f64,
length_value: f64,
position_value: f64,
) -> Result<f64, std::io::Error> {
let offset = offset_value.max(0.0) as usize;
let length = length_value.max(0.0) as usize;
let position = if position_value.is_finite() && position_value >= 0.0 {
Expand All @@ -378,12 +404,12 @@ pub(crate) fn write_buffer_sync_inner(
};
let buf = buffer_ptr_from_value(buffer_value);
if buf.is_null() {
return 0.0;
return Ok(0.0);
}
FD_REGISTRY.with(|r| {
let mut reg = r.borrow_mut();
let Some(file) = reg.get_mut(&fd) else {
return 0.0;
return Ok(0.0);
};
let restore_pos = position.and_then(|_| file.stream_position().ok());
if let Some(pos) = position {
Expand All @@ -395,14 +421,13 @@ pub(crate) fn write_buffer_sync_inner(
if let Some(pos) = restore_pos {
let _ = file.seek(SeekFrom::Start(pos));
}
return 0.0;
return Ok(0.0);
}
let n = length.min(cap - offset);
let data = crate::buffer::buffer_data(buf).add(offset);
let result = match file.write(std::slice::from_raw_parts(data, n)) {
Ok(written) => written as f64,
Err(_) => 0.0,
};
let result = file
.write(std::slice::from_raw_parts(data, n))
.map(|written| written as f64);
if let Some(pos) = restore_pos {
let _ = file.seek(SeekFrom::Start(pos));
}
Expand Down
46 changes: 46 additions & 0 deletions test-files/test_gap_fs_write_error_propagation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// `fs.write*` must distinguish a real OS write failure from a successful
// zero-byte write. A read-only fd is still open, so it bypasses EBADF
// preflight and exercises the syscall-error path.
import * as fs from "node:fs";

const dir = fs.mkdtempSync("perry-fs-write-error-");
const file = dir + "/file.txt";
fs.writeFileSync(file, "seed");
const fd = fs.openSync(file, "r");

function syncCode(label: string, fn: () => void): void {
try {
fn();
console.log(label + ": OK");
} catch (err: any) {
console.log(label + ": " + err.code + " " + err.syscall);
}
}

function callbackCode(
label: string,
invoke: (callback: (err: any) => void) => void,
): Promise<void> {
return new Promise((resolve) => {
invoke((err: any) => {
console.log(label + ": " + (err ? err.code + " " + err.syscall : "OK"));
resolve();
});
});
}

async function main(): Promise<void> {
try {
syncCode("write string sync", () => fs.writeSync(fd, "x"));
syncCode("write buffer sync", () => fs.writeSync(fd, Buffer.from("x"), 0, 1, null));
await callbackCode("write string callback", (callback) => fs.write(fd, "x", callback));
await callbackCode("write buffer callback", (callback) =>
fs.write(fd, Buffer.from("x"), 0, 1, null, callback),
);
} finally {
fs.closeSync(fd);
fs.rmSync(dir, { recursive: true, force: true });
}
}

main();
Loading