use crate::fs::{fail, stash, Slot, OK};
use crate::NovaStr;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::io::{Read as _, Write as _};
thread_local! {
static FILES: RefCell<HashMap<i64, std::fs::File>> = RefCell::new(HashMap::new());
static NEXT_FD: Cell<i64> = const { Cell::new(1) };
}
fn with_fd<R>(fd: i64, f: impl FnOnce(&mut std::fs::File) -> R) -> Option<R> {
FILES.with(|files| {
let Ok(mut files) = files.try_borrow_mut() else {
crate::task::abort_with("nova_rt_file: handle table is already borrowed")
};
files.get_mut(&fd).map(f)
})
}
fn register_new_file(file: std::fs::File) -> i64 {
let fd = NEXT_FD.with(|next| {
let fd = next.get();
next.set(fd + 1);
fd
});
FILES.with(|files| {
let Ok(mut files) = files.try_borrow_mut() else {
crate::task::abort_with("nova_rt_file: handle table is already borrowed")
};
files.insert(fd, file);
});
fd
}
fn stash_i64(n: i64) {
stash(Slot::Buffer, crate::bytes::gc_bytes(&n.to_le_bytes()));
}
fn closed_fd_error() -> i64 {
fail(&std::io::Error::other("file descriptor is not open"))
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_file_open(
path: *const NovaStr,
read: i8,
write: i8,
append: i8,
truncate: i8,
create: i8,
create_new: i8,
) -> i64 {
let p = unsafe { crate::as_str(path) };
match std::fs::OpenOptions::new()
.read(read != 0)
.write(write != 0)
.append(append != 0)
.truncate(truncate != 0)
.create(create != 0)
.create_new(create_new != 0)
.open(p)
{
Ok(file) => {
stash_i64(register_new_file(file));
OK
}
Err(e) => fail(&e),
}
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_file_close(fd: i64) -> i64 {
FILES.with(|files| {
let Ok(mut files) = files.try_borrow_mut() else {
crate::task::abort_with("nova_rt_file: handle table is already borrowed")
};
files.remove(&fd);
});
OK
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_file_read(fd: i64, max: i64) -> i64 {
let Ok(cap) = usize::try_from(max) else {
crate::task::abort_with("nova_rt_file_read: negative maximum")
};
let mut buf = vec![0u8; cap];
match with_fd(fd, |file| file.read(&mut buf)) {
Some(Ok(n)) => {
buf.truncate(n);
stash(Slot::Buffer, crate::bytes::gc_bytes(&buf));
OK
}
Some(Err(e)) => fail(&e),
None => closed_fd_error(),
}
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_file_write(fd: i64, buf: *const NovaStr) -> i64 {
let bytes = unsafe { crate::bytes::as_bytes(buf) };
match with_fd(fd, |file| file.write(bytes)) {
Some(Ok(n)) => {
stash_i64(n as i64);
OK
}
Some(Err(e)) => fail(&e),
None => closed_fd_error(),
}
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_file_flush(fd: i64) -> i64 {
match with_fd(fd, |file| file.flush()) {
Some(Ok(())) => OK,
Some(Err(e)) => fail(&e),
None => closed_fd_error(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn unique_temp_path(label: &str) -> String {
std::env::temp_dir()
.join(format!("nova-rt-file-{label}-{}", std::process::id()))
.to_string_lossy()
.into_owned()
}
fn take_fd() -> i64 {
let ptr = crate::fs::take_for_test(Slot::Buffer) as *const NovaStr;
let bytes = unsafe { crate::bytes::as_bytes(ptr) };
let Ok(arr) = <[u8; 8]>::try_from(bytes) else {
panic!(
"stash_i64 stashes an 8-byte payload; got {} bytes instead -- \
either nothing was pending or the encoding changed without \
updating this test helper to match",
bytes.len()
);
};
i64::from_le_bytes(arr)
}
fn take_bytes() -> Vec<u8> {
let ptr = crate::fs::take_for_test(Slot::Buffer) as *const NovaStr;
unsafe { crate::bytes::as_bytes(ptr) }.to_vec()
}
#[test]
fn a_file_round_trips_through_the_handle_table() {
let path = unique_temp_path("round-trip");
let p = crate::gc_str(&path);
let status = unsafe { nova_rt_file_open(p, 0, 1, 0, 1, 1, 0) };
assert_eq!(status, OK, "open for writing must succeed");
let fd = take_fd();
let payload = crate::bytes::gc_bytes(b"handle-table");
assert_eq!(unsafe { nova_rt_file_write(fd, payload) }, OK);
assert_eq!(unsafe { nova_rt_file_close(fd) }, OK);
let status = unsafe { nova_rt_file_open(p, 1, 0, 0, 0, 0, 0) };
assert_eq!(status, OK, "open for reading must succeed");
let fd = take_fd();
assert_eq!(unsafe { nova_rt_file_read(fd, 64) }, OK);
assert_eq!(
take_bytes(),
b"handle-table",
"the bytes must survive the round trip"
);
assert_eq!(unsafe { nova_rt_file_close(fd) }, OK);
let _ = std::fs::remove_file(&path);
}
#[test]
fn using_a_file_after_close_is_an_error_not_a_panic() {
let path = unique_temp_path("use-after-close");
let p = crate::gc_str(&path);
assert_eq!(unsafe { nova_rt_file_open(p, 1, 1, 0, 1, 1, 0) }, OK);
let fd = take_fd();
assert_eq!(unsafe { nova_rt_file_close(fd) }, OK);
let read_status = unsafe { nova_rt_file_read(fd, 16) };
assert_ne!(
read_status, OK,
"reading a closed fd must fail, not silently succeed"
);
let payload = crate::bytes::gc_bytes(b"x");
let write_status = unsafe { nova_rt_file_write(fd, payload) };
assert_ne!(
write_status, OK,
"writing a closed fd must fail, not silently succeed"
);
let flush_status = unsafe { nova_rt_file_flush(fd) };
assert_ne!(
flush_status, OK,
"flushing a closed fd must fail, not silently succeed"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_never_issued_fd_is_an_error_not_a_panic() {
assert_ne!(unsafe { nova_rt_file_read(0, 16) }, OK);
assert_ne!(unsafe { nova_rt_file_flush(0) }, OK);
assert_eq!(
unsafe { nova_rt_file_close(0) },
OK,
"closing an fd that was never open is still OK -- close is idempotent \
over absence, not only over a fd it previously held"
);
}
#[test]
fn two_open_files_do_not_collide() {
let path_a = unique_temp_path("two-files-a");
let path_b = unique_temp_path("two-files-b");
let pa = crate::gc_str(&path_a);
let pb = crate::gc_str(&path_b);
assert_eq!(unsafe { nova_rt_file_open(pa, 0, 1, 0, 1, 1, 0) }, OK);
let fd_a = take_fd();
assert_eq!(unsafe { nova_rt_file_open(pb, 0, 1, 0, 1, 1, 0) }, OK);
let fd_b = take_fd();
assert_ne!(
fd_a, fd_b,
"two opens live at the same time must not be handed the same descriptor"
);
assert_eq!(
unsafe { nova_rt_file_write(fd_a, crate::bytes::gc_bytes(b"file-a-payload")) },
OK
);
assert_eq!(
unsafe { nova_rt_file_write(fd_b, crate::bytes::gc_bytes(b"file-b-payload")) },
OK
);
assert_eq!(unsafe { nova_rt_file_close(fd_a) }, OK);
assert_eq!(unsafe { nova_rt_file_close(fd_b) }, OK);
let got_a = std::fs::read(&path_a).expect("file a must be readable after close");
let got_b = std::fs::read(&path_b).expect("file b must be readable after close");
assert_eq!(got_a, b"file-a-payload", "file a must hold its own payload");
assert_eq!(got_b, b"file-b-payload", "file b must hold its own payload");
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
#[test]
fn close_is_idempotent_across_repeated_calls() {
let path = unique_temp_path("idempotent-close");
let p = crate::gc_str(&path);
assert_eq!(unsafe { nova_rt_file_open(p, 0, 1, 0, 1, 1, 0) }, OK);
let fd = take_fd();
assert_eq!(unsafe { nova_rt_file_close(fd) }, OK, "the first close");
assert_eq!(unsafe { nova_rt_file_close(fd) }, OK, "a second close");
assert_eq!(unsafe { nova_rt_file_close(fd) }, OK, "a third close");
let _ = std::fs::remove_file(&path);
}
#[test]
fn opening_a_missing_file_for_reading_fails() {
let path = unique_temp_path("does-not-exist");
let p = crate::gc_str(&path);
let status = unsafe { nova_rt_file_open(p, 1, 0, 0, 0, 0, 0) };
assert_ne!(
status, OK,
"opening a nonexistent path for reading must fail"
);
}
#[test]
fn create_new_on_an_existing_path_fails() {
let path = unique_temp_path("create-new-collision");
let p = crate::gc_str(&path);
assert_eq!(unsafe { nova_rt_file_open(p, 0, 1, 0, 0, 1, 0) }, OK);
let fd = take_fd();
assert_eq!(unsafe { nova_rt_file_close(fd) }, OK);
let status = unsafe { nova_rt_file_open(p, 0, 1, 0, 0, 0, 1) };
assert_ne!(status, OK, "create_new on an existing path must fail");
let _ = std::fs::remove_file(&path);
}
#[test]
fn each_constructors_flags_reach_the_os() {
let path = unique_temp_path("constructor-flags");
let p = crate::gc_str(&path);
let _ = std::fs::remove_file(&path);
assert_eq!(unsafe { nova_rt_file_open(p, 0, 1, 0, 1, 1, 0) }, OK);
let fd = take_fd();
assert_eq!(
unsafe { nova_rt_file_write(fd, crate::bytes::gc_bytes(b"first")) },
OK
);
assert_eq!(unsafe { nova_rt_file_close(fd) }, OK);
assert_eq!(
unsafe { nova_rt_file_open(p, 0, 0, 1, 0, 1, 0) },
OK,
"opening for appending must succeed"
);
let fd = take_fd();
assert_eq!(
unsafe { nova_rt_file_write(fd, crate::bytes::gc_bytes(b"-second")) },
OK
);
assert_eq!(unsafe { nova_rt_file_close(fd) }, OK);
assert_eq!(
std::fs::read(&path).expect("the appended-to file must be readable"),
b"first-second",
"appending() must leave content the file already had in place"
);
assert_eq!(unsafe { nova_rt_file_open(p, 0, 1, 0, 1, 1, 0) }, OK);
let fd = take_fd();
assert_eq!(
unsafe { nova_rt_file_write(fd, crate::bytes::gc_bytes(b"third")) },
OK
);
assert_eq!(unsafe { nova_rt_file_close(fd) }, OK);
assert_eq!(
std::fs::read(&path).expect("the rewritten file must be readable"),
b"third",
"writing() must discard content the file already had"
);
assert_eq!(unsafe { nova_rt_file_open(p, 1, 0, 0, 0, 0, 0) }, OK);
let fd = take_fd();
assert_ne!(
unsafe { nova_rt_file_write(fd, crate::bytes::gc_bytes(b"nope")) },
OK,
"writing a read-only handle must fail, not silently succeed"
);
assert_eq!(unsafe { nova_rt_file_close(fd) }, OK);
assert_eq!(
std::fs::read(&path).expect("the read-only-opened file must be readable"),
b"third",
"a write the OS rejected must not have reached the file"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn no_file_intrinsic_can_panic() {
let source = include_str!("file.rs");
let production = source.split("mod tests {").next().unwrap_or(source);
for needle in [
".borrow_mut()",
".borrow()",
"unwrap()",
".expect(",
"panic!",
"format!",
] {
assert!(
!production.contains(needle),
"a std/fs File intrinsic must not panic: `{needle}` found in this \
file's production code, which is reachable from a generated poll \
boundary with no landing pad to unwind through"
);
}
}
}