From 405a90cbfd8309a82258715c5c32298a65dd0b0b Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 23 Oct 2026 13:30:22 +0200 Subject: [PATCH 1/3] fs: file protocol v2, and the *at calls, pread and file times on it (storage C1) The wire a program speaks to a file server (`sdk/toyos/src/fs.rs`) is replaced, not extended: - A path is resolved from a directory the connection holds: `ROOT`, the grant's directory, or a directory it opened, by file id. An open of a directory answers a directory id; a request naming no path acts on the id itself. A `..` on the wire is the server's, the parent of the directory it is met in, and one above the grant's root is refused `Escapes`. - A request carries a tag, which its reply echoes, and its span of the window, which holds its input and its answer. The client keeps one request outstanding per capability; the wire allows `MAX_OUTSTANDING`, for the concurrent core. - New requests: `PREAD`, `PWRITE`, `SETATTR` (size, access and modification time, either to the server's now), `CREATE_UNNAMED`, `LINK` with `LINK_REPLACE`, and `READDIR` from a cursor in batches. `STAT` answers a link count and three times; `LSTAT`, `FSTAT`, `TRUNCATE` and `RMDIR` are `STAT`'s flag, `STAT` of an id, `SETATTR`'s size and `UNLINK`'s flag. - The absolute-link reply is `FOLLOW`, since `LINK` is a request now. `toyos::files` takes an `At` on every path call (the working directory or a descriptor), keeps directories as descriptors, and adds `pread`, `pwrite`, `link`, `set_times` and batched `read_dir`. A kernel directory has no handle, so its descriptor names its absolute path. The kernel gains `SYS_PREAD` and `SYS_PWRITE` for its own files, so `pread` never moves an offset on `/tmp` either. libc gains `openat`, `fstatat`, `mkdirat`, `unlinkat`, `renameat`, `linkat`, `link`, `symlinkat`, `readlinkat`, `utimensat`, `futimens`, `utimes`, `pread`, `pwrite`, `fdopendir`, `dirfd`, `O_DIRECTORY` and `O_TMPFILE`, its decisions host-tested in `atreq.rs` against the headers; `fchmodat` is refused as `chmod` is, for libc++. std's `set_times`, `hard_link`, `FileExt::read_at`/`write_at` and `MetadataExt::nlink` reach the server, and `read_dir` reads its directory's descriptor a batch at a time. The file servers speak v2 over the path-keyed `Volume` they have, and refuse what it cannot hold: `LINK`, `CREATE_UNNAMED` and an access time are `Unsupported`, a link count is 1, and a held directory is its volume path, moved with each rename the server makes. The node core (C2) replaces this. A listing's cursor is a hash of a name, so removing what was listed skips nothing after it, as a count of entries would. `readdir_bound` fills `/tmp` to two entries short of directories: opening a directory now numbers it, out of the same 16 384. Its refusals are asked of the whole listing, which `read_dir` now reports from the iterator. `tests/corpus/tinycc/214_at_calls.c` holds the calls on `/home` and `/tmp`, `pread` and `pwrite` beside a thread reading and writing through the same descriptor; with both as a seek, a transfer and a seek back every concurrent case is red on both. Closes `libc-has-no-pread-or-pwrite` (that case and its control), `std-set-times-answers-ok-and-sets-nothing` (it sets, or answers `Unsupported`) and `dir-symlink-skips-the-reconnect-every-other-path-call-makes` (`Dir::symlink` is one `path_call`; the reconnect test is the small-kernel track's step 4). Opens `a-served-file-keeps-one-name-and-no-access-time-until-the-node-core`. Co-Authored-By: Claude Opus 5.5 (1M context) --- build/libcxx.rs | 6 +- ...-and-no-access-time-until-the-node-core.md | 37 + ...e-reconnect-every-other-path-call-makes.md | 18 - issues/libc-has-no-pread-or-pwrite.md | 22 - ...bc-refuses-what-toyos-cannot-yet-answer.md | 4 +- ...-all-follows-a-link-swapped-in-mid-walk.md | 9 +- ...d-set-times-answers-ok-and-sets-nothing.md | 14 - issues/toyos-builds-itself.md | 2 +- kernel/src/object/ops.rs | 138 +-- kernel/src/syscall/dispatch.rs | 8 + sdk/abi/src/syscall.rs | 22 +- sdk/libc/arch/src/at_requests.rs | 31 + sdk/libc/arch/src/header.rs | 8 +- sdk/libc/arch/src/lib.rs | 16 +- sdk/libc/include/dirent.h | 7 +- sdk/libc/include/fcntl.h | 13 + sdk/libc/include/stdio.h | 1 + sdk/libc/include/sys/stat.h | 9 + sdk/libc/include/unistd.h | 6 + sdk/libc/src/atreq.rs | 191 +++++ sdk/libc/src/errcode.rs | 1 + sdk/libc/src/lib.rs | 1 + sdk/libc/src/posix_io.rs | 342 ++++++-- sdk/libc/src/refused.rs | 10 +- sdk/libc/src/stdio.rs | 6 +- sdk/libc/src/uname.rs | 2 +- sdk/std/os/fs.rs | 31 +- sdk/std/sys/fs.rs | 141 ++- sdk/std/sys/pal/mod.rs | 9 +- sdk/std/sys/time.rs | 6 + sdk/toyos/src/fd.rs | 5 +- sdk/toyos/src/files.rs | 257 ++++-- sdk/toyos/src/files/owner.rs | 557 +++++++----- sdk/toyos/src/fs.rs | 804 +++++++++++++----- system/fileserver/src/absent.rs | 4 + system/fileserver/src/bounds.rs | 77 +- system/fileserver/src/data.rs | 7 + system/fileserver/src/descriptions.rs | 89 +- system/fileserver/src/fat.rs | 5 + system/fileserver/src/lib.rs | 6 +- system/fileserver/src/listing.rs | 143 ++++ system/fileserver/src/main.rs | 555 ++++++++---- system/fileserver/src/resolve.rs | 47 +- system/fileserver/src/rights.rs | 15 +- system/fileserver/src/root.rs | 4 + system/fileserver/src/volume.rs | 4 + tests/corpus/tinycc/206_libc_refusals.c | 3 +- tests/corpus/tinycc/206_libc_refusals.expect | 4 +- tests/corpus/tinycc/214_at_calls.c | 283 ++++++ tests/corpus/tinycc/214_at_calls.expect | 51 ++ tests/corpus/tinycc/LICENSE | 4 +- .../bin/an_export_shares_its_descriptions.rs | 51 +- tests/guest/rust-tests/src/bin/app_view.rs | 22 +- .../guest/rust-tests/src/bin/readdir_bound.rs | 39 +- 54 files changed, 3102 insertions(+), 1045 deletions(-) create mode 100644 issues/a-served-file-keeps-one-name-and-no-access-time-until-the-node-core.md delete mode 100644 issues/dir-symlink-skips-the-reconnect-every-other-path-call-makes.md delete mode 100644 issues/libc-has-no-pread-or-pwrite.md delete mode 100644 issues/std-set-times-answers-ok-and-sets-nothing.md create mode 100644 sdk/libc/arch/src/at_requests.rs create mode 100644 sdk/libc/src/atreq.rs create mode 100644 system/fileserver/src/listing.rs create mode 100644 tests/corpus/tinycc/214_at_calls.c create mode 100644 tests/corpus/tinycc/214_at_calls.expect diff --git a/build/libcxx.rs b/build/libcxx.rs index 83004f9972e..83793e83261 100644 --- a/build/libcxx.rs +++ b/build/libcxx.rs @@ -40,9 +40,9 @@ pub(crate) const OPTIONS: [(&str, &str); 18] = [ ("LIBCXXABI_HAS_CXA_THREAD_ATEXIT_IMPL", "ON"), // The C library has no `dladdr`, so libunwind names no function it unwinds. ("LIBUNWIND_ADDITIONAL_COMPILE_FLAGS", "-D_LIBUNWIND_USE_DLADDR=0"), - // `open` refuses every directory, so no descriptor names one for `openat` - // and `unlinkat` to resolve against: `remove_all` walks a directory - // iterator, as libc++'s Windows does + // The C library has no `O_NOFOLLOW`, which libc++'s descriptor walk opens + // each directory with so a link swapped in is not followed: `remove_all` + // walks a directory iterator, as libc++'s Windows does // (`issues/remove-all-follows-a-link-swapped-in-mid-walk.md`). ("LIBCXX_ADDITIONAL_COMPILE_FLAGS", "-DREMOVE_ALL_USE_DIRECTORY_ITERATOR"), ("LIBCXX_INCLUDE_BENCHMARKS", "OFF"), diff --git a/issues/a-served-file-keeps-one-name-and-no-access-time-until-the-node-core.md b/issues/a-served-file-keeps-one-name-and-no-access-time-until-the-node-core.md new file mode 100644 index 00000000000..5d237d39dbe --- /dev/null +++ b/issues/a-served-file-keeps-one-name-and-no-access-time-until-the-node-core.md @@ -0,0 +1,37 @@ +--- +status: open +kind: defect +opened: 2026-10-23 +--- + +# A served file keeps one name and no access time until the node core + +File protocol v2 (`sdk/toyos/src/fs.rs`) carries hard links, `LINK_REPLACE`, +unnamed files, access and change times and a link count. Today's servers +are path-keyed (`system/fileserver/src/volume.rs`) and keep none of them, so +`system/fileserver/src/main.rs` answers: + +- `LINK` and `CREATE_UNNAMED`: `Unsupported`, so libc's `link`, `linkat` and + `O_TMPFILE` and std's `hard_link` fail `ENOTSUP`; +- an access time to set: `Unsupported`, so `utimensat` and `utimes` with a + null `times`, which set both times, fail `ENOTSUP`; a modification time is + set on DATA's files and refused on FAT's and on a directory; +- a link count of 1, no access or change time, and the server's pid as the + volume, so `st_dev` changes at a restart; +- a held directory by its volume path, which the server moves with each + rename it makes and marks removed at its removal. + +The kernel's `/tmp` keeps no second name, unnamed file or set time +(`sdk/toyos/src/files.rs`, `NotSupported`), and a descriptor of one of its +directories names its absolute path, so a path from it is that path's, +lexically, and a rename of the directory is not followed. + +`tests/corpus/tinycc/214_at_calls.c` asserts each refusal on `/home` and +`/tmp`. + +**Exit**: the node core and its memory-format adapter (storage C2) serve +`LINK`, `LINK_REPLACE`, `CREATE_UNNAMED`, the access time and the format's +link count, and the bcachefs adapter (C3) does on a durable DATA with +`st_dev` derived from the volume's UUID; `214_at_calls`' refusals on `/home` +become their effects. `/tmp` served by the memory format (storage W3) ends +the kernel's half. diff --git a/issues/dir-symlink-skips-the-reconnect-every-other-path-call-makes.md b/issues/dir-symlink-skips-the-reconnect-every-other-path-call-makes.md deleted file mode 100644 index bf934718775..00000000000 --- a/issues/dir-symlink-skips-the-reconnect-every-other-path-call-makes.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# `Dir::symlink` skips the reconnect every other path call makes - -`sdk/toyos/src/fs.rs`'s `Dir::symlink` writes its own request instead of going -through `Dir::path_call`, so it answers a server's restart `Gone` where every -other path call reconnects once and asks again. Its only difference of -substance is that the target is stored, not resolved, and so is not held to -`canonical`. - -Owner: the SDK's file-server client (`sdk/toyos/src/fs.rs`). - -**Exit**: `Dir::symlink` is one `path_call`, with the target exempt from -`canonical`, and a test that makes a symlink across a server's restart. diff --git a/issues/libc-has-no-pread-or-pwrite.md b/issues/libc-has-no-pread-or-pwrite.md deleted file mode 100644 index 6b0f4a28758..00000000000 --- a/issues/libc-has-no-pread-or-pwrite.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-30 ---- - -# libc has no pread or pwrite - -libc defines neither `pread` nor `pwrite`. The ABI has no positional read or write -(`sdk/abi/src/syscall.rs` has `seek`). With no header declaring `pread`, -LLVM's configure finds none, and LLVM reads a file slice with `lseek` and -`read` instead (`llvm/lib/Support/Unix/Path.inc`, `readNativeFileSlice`), which -moves an offset another thread shares just as they did. - -**Exit**: `pread` and `pwrite` are declared and defined and never move the -descriptor's offset, which a guest C case shows over a file whose every 8-byte -word holds its own offset. While one thread `read`s the file through, another -`pread`s it at one offset, and every word either reads holds the offset it was -read from. While one thread `write`s such words through a second file, another -`pwrite`s one at its offset, and every word of that file then holds its own -offset. A seek, read or write, seek-back `pread` and `pwrite` are the negative -control: the case is measured red against them. diff --git a/issues/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/libc-refuses-what-toyos-cannot-yet-answer.md index a214f0aa3e8..bb3e04aa613 100644 --- a/issues/libc-refuses-what-toyos-cannot-yet-answer.md +++ b/issues/libc-refuses-what-toyos-cannot-yet-answer.md @@ -10,13 +10,11 @@ These answer failure in their POSIX form and do nothing (`sdk/libc/src/refused.rs`, `posix_io.rs`), each asserted by `tests/corpus/tinycc/206_libc_refusals.c`. Each waits on what it names: -- `link`, `ENOSYS`: no call gives a file a second name. - `symlink` of a link the kernel would hold, `ENOSYS`: `SYS_SYMLINK` displaces what its name held (`issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md`). A file server makes one and refuses a name that exists, as POSIX has it. -- `chmod` and `fchmod`, `ENOSYS`: a file has no mode bits to set. -- `utimes`, `ENOSYS`: no call sets a file's times. +- `chmod`, `fchmod` and `fchmodat`, `ENOSYS`: a file has no mode bits to set. - `statvfs` and `fstatvfs`, `ENOSYS`: no call answers a filesystem's size or free space. - `getrlimit` and `setrlimit`, `ENOSYS`: no call answers a process's limits. diff --git a/issues/remove-all-follows-a-link-swapped-in-mid-walk.md b/issues/remove-all-follows-a-link-swapped-in-mid-walk.md index d4804b3b822..bb521290404 100644 --- a/issues/remove-all-follows-a-link-swapped-in-mid-walk.md +++ b/issues/remove-all-follows-a-link-swapped-in-mid-walk.md @@ -15,12 +15,13 @@ outside the tree it was asked to remove. libc++ says so of that walk https://reviews.llvm.org/D118134), the class of CVE-2022-21658. Its other walk holds a descriptor for each directory and resolves every name against it (`openat`, `fdopendir`, `unlinkat`, `O_DIRECTORY`, `O_NOFOLLOW`, -`AT_REMOVEDIR`), and ToyOS has no such descriptor: `open` refuses every -directory, and the kernel resolves every path from the root. +`AT_REMOVEDIR`). libc has each of those but `O_NOFOLLOW`, and a descriptor of +a directory the kernel's `/tmp` holds names its path, which the kernel +resolves from the root (`sdk/toyos/src/files.rs`). Owner: `issues/toyos-builds-itself.md`, beside its path, since no exit there needs it; its M2 clang brought `std::filesystem` in. -**Exit**: a directory opens as a handle that the kernel resolves names -against, libc defines `openat`, `fdopendir` and `unlinkat` over it, and +**Exit**: libc's `openat` takes `O_NOFOLLOW`, a descriptor of every +directory names the directory itself and not its path, and `build/libcxx.rs` passes no `REMOVE_ALL_USE_DIRECTORY_ITERATOR`. diff --git a/issues/std-set-times-answers-ok-and-sets-nothing.md b/issues/std-set-times-answers-ok-and-sets-nothing.md deleted file mode 100644 index fd03d2fd1ca..00000000000 --- a/issues/std-set-times-answers-ok-and-sets-nothing.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-28 ---- - -# std's `set_times` answers `Ok` and sets nothing - -`File::set_times`, `fs::set_times` and `set_times_nofollow` -(`sdk/std/sys/fs.rs`) return `Ok(())` having set nothing, so a tool that stamps -an output (`touch`, a build system's restat) is told it did. - -**Exit condition.** A time-setting call sets the stamp through the kernel or -the file server that holds the file, or is refused as `Unsupported`. diff --git a/issues/toyos-builds-itself.md b/issues/toyos-builds-itself.md index 668899ad69b..fa9c6f03ce7 100644 --- a/issues/toyos-builds-itself.md +++ b/issues/toyos-builds-itself.md @@ -72,7 +72,7 @@ An owner is an issue under `issues/`, or this track. | # | Item | Status | Owner | Blocked on | |---|---|---|---|---| | 7 | Threads mature enough for LLVM | not started | `std-and-libc-drop-the-answer-thread-join-gives` | — | -| 8 | Concurrent reads and writes of one file: `pread` and `pwrite`. Without them LLVM reads a file slice with `lseek` and `read`, which moves an offset other threads share | not started | `libc-has-no-pread-or-pwrite` | — | +| 8 | Concurrent reads and writes of one file: `pread` and `pwrite`, on a served file and a kernel one, which never move a shared offset (`tests/corpus/tinycc/214_at_calls.c`) | done | this track | — | | 9 | LLVM, clang and lld built by bootstrap for a ToyOS host, and the libc names they call | not started | `bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host`, `rustc-llvm-cannot-build-for-a-toyos-host`, `libc-lacks-names-llvm-for-a-toyos-host-calls`, `a-toyos-hosted-llvm-is-configured-as-running-on-the-build-machine` | — | | 10 | A std binary links the C++ runtime | not started | `a-rust-std-binary-cannot-link-the-cxx-runtime` | 9 | | 11 | A package ships its own libraries (`librustc_driver`); moot for M2's static PIEs | not started | `a-package-cannot-ship-its-own-libraries` | 3 | diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs index a85b11b9ca7..7e7ed46d601 100644 --- a/kernel/src/object/ops.rs +++ b/kernel/src/object/ops.rs @@ -391,42 +391,82 @@ fn close_ends_polls(object: &KObjectRef) -> bool { fn read_file(file: &FileObject, buf: &mut UserBytesMut) -> Option { file.with(|state| { - let size = file_cache::size(state.file_id) as usize; - let available = size.saturating_sub(state.position); - let count = buf.len().min(available); - if count == 0 { - return Some(0); - } - let mut read = 0; - let mut refused = false; - while read < count { - let abs_pos = state.position + read; - let page_idx = (abs_pos / 4096) as u32; - let offset_in_page = abs_pos % 4096; - let remaining_in_page = 4096 - offset_in_page; - let to_read = remaining_in_page.min(count - read); - // A refused page is not a page of zeros: stop short rather than fake a hole under a success. - if file_cache::read_page( - state.file_id, - page_idx, - offset_in_page, - &mut buf.sub(read, to_read), - ) - .is_err() - { - refused = true; - break; - } - read += to_read; - } - if read == 0 && refused { - return Some(SyscallError::Io.to_u64()); - } + let (answer, read) = read_file_at(state.file_id, state.position, buf); state.position += read; - Some(read as u64) + Some(answer) }) } +/// Read file `file_id` at `position` into `buf`: the syscall's answer, and how +/// many bytes it read. +fn read_file_at(file_id: file_cache::FileId, position: usize, buf: &mut UserBytesMut) -> (u64, usize) { + let size = file_cache::size(file_id) as usize; + let count = buf.len().min(size.saturating_sub(position)); + let mut read = 0; + let mut refused = false; + while read < count { + let abs_pos = position + read; + let page_idx = (abs_pos / 4096) as u32; + let offset_in_page = abs_pos % 4096; + let to_read = (4096 - offset_in_page).min(count - read); + // A refused page is not a page of zeros: stop short rather than fake a hole under a success. + if file_cache::read_page(file_id, page_idx, offset_in_page, &mut buf.sub(read, to_read)).is_err() { + refused = true; + break; + } + read += to_read; + } + if read == 0 && refused { + return (SyscallError::Io.to_u64(), 0); + } + (read as u64, read) +} + +/// Write `buf` to file `file_id` at `position`, stamping it: the syscall's +/// answer, and how many bytes it wrote. +fn write_file_at(state: &mut OpenFileState, position: usize, buf: &UserBytes) -> (u64, usize) { + // Refused, never wrapped: past this the `u32` page index would alias a low page. + if (position as u64).saturating_add(buf.len() as u64) > file_cache::MAX_FILE_SIZE { + return (SyscallError::InvalidArgument.to_u64(), 0); + } + let mut written = 0; + let mut refused = false; + while written < buf.len() { + let abs_pos = position + written; + let page_idx = (abs_pos / 4096) as u32; + let offset_in_page = abs_pos % 4096; + let to_write = (4096 - offset_in_page).min(buf.len() - written); + // A page that cannot be re-read off the device is refused, not merged into zeros. + if file_cache::write_page(state.file_id, page_idx, offset_in_page, &buf.sub(written, to_write)).is_err() { + refused = true; + break; + } + written += to_write; + } + if written == 0 && refused { + return (SyscallError::Io.to_u64(), 0); + } + state.mtime = crate::clock::mtime_now(); + file_cache::touch(state.file_id, state.mtime); + (written as u64, written) +} + +/// Read a file at `offset`, leaving its position: any other object is +/// refused `NotSupported`, as a pipe has no offset to read at. +pub fn pread(object: &KObjectRef, offset: u64, buf: &mut UserBytesMut) -> u64 { + let KObjectRef::File(file) = object else { return SyscallError::NotSupported.to_u64() }; + let Ok(position) = usize::try_from(offset) else { return 0 }; + file.with(|state| read_file_at(state.file_id, position, buf).0) +} + +/// Write a file at `offset`, leaving its position: any other object is +/// refused `NotSupported`. +pub fn pwrite(object: &KObjectRef, offset: u64, buf: &UserBytes) -> u64 { + let KObjectRef::File(file) = object else { return SyscallError::NotSupported.to_u64() }; + let Ok(position) = usize::try_from(offset) else { return SyscallError::InvalidArgument.to_u64() }; + file.with(|state| write_file_at(state, position, buf).0) +} + /// Read a device claim; takes `table` because describing a device installs handles into it. pub fn read_device( claim: &DeviceClaim, @@ -562,39 +602,9 @@ fn write_pipe(object: &KObjectRef, buf: &UserBytes) -> Option { pub fn try_write(object: &KObjectRef, buf: &UserBytes) -> Option { match object { KObjectRef::File(f) => f.with(|state| { - // Refused, never wrapped: past this the `u32` page index would alias a low page. - if (state.position as u64).saturating_add(buf.len() as u64) > file_cache::MAX_FILE_SIZE { - return Some(SyscallError::InvalidArgument.to_u64()); - } - let mut written = 0; - let mut refused = false; - while written < buf.len() { - let abs_pos = state.position + written; - let page_idx = (abs_pos / 4096) as u32; - let offset_in_page = abs_pos % 4096; - let remaining_in_page = 4096 - offset_in_page; - let to_write = remaining_in_page.min(buf.len() - written); - // A page that cannot be re-read off the device is refused, not merged into zeros. - if file_cache::write_page( - state.file_id, - page_idx, - offset_in_page, - &buf.sub(written, to_write), - ) - .is_err() - { - refused = true; - break; - } - written += to_write; - } - if written == 0 && refused { - return Some(SyscallError::Io.to_u64()); - } + let (answer, written) = write_file_at(state, state.position, buf); state.position += written; - state.mtime = crate::clock::mtime_now(); - file_cache::touch(state.file_id, state.mtime); - Some(written as u64) + Some(answer) }), KObjectRef::PipeWrite(_) | KObjectRef::Connection(_) => write_pipe(object, buf), // The whole lines `klogd`'s queue has room for, and a trailing partial one: diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index 92f832b431d..68db3d69b1b 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -351,6 +351,14 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> }, SYS_MMAP => sys_mmap(a1, a2, MmapProt(a3), MmapFlags(a4)), SYS_MUNMAP => sys_munmap(a1, a2), + SYS_PREAD => { + let Some(mut buf) = ctx.user_bytes_mut(UserAddr::new(a2), a3) else { return bad_addr }; + with_object(RawHandle(a1 as u32), Rights::READ, |o| ops::pread(o, a4, &mut buf)) + } + SYS_PWRITE => { + let Some(buf) = ctx.user_bytes(UserAddr::new(a2), a3) else { return bad_addr }; + with_object(RawHandle(a1 as u32), Rights::WRITE, |o| ops::pwrite(o, a4, &buf)) + } SYS_READ_NONBLOCK => { let Some(mut buf) = ctx.user_bytes_mut(UserAddr::new(a2), a3) else { return bad_addr }; sys_read_nonblock(RawHandle(a1 as u32), &mut buf) diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index 11621d0e49e..c0ef5048eeb 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -302,6 +302,12 @@ pub const SYS_MACHINE_STOPS: u64 = 136; /// /// [`Rights::INVENTORY`]: crate::handle::Rights::INVENTORY pub const SYS_BOOT_SOURCES: u64 = 137; +/// Read a kernel file at an offset, leaving its handle's offset where it is. +/// See [`pread`]. +pub const SYS_PREAD: u64 = 138; +/// Write a kernel file at an offset, whether or not its handle appends, and +/// leaving the handle's offset where it is. See [`pwrite`]. +pub const SYS_PWRITE: u64 = 139; /// Bins in the per-process syscall profile — one for every number this ABI /// issues, and one at the end for every number it does not. @@ -309,13 +315,13 @@ pub const SYS_BOOT_SOURCES: u64 = 137; /// **The profile's parts sum to its total, and that is the whole requirement.** /// A bin array narrower than the ABI reaches drops calls out of the line while /// the total goes on counting them. -pub const SYSCALL_PROFILE_BINS: usize = 139; +pub const SYSCALL_PROFILE_BINS: usize = 141; /// Where a number this ABI does not issue is counted. Merging is a degradation /// a reader can see in the line; dropping is one nobody can. pub const SYSCALL_PROFILE_OTHER: usize = SYSCALL_PROFILE_BINS - 1; -const _: () = assert!(SYS_BOOT_SOURCES < SYSCALL_PROFILE_OTHER as u64); +const _: () = assert!(SYS_PWRITE < SYSCALL_PROFILE_OTHER as u64); pub const WNOHANG: u64 = 1; /// Every bit `SYS_PROCESS_WAIT`'s flag word defines; a wait carrying another @@ -821,6 +827,18 @@ pub fn read(handle: RawHandle, buf: &mut [u8]) -> Result { check(syscall(SYS_READ, handle.0 as u64, buf.as_mut_ptr() as u64, buf.len() as u64, 0)).map(|n| n as usize) } +/// Read a file at `offset`: how many bytes, 0 at or past its end. Any handle +/// but a file's is [`SyscallError::NotSupported`]. +pub fn pread(handle: RawHandle, buf: &mut [u8], offset: u64) -> Result { + check(syscall(SYS_PREAD, handle.0 as u64, buf.as_mut_ptr() as u64, buf.len() as u64, offset)).map(|n| n as usize) +} + +/// Write a file at `offset`: how many bytes. Any handle but a file's is +/// [`SyscallError::NotSupported`]. +pub fn pwrite(handle: RawHandle, buf: &[u8], offset: u64) -> Result { + check(syscall(SYS_PWRITE, handle.0 as u64, buf.as_ptr() as u64, buf.len() as u64, offset)).map(|n| n as usize) +} + /// Exit the current thread only. Does not return. /// Use `exit()` to exit the entire process (all threads). pub fn thread_exit(code: i32) -> ! { diff --git a/sdk/libc/arch/src/at_requests.rs b/sdk/libc/arch/src/at_requests.rs new file mode 100644 index 00000000000..fe38afbf9a0 --- /dev/null +++ b/sdk/libc/arch/src/at_requests.rs @@ -0,0 +1,31 @@ +//! The `*at` calls' and `open`'s words as `include/fcntl.h` and +//! `include/sys/stat.h` declare them, against the ones `atreq` decides by. + +use crate::atreq; +use crate::header::{self, FCNTL_H, STAT_H}; + +#[test] +fn every_word_the_at_calls_decide_by_is_the_headers() { + let fcntl = [ + ("O_RDONLY", atreq::O_RDONLY), + ("O_WRONLY", atreq::O_WRONLY), + ("O_RDWR", atreq::O_RDWR), + ("O_CREAT", atreq::O_CREAT), + ("O_EXCL", atreq::O_EXCL), + ("O_TRUNC", atreq::O_TRUNC), + ("O_APPEND", atreq::O_APPEND), + ("O_DIRECTORY", atreq::O_DIRECTORY), + ("O_CLOEXEC", atreq::O_CLOEXEC), + ("O_TMPFILE", atreq::O_TMPFILE), + ("AT_FDCWD", atreq::AT_FDCWD), + ("AT_SYMLINK_NOFOLLOW", atreq::AT_SYMLINK_NOFOLLOW), + ("AT_REMOVEDIR", atreq::AT_REMOVEDIR), + ("AT_SYMLINK_FOLLOW", atreq::AT_SYMLINK_FOLLOW), + ("AT_EMPTY_PATH", atreq::AT_EMPTY_PATH), + ]; + for (name, value) in fcntl { + assert_eq!(header::int(FCNTL_H, name), value, "{name}"); + } + assert_eq!(i64::from(header::int(STAT_H, "UTIME_NOW")), atreq::UTIME_NOW); + assert_eq!(i64::from(header::int(STAT_H, "UTIME_OMIT")), atreq::UTIME_OMIT); +} diff --git a/sdk/libc/arch/src/header.rs b/sdk/libc/arch/src/header.rs index b593ebb3da8..38cfb791b63 100644 --- a/sdk/libc/arch/src/header.rs +++ b/sdk/libc/arch/src/header.rs @@ -5,17 +5,23 @@ pub(crate) const ERRNO_H: &str = include_str!("../../include/errno.h"); pub(crate) const FCNTL_H: &str = include_str!("../../include/fcntl.h"); pub(crate) const LIMITS_H: &str = include_str!("../../include/limits.h"); pub(crate) const SIGNAL_H: &str = include_str!("../../include/signal.h"); +pub(crate) const STAT_H: &str = include_str!("../../include/sys/stat.h"); /// Each integer `#define` in `header`, in its order. fn defines(header: &str) -> impl Iterator { header.lines().filter_map(|line| { let mut words = line.strip_prefix("#define ")?.split_whitespace(); let (name, value) = (words.next()?, words.next()?); + let (negative, value) = match value.strip_prefix('-') { + Some(rest) => (true, rest), + None => (false, value), + }; let value = match value.strip_prefix("0x") { Some(hex) => i64::from_str_radix(hex, 16), None => value.parse(), }; - Some((name, value.ok()?)) + let value = value.ok()?; + Some((name, if negative { -value } else { value })) }) } diff --git a/sdk/libc/arch/src/lib.rs b/sdk/libc/arch/src/lib.rs index 191686d9d5d..c7083fbcd77 100644 --- a/sdk/libc/arch/src/lib.rs +++ b/sdk/libc/arch/src/lib.rs @@ -7,11 +7,12 @@ //! `core::str::from_utf8`, the number reader against the host C library's and //! IEEE 754's rounding of hexadecimal input computed exactly, AArch64's //! `long double` widening against compiler-builtins', the errno codes against -//! `include/errno.h`, IPv4 addresses and their texts against the host C -//! library's, the socket option rule against the host's `setsockopt` and -//! `getsockopt`, a datagram send's bind, printf's numeric conversions against -//! the C standard's text, `realpath`'s resolution over a tree laid out here, -//! and C's allocator over a heap that checks every layout it is handed. +//! `include/errno.h`, the `*at` calls' directory, open flags and times, IPv4 +//! addresses and their texts against the host C library's, the socket option +//! rule against the host's `setsockopt` and `getsockopt`, a datagram send's +//! bind, printf's numeric conversions against the C standard's text, +//! `realpath`'s resolution over a tree laid out here, and C's allocator over +//! a heap that checks every layout it is handed. #[cfg(test)] extern crate alloc; @@ -32,6 +33,9 @@ mod errtext; #[path = "../../src/arch/mod.rs"] mod arch; #[cfg(test)] +#[path = "../../src/atreq.rs"] +mod atreq; +#[cfg(test)] #[path = "../../src/conversion.rs"] mod conversion; #[cfg(test)] @@ -100,6 +104,8 @@ mod error_texts; #[cfg(test)] mod datagram_sends; #[cfg(test)] +mod at_requests; +#[cfg(test)] mod descriptor_requests; #[cfg(test)] mod dladdr_symbols; diff --git a/sdk/libc/include/dirent.h b/sdk/libc/include/dirent.h index 794953b9ef1..841daac9d17 100644 --- a/sdk/libc/include/dirent.h +++ b/sdk/libc/include/dirent.h @@ -15,8 +15,9 @@ struct dirent { char d_name[256]; }; -/* What d_type can say. ToyOS's listing tells a directory from everything - else, so readdir answers DT_DIR or DT_UNKNOWN. */ +/* What d_type can say. A file server's listing tells a directory, a file + and a symlink apart; the kernel's tells a directory from anything else, + which it answers DT_UNKNOWN. */ #define DT_UNKNOWN 0 #define DT_FIFO 1 #define DT_CHR 2 @@ -27,6 +28,8 @@ struct dirent { #define DT_SOCK 12 DIR *opendir(const char *path); +DIR *fdopendir(int fd); +int dirfd(DIR *dir); struct dirent *readdir(DIR *dir); int closedir(DIR *dir); diff --git a/sdk/libc/include/fcntl.h b/sdk/libc/include/fcntl.h index e72e33fad25..b98dd3b8930 100644 --- a/sdk/libc/include/fcntl.h +++ b/sdk/libc/include/fcntl.h @@ -15,7 +15,19 @@ extern "C" { #define O_TRUNC 0x0200 #define O_APPEND 0x0400 #define O_NONBLOCK 0x0800 +#define O_DIRECTORY 0x10000 #define O_CLOEXEC 0x80000 +/* A file with no name in the directory the path names, which linkat names. + It carries O_DIRECTORY's bit, so a library that does not know it opens no + file where a directory was asked for. */ +#define O_TMPFILE 0x410000 + +/* The *at calls' directory and flags. */ +#define AT_FDCWD -100 +#define AT_SYMLINK_NOFOLLOW 0x100 +#define AT_REMOVEDIR 0x200 +#define AT_SYMLINK_FOLLOW 0x400 +#define AT_EMPTY_PATH 0x1000 #define F_DUPFD 0 #define F_GETFD 1 @@ -43,6 +55,7 @@ struct flock { }; int open(const char *path, int flags, ...); +int openat(int dirfd, const char *path, int flags, ...); int fcntl(int fd, int cmd, ...); int creat(const char *path, mode_t mode); diff --git a/sdk/libc/include/stdio.h b/sdk/libc/include/stdio.h index bafedda300b..67fd9ba21c2 100644 --- a/sdk/libc/include/stdio.h +++ b/sdk/libc/include/stdio.h @@ -66,6 +66,7 @@ int ungetc(int c, FILE *stream); int remove(const char *path); int rename(const char *oldpath, const char *newpath); +int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath); FILE *tmpfile(void); void perror(const char *s); diff --git a/sdk/libc/include/sys/stat.h b/sdk/libc/include/sys/stat.h index 3dfdbe5106a..071ebb41fa1 100644 --- a/sdk/libc/include/sys/stat.h +++ b/sdk/libc/include/sys/stat.h @@ -60,12 +60,21 @@ struct stat { #define S_IWOTH 0002 #define S_IXOTH 0001 +/* utimensat's and futimens' tv_nsec: the time now, or left as it is. */ +#define UTIME_NOW 0x3fffffff +#define UTIME_OMIT 0x3ffffffe + int stat(const char *path, struct stat *buf); +int fstatat(int dirfd, const char *path, struct stat *buf, int flags); +int mkdirat(int dirfd, const char *path, mode_t mode); +int utimensat(int dirfd, const char *path, const struct timespec times[2], int flags); +int futimens(int fd, const struct timespec times[2]); int fstat(int fd, struct stat *buf); int lstat(const char *path, struct stat *buf); int mkdir(const char *path, mode_t mode); int chmod(const char *path, mode_t mode); int fchmod(int fd, mode_t mode); +int fchmodat(int dirfd, const char *path, mode_t mode, int flags); mode_t umask(mode_t mask); #ifdef __cplusplus diff --git a/sdk/libc/include/unistd.h b/sdk/libc/include/unistd.h index fc0cade99aa..9376e85b740 100644 --- a/sdk/libc/include/unistd.h +++ b/sdk/libc/include/unistd.h @@ -14,11 +14,14 @@ extern "C" { ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); +ssize_t pread(int fd, void *buf, size_t count, off_t offset); +ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset); int close(int fd); off_t lseek(int fd, off_t offset, int whence); int dup(int oldfd); int dup2(int oldfd, int newfd); int unlink(const char *path); +int unlinkat(int dirfd, const char *path, int flags); int rmdir(const char *path); char *getcwd(char *buf, size_t size); int chdir(const char *path); @@ -39,8 +42,11 @@ int ftruncate(int fd, off_t length); int truncate(const char *path, off_t length); long pathconf(const char *path, int name); ssize_t readlink(const char *path, char *buf, size_t size); +ssize_t readlinkat(int dirfd, const char *path, char *buf, size_t size); int symlink(const char *target, const char *linkpath); +int symlinkat(const char *target, int dirfd, const char *linkpath); int link(const char *existing, const char *newpath); +int linkat(int fromdirfd, const char *existing, int todirfd, const char *newpath, int flags); int fchown(int fd, uid_t owner, gid_t group); int gethostname(char *name, size_t len); diff --git a/sdk/libc/src/atreq.rs b/sdk/libc/src/atreq.rs new file mode 100644 index 00000000000..315af5e5a67 --- /dev/null +++ b/sdk/libc/src/atreq.rs @@ -0,0 +1,191 @@ +//! What `open`, the `*at` calls and the time setters decide before a path is +//! asked: the directory a path is resolved from, an open's flags, and the +//! times a caller asked for. It reads nothing but what it is handed, so the +//! host tests it (`toyos-libc-copies`). + +use toyos::files::{At, Change, Open, Time}; + +use crate::errcode::{EBADF, EINVAL}; + +// The values `` declares. +pub(crate) const O_RDONLY: i32 = 0; +pub(crate) const O_WRONLY: i32 = 1; +pub(crate) const O_RDWR: i32 = 2; +pub(crate) const O_CREAT: i32 = 0x40; +pub(crate) const O_EXCL: i32 = 0x80; +pub(crate) const O_TRUNC: i32 = 0x200; +pub(crate) const O_APPEND: i32 = 0x400; +pub(crate) const O_DIRECTORY: i32 = 0x10000; +pub(crate) const O_CLOEXEC: i32 = 0x80000; +/// Includes [`O_DIRECTORY`], as POSIX's own does, so a library that does not +/// know it refuses to open a file where it asked for a directory. +pub(crate) const O_TMPFILE: i32 = 0x400000 | O_DIRECTORY; + +pub(crate) const AT_FDCWD: i32 = -100; +pub(crate) const AT_SYMLINK_NOFOLLOW: i32 = 0x100; +pub(crate) const AT_REMOVEDIR: i32 = 0x200; +pub(crate) const AT_SYMLINK_FOLLOW: i32 = 0x400; +pub(crate) const AT_EMPTY_PATH: i32 = 0x1000; + +/// ``'s: a `tv_nsec` that sets the time to now, or leaves it. +pub(crate) const UTIME_NOW: i64 = (1 << 30) - 1; +pub(crate) const UTIME_OMIT: i64 = (1 << 30) - 2; + +/// Where `path` is resolved from, `dirfd` as the `*at` calls take it: an +/// absolute path from neither, and a negative descriptor but [`AT_FDCWD`] +/// `EBADF` for any other. +pub(crate) fn at(dirfd: i32, path: &[u8]) -> Result { + match dirfd { + _ if path.first() == Some(&b'/') => Ok(At::Cwd), + AT_FDCWD => Ok(At::Cwd), + fd if fd < 0 => Err(EBADF), + fd => Ok(At::Fd(fd)), + } +} + +/// `flags`, `EINVAL` when any bit is not in `allowed`. +pub(crate) fn only(flags: i32, allowed: i32) -> Result { + match flags & !allowed { + 0 => Ok(flags), + _ => Err(EINVAL), + } +} + +/// Whether `open`'s `flags` ask to read and to write. +pub(crate) fn read_write(flags: i32) -> (bool, bool) { + let access = flags & 3; + (access == O_RDONLY || access == O_RDWR, access == O_WRONLY || access == O_RDWR) +} + +/// What `open`'s `flags` ask `toyos::files` for. An unnamed file is opened to +/// write, as POSIX requires of `O_TMPFILE`, and is never also created at the +/// name: `EINVAL`. +pub(crate) fn how(flags: i32) -> Result { + let (read, write) = read_write(flags); + let unnamed = flags & O_TMPFILE == O_TMPFILE; + if unnamed && (!write || flags & O_CREAT != 0) { + return Err(EINVAL); + } + Ok(Open { + read, + write, + append: flags & O_APPEND != 0, + create: flags & O_CREAT != 0 && flags & O_EXCL == 0, + create_new: flags & O_CREAT != 0 && flags & O_EXCL != 0, + truncate: flags & O_TRUNC != 0, + directory: flags & O_DIRECTORY != 0 && !unnamed, + unnamed, + cloexec: flags & O_CLOEXEC != 0, + }) +} + +/// One time `utimensat` was handed, `(tv_sec, tv_nsec)`: `None` to leave it. +fn time(sec: i64, nsec: i64) -> Result, i32> { + match nsec { + UTIME_NOW => Ok(Some(Time::Now)), + UTIME_OMIT => Ok(None), + 0..=999_999_999 => { + let secs = u64::try_from(sec).map_err(|_| EINVAL)?; + let nanos = secs.checked_mul(1_000_000_000).and_then(|n| n.checked_add(nsec as u64)).ok_or(EINVAL)?; + Ok(Some(Time::At(nanos))) + } + _ => Err(EINVAL), + } +} + +/// The change `utimensat`'s `times` asks for, access then modification, each +/// `(tv_sec, tv_nsec)`: both now where it was handed none. A time before the +/// epoch, which no file here keeps, is `EINVAL`. +pub(crate) fn times(times: Option<[(i64, i64); 2]>) -> Result { + let [(asec, ansec), (msec, mnsec)] = times.unwrap_or([(0, UTIME_NOW), (0, UTIME_NOW)]); + Ok(Change { size: None, atime: time(asec, ansec)?, mtime: time(msec, mnsec)? }) +} + +/// [`times`] of `utimes`' `(tv_sec, tv_usec)` pair, which has no now and no +/// omission but its absence. +pub(crate) fn utimes(times: Option<[(i64, i64); 2]>) -> Result { + let Some(pair) = times else { return self::times(None) }; + let mut nanos = [(0, 0); 2]; + for (out, (sec, usec)) in nanos.iter_mut().zip(pair) { + if !(0..1_000_000).contains(&usec) { + return Err(EINVAL); + } + *out = (sec, usec * 1000); + } + self::times(Some(nanos)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_path_is_resolved_from_the_working_directory_a_descriptor_or_neither() { + assert_eq!(at(AT_FDCWD, b"a"), Ok(At::Cwd)); + assert_eq!(at(3, b"a"), Ok(At::Fd(3))); + assert_eq!(at(0, b""), Ok(At::Fd(0))); + assert_eq!(at(-1, b"a"), Err(EBADF)); + assert_eq!(at(-101, b"a"), Err(EBADF)); + // POSIX: an absolute path ignores the descriptor, a bad one included. + assert_eq!(at(-1, b"/tmp/a"), Ok(At::Cwd)); + assert_eq!(at(7, b"/tmp/a"), Ok(At::Cwd)); + } + + #[test] + fn an_open_asks_for_what_its_flags_say() { + let read = how(O_RDONLY).unwrap(); + assert!(read.read && !read.write && !read.directory && !read.unnamed); + let dir = how(O_RDONLY | O_DIRECTORY | O_CLOEXEC).unwrap(); + assert!(dir.directory && dir.cloexec && !dir.unnamed); + let new = how(O_WRONLY | O_CREAT | O_EXCL).unwrap(); + assert!(new.create_new && !new.create && new.write && !new.read); + let tmp = how(O_TMPFILE | O_RDWR).unwrap(); + assert!(tmp.unnamed && tmp.read && tmp.write && !tmp.directory, "O_TMPFILE's directory bit names the directory it is made in"); + } + + /// POSIX: `O_TMPFILE` without a write is `EINVAL`, and so is one that also + /// creates; `O_DIRECTORY` alone is no unnamed file. + #[test] + fn an_unnamed_file_is_written_and_never_also_created() { + assert_eq!(how(O_TMPFILE | O_RDONLY), Err(EINVAL)); + assert_eq!(how(O_TMPFILE | O_WRONLY | O_CREAT), Err(EINVAL)); + assert!(!how(O_DIRECTORY | O_RDWR).unwrap().unnamed); + } + + #[test] + fn a_flag_a_call_does_not_take_is_einval() { + assert_eq!(only(AT_SYMLINK_NOFOLLOW, AT_SYMLINK_NOFOLLOW), Ok(AT_SYMLINK_NOFOLLOW)); + assert_eq!(only(0, AT_REMOVEDIR), Ok(0)); + let stat = AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH; + assert_eq!(only(stat, stat), Ok(stat)); + assert_eq!(only(AT_REMOVEDIR | 1, AT_REMOVEDIR), Err(EINVAL)); + assert_eq!(only(AT_SYMLINK_FOLLOW, AT_SYMLINK_NOFOLLOW), Err(EINVAL)); + } + + #[test] + fn utimensat_s_times_are_now_a_time_or_left() { + let both_now = Change { size: None, atime: Some(Time::Now), mtime: Some(Time::Now) }; + assert_eq!(times(None), Ok(both_now)); + assert_eq!(times(Some([(0, UTIME_NOW), (0, UTIME_NOW)])), Ok(both_now)); + assert_eq!( + times(Some([(5, UTIME_OMIT), (7, 9)])), + Ok(Change { size: None, atime: None, mtime: Some(Time::At(7_000_000_009)) }) + ); + // The seconds beside UTIME_NOW and UTIME_OMIT are ignored, as POSIX has it. + assert_eq!(times(Some([(-1, UTIME_OMIT), (-1, UTIME_NOW)])).unwrap().mtime, Some(Time::Now)); + for bad in [(0, -1), (0, 1_000_000_000), (-1, 0), (i64::MAX, 0)] { + assert_eq!(times(Some([bad, (0, UTIME_OMIT)])), Err(EINVAL), "{bad:?}"); + } + } + + #[test] + fn utimes_takes_microseconds_and_nothing_past_a_second() { + assert_eq!(utimes(None), times(None)); + assert_eq!( + utimes(Some([(1, 2), (3, 999_999)])), + Ok(Change { size: None, atime: Some(Time::At(1_000_002_000)), mtime: Some(Time::At(3_999_999_000)) }) + ); + assert_eq!(utimes(Some([(1, 1_000_000), (0, 0)])), Err(EINVAL)); + assert_eq!(utimes(Some([(1, -1), (0, 0)])), Err(EINVAL)); + } +} diff --git a/sdk/libc/src/errcode.rs b/sdk/libc/src/errcode.rs index 98385ac7a2c..2eaa6163f7e 100644 --- a/sdk/libc/src/errcode.rs +++ b/sdk/libc/src/errcode.rs @@ -96,6 +96,7 @@ pub(crate) fn of_files(e: toyos::files::Error) -> i32 { Error::NotUtf8 => EILSEQ, Error::Loop => ELOOP, Error::CrossesServers => EXDEV, + Error::NotADirectory => ENOTDIR, Error::Stream(refused) => of_stream(refused), } } diff --git a/sdk/libc/src/lib.rs b/sdk/libc/src/lib.rs index 9e28af43a99..8db880ef797 100644 --- a/sdk/libc/src/lib.rs +++ b/sdk/libc/src/lib.rs @@ -9,6 +9,7 @@ mod signal; mod sigreq; mod errtext; mod arch; +mod atreq; mod conversion; mod ctype; mod dgramreq; diff --git a/sdk/libc/src/posix_io.rs b/sdk/libc/src/posix_io.rs index 52ccf2e1f7a..e872a18f3e8 100644 --- a/sdk/libc/src/posix_io.rs +++ b/sdk/libc/src/posix_io.rs @@ -11,22 +11,14 @@ use core::ptr; use toyos_abi::{RawHandle, Rights}; use toyos_abi::syscall::{self, OpenFlags, SeekFrom}; -use toyos::files::{self, Open}; +use toyos::files::{self, At}; use crate::errcode::{EBADF, EINTR, EINVAL}; use crate::signal::Call; use crate::time::Timespec; -// The values `` declares. - -pub(crate) const O_RDONLY: i32 = 0; -pub(crate) const O_WRONLY: i32 = 1; -pub(crate) const O_RDWR: i32 = 2; -pub(crate) const O_CREAT: i32 = 0x40; -pub(crate) const O_EXCL: i32 = 0x80; -pub(crate) const O_TRUNC: i32 = 0x200; -pub(crate) const O_APPEND: i32 = 0x400; -pub(crate) const O_CLOEXEC: i32 = 0x80000; +pub(crate) use crate::atreq::{O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY}; +use crate::atreq::{read_write, AT_EMPTY_PATH, AT_FDCWD, AT_REMOVEDIR, AT_SYMLINK_FOLLOW, AT_SYMLINK_NOFOLLOW}; const SEEK_SET: i32 = 0; const SEEK_CUR: i32 = 1; @@ -98,32 +90,27 @@ pub fn c_str_to_bytes(s: *const u8) -> &'static [u8] { // File descriptor operations #[no_mangle] -pub unsafe extern "C" fn open(path: *const u8, flags: i32, _mode: u32) -> i32 { +pub unsafe extern "C" fn open(path: *const u8, flags: i32, mode: u32) -> i32 { + unsafe { openat(AT_FDCWD, path, flags, mode) } +} + +#[no_mangle] +pub unsafe extern "C" fn openat(dirfd: i32, path: *const u8, flags: i32, _mode: u32) -> i32 { let path = c_str_to_bytes(path); - let cloexec = flags & O_CLOEXEC != 0; - let (readable, writable) = read_write(flags); + let how = match crate::atreq::how(flags) { + Ok(how) => how, + Err(e) => return refuse(e), + }; if path == DEV_NULL { - return match syscall::null(null_rights(readable, writable)) { - Ok(handle) => install(handle, cloexec).unwrap_or_else(refuse), + return match syscall::null(null_rights(how.read, how.write)) { + Ok(handle) => install(handle, how.cloexec).unwrap_or_else(refuse), Err(e) => set_errno(e), }; } - let how = Open { - read: readable, - write: writable, - append: flags & O_APPEND != 0, - create: flags & O_CREAT != 0 && flags & O_EXCL == 0, - create_new: flags & O_CREAT != 0 && flags & O_EXCL != 0, - truncate: flags & O_TRUNC != 0, - cloexec, - }; - files::open(path, how).unwrap_or_else(refuse_files) -} - -/// Whether `open`'s `flags` ask to read and to write. -fn read_write(flags: i32) -> (bool, bool) { - let access = flags & 3; - (access == O_RDONLY || access == O_RDWR, access == O_WRONLY || access == O_RDWR) + match crate::atreq::at(dirfd, path) { + Ok(at) => files::open(at, path, how).unwrap_or_else(refuse_files), + Err(e) => refuse(e), + } } /// A spawn's `addopen` file, as a handle of this process's for the child. @@ -255,7 +242,7 @@ pub unsafe extern "C" fn fstat(raw_fd: i32, buf: *mut Stat) -> i32 { } /// `buf` filled from `answer`, or `errno` from its refusal. -unsafe fn answered(answer: Result, buf: *mut Stat) -> i32 { +unsafe fn answered(answer: Result, buf: *mut Stat) -> i32 { let st = match answer { Ok(st) => st, Err(e) => return refuse_files(e), @@ -266,13 +253,12 @@ unsafe fn answered(answer: Result, buf: *mut Stat) let s = unsafe { &mut *buf }; s.st_dev = st.volume; s.st_ino = st.node; - // Neither the kernel's `/tmp` nor the file server has hard links. - s.st_nlink = 1; + s.st_nlink = st.nlink; s.st_size = st.size as i64; - s.st_mtim = Timespec { - tv_sec: (st.mtime / NANOS_PER_SEC) as i64, - tv_nsec: (st.mtime % NANOS_PER_SEC) as i64, - }; + let time = |nanos: u64| Timespec { tv_sec: (nanos / NANOS_PER_SEC) as i64, tv_nsec: (nanos % NANOS_PER_SEC) as i64 }; + s.st_atim = time(st.atime); + s.st_mtim = time(st.mtime); + s.st_ctim = time(st.ctime); s.st_mode = match st.file_type { syscall::FileType::File => S_IFREG | 0o644, syscall::FileType::Directory => S_IFDIR | 0o755, @@ -378,14 +364,72 @@ pub unsafe extern "C" fn pipe(pipefd: *mut i32) -> i32 { // Path operations +/// `path` from `dirfd`, as the `*at` calls name it, with `f`'s answer as +/// theirs: 0, or `-1` and `errno`. +fn on_at(dirfd: i32, path: *const u8, f: impl FnOnce(At, &[u8]) -> Result<(), files::Error>) -> i32 { + let path = c_str_to_bytes(path); + match crate::atreq::at(dirfd, path) { + Ok(at) => f(at, path).map_or_else(refuse_files, |()| 0), + Err(e) => refuse(e), + } +} + #[no_mangle] pub unsafe extern "C" fn unlink(path: *const u8) -> i32 { - files::unlink(c_str_to_bytes(path)).map_or_else(refuse_files, |()| 0) + unsafe { unlinkat(AT_FDCWD, path, 0) } } #[no_mangle] pub unsafe extern "C" fn rmdir(path: *const u8) -> i32 { - files::rmdir(c_str_to_bytes(path)).map_or_else(refuse_files, |()| 0) + unsafe { unlinkat(AT_FDCWD, path, AT_REMOVEDIR) } +} + +#[no_mangle] +pub unsafe extern "C" fn unlinkat(dirfd: i32, path: *const u8, flags: i32) -> i32 { + match crate::atreq::only(flags, AT_REMOVEDIR) { + Ok(flags) => on_at(dirfd, path, |at, path| files::unlink(at, path, flags & AT_REMOVEDIR != 0)), + Err(e) => refuse(e), + } +} + +#[no_mangle] +pub unsafe extern "C" fn mkdirat(dirfd: i32, path: *const u8, _mode: u32) -> i32 { + on_at(dirfd, path, files::mkdir) +} + +#[no_mangle] +pub unsafe extern "C" fn renameat(from_dirfd: i32, from: *const u8, to_dirfd: i32, to: *const u8) -> i32 { + let (from, to) = (c_str_to_bytes(from), c_str_to_bytes(to)); + match (crate::atreq::at(from_dirfd, from), crate::atreq::at(to_dirfd, to)) { + (Ok(a), Ok(b)) => files::rename((a, from), (b, to)).map_or_else(refuse_files, |()| 0), + (Err(e), _) | (_, Err(e)) => refuse(e), + } +} + +#[no_mangle] +pub unsafe extern "C" fn link(existing: *const u8, new: *const u8) -> i32 { + unsafe { linkat(AT_FDCWD, existing, AT_FDCWD, new, 0) } +} + +/// The empty path from a descriptor, with `AT_EMPTY_PATH`, names what it +/// names: an unnamed file given its name. Without `AT_SYMLINK_FOLLOW` a +/// symlink is linked itself. +#[no_mangle] +pub unsafe extern "C" fn linkat(from_dirfd: i32, from: *const u8, to_dirfd: i32, to: *const u8, flags: i32) -> i32 { + let (from, to) = (c_str_to_bytes(from), c_str_to_bytes(to)); + let flags = match crate::atreq::only(flags, AT_SYMLINK_FOLLOW | AT_EMPTY_PATH) { + Ok(flags) => flags, + Err(e) => return refuse(e), + }; + if from.is_empty() && flags & AT_EMPTY_PATH == 0 { + return refuse(crate::errcode::ENOENT); + } + match (crate::atreq::at(from_dirfd, from), crate::atreq::at(to_dirfd, to)) { + (Ok(a), Ok(b)) => { + files::link((a, from), flags & AT_SYMLINK_FOLLOW != 0, (b, to)).map_or_else(refuse_files, |()| 0) + } + (Err(e), _) | (_, Err(e)) => refuse(e), + } } #[no_mangle] @@ -407,13 +451,31 @@ pub unsafe extern "C" fn chdir(path: *const u8) -> i32 { #[no_mangle] pub unsafe extern "C" fn stat(path: *const u8, buf: *mut Stat) -> i32 { - unsafe { answered(files::stat(c_str_to_bytes(path), true), buf) } + unsafe { fstatat(AT_FDCWD, path, buf, 0) } } /// `stat` that does not follow a final symbolic link. #[no_mangle] pub unsafe extern "C" fn lstat(path: *const u8, buf: *mut Stat) -> i32 { - unsafe { answered(files::stat(c_str_to_bytes(path), false), buf) } + unsafe { fstatat(AT_FDCWD, path, buf, AT_SYMLINK_NOFOLLOW) } +} + +/// The empty path from a descriptor names what it names, with +/// `AT_EMPTY_PATH`. +#[no_mangle] +pub unsafe extern "C" fn fstatat(dirfd: i32, path: *const u8, buf: *mut Stat, flags: i32) -> i32 { + let path = c_str_to_bytes(path); + let flags = match crate::atreq::only(flags, AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH) { + Ok(flags) => flags, + Err(e) => return refuse(e), + }; + if path.is_empty() && flags & AT_EMPTY_PATH == 0 { + return refuse(crate::errcode::ENOENT); + } + match crate::atreq::at(dirfd, path) { + Ok(at) => unsafe { answered(files::stat(at, path, flags & AT_SYMLINK_NOFOLLOW == 0), buf) }, + Err(e) => refuse(e), + } } /// `SYS_READLINK` answers `NotFound` for a path that is no link as for one @@ -421,11 +483,20 @@ pub unsafe extern "C" fn lstat(path: *const u8, buf: *mut Stat) -> i32 { /// file server tells them apart itself. #[no_mangle] pub unsafe extern "C" fn readlink(path: *const u8, buf: *mut u8, size: usize) -> isize { + unsafe { readlinkat(AT_FDCWD, path, buf, size) } +} + +#[no_mangle] +pub unsafe extern "C" fn readlinkat(dirfd: i32, path: *const u8, buf: *mut u8, size: usize) -> isize { let Some(len) = crate::linkreq::target_len(size) else { crate::errno::set(EINVAL); return -1; }; - match files::read_link(c_str_to_bytes(path)) { + let at = match crate::atreq::at(dirfd, c_str_to_bytes(path)) { + Ok(at) => at, + Err(e) => return refuse(e) as isize, + }; + match files::read_link(at, c_str_to_bytes(path)) { Ok(target) => { let n = target.len().min(len); // SAFETY: C's caller hands `size` bytes at `buf`, and `len` is at most it. @@ -434,7 +505,7 @@ pub unsafe extern "C" fn readlink(path: *const u8, buf: *mut u8, size: usize) -> } Err(files::Error::Kernel(syscall::SyscallError::NotFound)) => { // `stat` sets `errno` for a path that names nothing. - if unsafe { stat(path, ptr::null_mut()) } == 0 { + if unsafe { fstatat(dirfd, path, ptr::null_mut(), AT_SYMLINK_NOFOLLOW) } == 0 { crate::errno::set(EINVAL); } -1 @@ -448,11 +519,21 @@ pub unsafe extern "C" fn readlink(path: *const u8, buf: *mut u8, size: usize) -> /// races its answer, so a link the kernel would hold is refused `ENOSYS`. #[no_mangle] pub unsafe extern "C" fn symlink(target: *const u8, link: *const u8) -> i32 { - let link = c_str_to_bytes(link); - if !files::is_served(link) { - return refuse(crate::errcode::ENOSYS); + unsafe { symlinkat(target, AT_FDCWD, link) } +} + +#[no_mangle] +pub unsafe extern "C" fn symlinkat(target: *const u8, dirfd: i32, link: *const u8) -> i32 { + let (target, link) = (c_str_to_bytes(target), c_str_to_bytes(link)); + let at = match crate::atreq::at(dirfd, link) { + Ok(at) => at, + Err(e) => return refuse(e), + }; + match files::is_served_at(at, link) { + Ok(true) => files::symlink(target, (at, link)).map_or_else(refuse_files, |()| 0), + Ok(false) => refuse(crate::errcode::ENOSYS), + Err(e) => refuse_files(e), } - files::symlink(c_str_to_bytes(target), link).map_or_else(refuse_files, |()| 0) } struct TheServers; @@ -463,9 +544,11 @@ impl crate::resolve::Served for TheServers { } fn name(&mut self, at: &[u8]) -> Result { - let st = files::stat(at, false).map_err(crate::errcode::of_files)?; + let st = files::stat(At::Cwd, at, false).map_err(crate::errcode::of_files)?; match st.file_type { - syscall::FileType::Symlink => files::read_link(at).map(crate::resolve::Name::Link).map_err(crate::errcode::of_files), + syscall::FileType::Symlink => { + files::read_link(At::Cwd, at).map(crate::resolve::Name::Link).map_err(crate::errcode::of_files) + } _ => Ok(crate::resolve::Name::Other), } } @@ -550,7 +633,7 @@ pub unsafe extern "C" fn umask(mask: u32) -> u32 { /// Whether `path` names a file, and not a directory. pub(crate) fn is_file(path: &[u8]) -> bool { - files::stat(path, true).is_ok_and(|st| st.file_type == syscall::FileType::File) + files::stat(At::Cwd, path, true).is_ok_and(|st| st.file_type == syscall::FileType::File) } /// `arg` is the register C's variadic third argument arrives in (`fdreq`). @@ -569,17 +652,14 @@ pub unsafe extern "C" fn fcntl(raw_fd: i32, cmd: i32, arg: u64) -> i32 { // Directory operations -/// An open directory: its whole listing, and where the next `readdir` starts. +/// An open directory: its descriptor, the batch of its listing read last, and +/// where the next batch starts. pub struct DIR { - listing: Listing, + fd: i32, + entries: Vec<(u64, u8, Box<[u8]>)>, at: usize, -} - -/// A listing as its server answered it: the kernel's, or a file server's -/// entries, each its node, `d_type` and name. -enum Listing { - Kernel(Vec), - Served(Vec<(u64, u8, Box<[u8]>)>), + /// `None` once the last batch was read. + cursor: Option, } #[repr(C)] @@ -589,7 +669,6 @@ pub struct dirent { pub d_name: [u8; crate::listing::D_NAME], } -/// `d_type`s: the listing tells a directory from everything else, and no more. const DT_UNKNOWN: u8 = 0; const DT_DIR: u8 = 4; const DT_REG: u8 = 8; @@ -597,24 +676,48 @@ const DT_LNK: u8 = 10; #[no_mangle] pub unsafe extern "C" fn opendir(path: *const u8) -> *mut DIR { - let path = c_str_to_bytes(path); - let listing = match files::is_served(path) { - false => crate::listing::whole(|buf| syscall::readdir(path, buf)).map(Listing::Kernel).map_err(crate::errcode::of), - true => served_listing(path), - }; - match listing { - Ok(listing) => Box::into_raw(Box::new(DIR { listing, at: 0 })), + let fd = unsafe { openat(AT_FDCWD, path, O_RDONLY | crate::atreq::O_DIRECTORY | crate::atreq::O_CLOEXEC, 0) }; + if fd < 0 { + return ptr::null_mut(); + } + let dir = unsafe { fdopendir(fd) }; + if dir.is_null() { + let _ = toyos::fd::close(fd); + } + dir +} + +/// The directory `fd` names, read through it from here on, and closed with +/// it. +#[no_mangle] +pub unsafe extern "C" fn fdopendir(fd: i32) -> *mut DIR { + match files::fstat(fd) { + Ok(st) if st.file_type == syscall::FileType::Directory => { + Box::into_raw(Box::new(DIR { fd, entries: Vec::new(), at: 0, cursor: Some(0) })) + } + Ok(_) => { + crate::errno::set(crate::errcode::ENOTDIR); + ptr::null_mut() + } Err(e) => { - crate::errno::set(e); + refuse_files(e); ptr::null_mut() } } } -/// A file server's listing of `path`, each entry's node and `d_type` beside +#[no_mangle] +pub unsafe extern "C" fn dirfd(dir: *mut DIR) -> i32 { + match unsafe { dir.as_ref() } { + Some(dir) => dir.fd, + None => refuse(EINVAL), + } +} + +/// The next batch of `dir`'s listing, each entry's node and `d_type` beside /// its name. -fn served_listing(path: &[u8]) -> Result { - let listing = files::read_dir(path).map_err(crate::errcode::of_files)?; +fn next_batch(dir: &mut DIR, cursor: u64) -> Result<(), i32> { + let (listing, next) = files::read_dir(dir.fd, cursor).map_err(crate::errcode::of_files)?; let mut entries = Vec::new(); toyos::fs::for_each_entry(&listing, |entry| { let d_type = match entry.kind { @@ -626,24 +729,25 @@ fn served_listing(path: &[u8]) -> Result { entries.push((entry.node, d_type, Box::from(entry.name.as_bytes()))); }) .map_err(|e| crate::errcode::of_files(files::Error::Served(e)))?; - Ok(Listing::Served(entries)) + (dir.entries, dir.at, dir.cursor) = (entries, 0, next); + Ok(()) } +/// The end of the listing is null with `errno` left as it was; a refused +/// batch is null with `errno` set. #[no_mangle] pub unsafe extern "C" fn readdir(dir: *mut DIR) -> *mut dirent { - if dir.is_null() { return ptr::null_mut(); } - let d = &mut *dir; - let (d_ino, d_type, name) = match &d.listing { - Listing::Kernel(listing) => { - let Some(entry) = syscall::dirent(listing, &mut d.at) else { return ptr::null_mut() }; - (entry.node, if entry.is_dir { DT_DIR } else { DT_UNKNOWN }, entry.name) - } - Listing::Served(entries) => { - let Some((node, d_type, name)) = entries.get(d.at) else { return ptr::null_mut() }; - d.at += 1; - (*node, *d_type, &name[..]) + let Some(d) = (unsafe { dir.as_mut() }) else { return ptr::null_mut() }; + while d.at == d.entries.len() { + let Some(cursor) = d.cursor else { return ptr::null_mut() }; + if let Err(e) = next_batch(d, cursor) { + d.cursor = None; + crate::errno::set(e); + return ptr::null_mut(); } - }; + } + let (d_ino, d_type, name) = &d.entries[d.at]; + d.at += 1; let Some(d_name) = crate::listing::d_name(name) else { crate::errno::set(crate::errcode::EOVERFLOW); return ptr::null_mut(); @@ -652,16 +756,74 @@ pub unsafe extern "C" fn readdir(dir: *mut DIR) -> *mut dirent { // Use a static buffer for the dirent (not thread-safe, matching POSIX convention) static mut DIRENT_BUF: dirent = dirent { d_ino: 0, d_type: 0, d_name: [0; crate::listing::D_NAME] }; let ent = &raw mut DIRENT_BUF; - ent.write(dirent { d_ino, d_type, d_name }); + unsafe { ent.write(dirent { d_ino: *d_ino, d_type: *d_type, d_name }) }; ent } #[no_mangle] pub unsafe extern "C" fn closedir(dir: *mut DIR) -> i32 { if dir.is_null() { return -1; } - // SAFETY: a `DIR` `opendir` answered, closed once. - drop(unsafe { Box::from_raw(dir) }); - 0 + // SAFETY: a `DIR` `opendir` or `fdopendir` answered, closed once. + let dir = unsafe { Box::from_raw(dir) }; + toyos::fd::close(dir.fd).map_or_else(|e| refuse(fd_errno(e)), |()| 0) +} + +// Positions and times + +/// Never moves the descriptor's offset. +#[no_mangle] +pub unsafe extern "C" fn pread(raw_fd: i32, buf: *mut u8, count: usize, offset: i64) -> isize { + let Ok(offset) = u64::try_from(offset) else { return refuse(EINVAL) as isize }; + if count == 0 { return 0; } + // SAFETY: C's caller hands `count` writable bytes at `buf`. + let slice = unsafe { core::slice::from_raw_parts_mut(buf, count) }; + files::pread(raw_fd, slice, offset).map_or_else(|e| refuse_files(e) as isize, |n| n as isize) +} + +/// Never moves the descriptor's offset, and writes at `offset` whether or +/// not it appends, as POSIX has it. +#[no_mangle] +pub unsafe extern "C" fn pwrite(raw_fd: i32, buf: *const u8, count: usize, offset: i64) -> isize { + let Ok(offset) = u64::try_from(offset) else { return refuse(EINVAL) as isize }; + if count == 0 { return 0; } + // SAFETY: C's caller hands `count` bytes at `buf`. + let slice = unsafe { core::slice::from_raw_parts(buf, count) }; + files::pwrite(raw_fd, slice, offset).map_or_else(|e| refuse_files(e) as isize, |n| n as isize) +} + +/// The two `(tv_sec, tv_nsec)` or `(tv_sec, tv_usec)` pairs at `times`, +/// none where it is null. +/// +/// # Safety +/// `times` is null or names two `{long, long}` pairs. +unsafe fn pairs(times: *const [i64; 2]) -> Option<[(i64, i64); 2]> { + // SAFETY: the caller's. + unsafe { times.as_ref().map(|_| [((*times)[0], (*times)[1]), ((*times.add(1))[0], (*times.add(1))[1])]) } +} + +#[no_mangle] +pub unsafe extern "C" fn utimensat(dirfd: i32, path: *const u8, times: *const [i64; 2], flags: i32) -> i32 { + let change = match crate::atreq::only(flags, AT_SYMLINK_NOFOLLOW).and_then(|_| crate::atreq::times(unsafe { pairs(times) })) { + Ok(change) => change, + Err(e) => return refuse(e), + }; + on_at(dirfd, path, |at, path| files::set_times(at, path, flags & AT_SYMLINK_NOFOLLOW == 0, &change)) +} + +#[no_mangle] +pub unsafe extern "C" fn futimens(fd: i32, times: *const [i64; 2]) -> i32 { + match crate::atreq::times(unsafe { pairs(times) }) { + Ok(change) => files::set_times(At::Fd(fd), b"", false, &change).map_or_else(refuse_files, |()| 0), + Err(e) => refuse(e), + } +} + +#[no_mangle] +pub unsafe extern "C" fn utimes(path: *const u8, times: *const [i64; 2]) -> i32 { + match crate::atreq::utimes(unsafe { pairs(times) }) { + Ok(change) => on_at(AT_FDCWD, path, |at, path| files::set_times(at, path, true, &change)), + Err(e) => refuse(e), + } } // struct stat diff --git a/sdk/libc/src/refused.rs b/sdk/libc/src/refused.rs index 50571ed05bb..c2708f9e889 100644 --- a/sdk/libc/src/refused.rs +++ b/sdk/libc/src/refused.rs @@ -41,12 +41,6 @@ pub unsafe extern "C" fn gethostname(_name: *mut u8, _len: usize) -> i32 { refuse() } -/// No filesystem ToyOS mounts keeps a second name for a file. -#[no_mangle] -pub unsafe extern "C" fn link(_existing: *const u8, _new: *const u8) -> i32 { - refuse() -} - /// A file has no owner. #[no_mangle] pub extern "C" fn fchown(_fd: i32, _owner: u32, _group: u32) -> i32 { @@ -65,9 +59,9 @@ pub extern "C" fn fchmod(_fd: i32, _mode: u32) -> i32 { refuse() } -/// No call sets a file's times. +/// As [`chmod`]. #[no_mangle] -pub unsafe extern "C" fn utimes(_path: *const u8, _times: *const u8) -> i32 { +pub unsafe extern "C" fn fchmodat(_dirfd: i32, _path: *const u8, _mode: u32, _flags: i32) -> i32 { refuse() } diff --git a/sdk/libc/src/stdio.rs b/sdk/libc/src/stdio.rs index dce990d2c3f..29a2d29516f 100644 --- a/sdk/libc/src/stdio.rs +++ b/sdk/libc/src/stdio.rs @@ -616,12 +616,12 @@ pub(crate) unsafe fn unget(f: *mut FILE, bytes: &[u8]) -> bool { #[no_mangle] pub unsafe extern "C" fn remove(path: *const u8) -> i32 { - answered(files::unlink(unsafe { c_str_bytes(path) })) + answered(files::unlink(files::At::Cwd, unsafe { c_str_bytes(path) }, false)) } #[no_mangle] pub unsafe extern "C" fn rename(old: *const u8, new: *const u8) -> i32 { - answered(files::rename(unsafe { c_str_bytes(old) }, unsafe { c_str_bytes(new) })) + answered(files::rename((files::At::Cwd, unsafe { c_str_bytes(old) }), (files::At::Cwd, unsafe { c_str_bytes(new) }))) } #[no_mangle] @@ -648,7 +648,7 @@ pub unsafe extern "C" fn atof(s: *const u8) -> f64 { #[no_mangle] pub unsafe extern "C" fn mkdir(path: *const u8, _mode: u32) -> i32 { - answered(files::mkdir(unsafe { c_str_bytes(path) })) + answered(files::mkdir(files::At::Cwd, unsafe { c_str_bytes(path) })) } #[no_mangle] diff --git a/sdk/libc/src/uname.rs b/sdk/libc/src/uname.rs index 5c5062317b4..059dbc8bc30 100644 --- a/sdk/libc/src/uname.rs +++ b/sdk/libc/src/uname.rs @@ -71,7 +71,7 @@ impl Write for Field<'_> { /// As much of the file at `path` as `into` holds. fn read(path: &str, into: &mut [u8]) -> Result { - let fd = files::open(path.as_bytes(), Open { read: true, ..Open::default() })?; + let fd = files::open(files::At::Cwd, path.as_bytes(), Open { read: true, ..Open::default() })?; let mut len = 0; let read = loop { match files::read(fd, &mut into[len..]) { diff --git a/sdk/std/os/fs.rs b/sdk/std/os/fs.rs index 744be69744b..8aa8dbf4681 100644 --- a/sdk/std/os/fs.rs +++ b/sdk/std/os/fs.rs @@ -1,4 +1,4 @@ -use crate::fs::Metadata; +use crate::fs::{File, Metadata}; use crate::io; use crate::path::Path; use crate::sys::AsInner; @@ -15,6 +15,9 @@ pub trait MetadataExt { /// that reaches the file and no other existing file's. #[stable(feature = "toyos_ext", since = "1.0.0")] fn ino(&self) -> u64; + /// The names the file has, as Unix's `nlink`. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn nlink(&self) -> u64; } #[stable(feature = "toyos_ext", since = "1.0.0")] @@ -25,6 +28,32 @@ impl MetadataExt for Metadata { fn ino(&self) -> u64 { self.as_inner().node() } + fn nlink(&self) -> u64 { + self.as_inner().nlink() + } +} + +/// Reads and writes at an offset, which leave the file's own where it is, as +/// Unix's `FileExt` does. +#[stable(feature = "toyos_ext", since = "1.0.0")] +pub trait FileExt { + /// Read at `offset`: how many bytes, 0 at or past the file's end. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result; + /// Write at `offset`, whether or not the file was opened to append: + /// how many bytes. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn write_at(&self, buf: &[u8], offset: u64) -> io::Result; +} + +#[stable(feature = "toyos_ext", since = "1.0.0")] +impl FileExt for File { + fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result { + self.as_inner().read_at(buf, offset) + } + fn write_at(&self, buf: &[u8], offset: u64) -> io::Result { + self.as_inner().write_at(buf, offset) + } } /// Creates a new symbolic link on the filesystem. diff --git a/sdk/std/sys/fs.rs b/sdk/std/sys/fs.rs index ae9a8989144..cd419554bd6 100644 --- a/sdk/std/sys/fs.rs +++ b/sdk/std/sys/fs.rs @@ -3,7 +3,7 @@ //! `toyos::files` routes it, follows its links and keeps what it opened. A //! `File` is one descriptor of the process's table either way. -use toyos::files::{self, Open}; +use toyos::files::{self, At, Open}; use toyos_abi::syscall::{self, SeekFrom as AbiSeek}; use crate::ffi::OsString; @@ -23,15 +23,22 @@ pub struct File(i32); pub struct FileAttr { size: u64, file_type: FileType, + nlink: u64, + atime: u64, mtime: u64, /// `toyos_abi::syscall::Stat`'s identity. volume: u64, node: u64, } +/// A directory's listing, asked of its open descriptor a batch at a time. pub struct ReadDir { - entries: Vec, + dir: File, + dir_path: PathBuf, + batch: Vec, index: usize, + /// Where the next batch starts, `None` once the last was read. + cursor: Option, } pub struct DirEntry { @@ -51,7 +58,10 @@ pub struct OpenOptions { } #[derive(Copy, Clone, Debug, Default)] -pub struct FileTimes {} +pub struct FileTimes { + accessed: Option, + modified: Option, +} #[derive(Clone, PartialEq, Eq, Debug)] pub struct FilePermissions { @@ -91,16 +101,22 @@ impl FileType { } impl FileAttr { - fn of_kernel(stat: syscall::Stat) -> FileAttr { + fn of(stat: files::Stat) -> FileAttr { FileAttr { size: stat.size, file_type: kernel_file_type(stat.file_type), + nlink: stat.nlink, + atime: stat.atime, mtime: stat.mtime, volume: stat.volume, node: stat.node, } } + pub fn nlink(&self) -> u64 { + self.nlink + } + pub fn volume(&self) -> u64 { self.volume } @@ -129,7 +145,10 @@ impl FileAttr { } pub fn accessed(&self) -> io::Result { - Err(io::Error::new(io::ErrorKind::Unsupported, "ToyOS does not track access time")) + match self.atime { + 0 => Err(io::const_error!(io::ErrorKind::Unsupported, "the volume keeps no access time")), + nanos => Ok(SystemTime::from_nanos(nanos)), + } } pub fn created(&self) -> io::Result { @@ -148,8 +167,25 @@ impl FilePermissions { } impl FileTimes { - pub fn set_accessed(&mut self, _t: SystemTime) {} - pub fn set_modified(&mut self, _t: SystemTime) {} + pub fn set_accessed(&mut self, t: SystemTime) { + self.accessed = Some(t); + } + pub fn set_modified(&mut self, t: SystemTime) { + self.modified = Some(t); + } + + /// The change `toyos::files` makes: a time past what a file keeps is + /// refused. + fn change(&self) -> io::Result { + let time = |t: Option| match t { + None => Ok(None), + Some(t) => t.nanos().map(|n| Some(files::Time::At(n))).ok_or(io::const_error!( + io::ErrorKind::InvalidInput, + "a time past what a file keeps", + )), + }; + Ok(files::Change { size: None, atime: time(self.accessed)?, mtime: time(self.modified)? }) + } } impl fmt::Debug for ReadDir { @@ -162,7 +198,27 @@ impl Iterator for ReadDir { type Item = io::Result; fn next(&mut self) -> Option> { - let e = self.entries.get(self.index)?; + while self.index == self.batch.len() { + let cursor = self.cursor?; + let (listing, next) = match files::read_dir(self.dir.0, cursor) { + Ok(read) => read, + Err(e) => { + self.cursor = None; + return Some(Err(files_error(e))); + } + }; + let (mut batch, dir_path) = (Vec::new(), &self.dir_path); + let decoded = toyos::fs::for_each_entry(&listing, |entry| { + let (name, file_type) = (OsString::from(entry.name), FileType::of_kind(entry.kind)); + batch.push(DirEntry { dir_path: dir_path.clone(), name, file_type }) + }); + if let Err(e) = decoded { + self.cursor = None; + return Some(Err(served_error(e))); + } + (self.batch, self.index, self.cursor) = (batch, 0, next); + } + let e = &self.batch[self.index]; self.index += 1; Some(Ok(DirEntry { dir_path: e.dir_path.clone(), name: e.name.clone(), file_type: e.file_type })) } @@ -182,8 +238,13 @@ impl DirEntry { lstat(&self.path()) } + /// What the listing said, or where it said only that the entry is no + /// directory, as the kernel's does, what the entry is asked. pub fn file_type(&self) -> io::Result { - Ok(self.file_type) + match self.file_type { + FileType { is_file: false, is_dir: false, is_symlink: false } => lstat(&self.path()).map(|a| a.file_type), + known => Ok(known), + } } } @@ -228,6 +289,8 @@ impl OpenOptions { create: self.create, create_new: self.create_new, truncate: self.truncate, + directory: false, + unnamed: false, cloexec: true, } } @@ -278,11 +341,11 @@ impl File { } pub fn open(path: &Path, opts: &OpenOptions) -> io::Result { - files::open(bytes(path), opts.how()).map(File).map_err(files_error) + files::open(At::Cwd, bytes(path), opts.how()).map(File).map_err(files_error) } pub fn file_attr(&self) -> io::Result { - files::fstat(self.0).map(FileAttr::of_kernel).map_err(files_error) + files::fstat(self.0).map(FileAttr::of).map_err(files_error) } pub fn fsync(&self) -> io::Result<()> { @@ -335,6 +398,17 @@ impl File { files::write(self.0, buf).map_err(files_error) } + /// Read at `offset`, leaving the file's offset where it is. + pub fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result { + files::pread(self.0, buf, offset).map_err(files_error) + } + + /// Write at `offset`, leaving the file's offset where it is, whether or + /// not it appends. + pub fn write_at(&self, buf: &[u8], offset: u64) -> io::Result { + files::pwrite(self.0, buf, offset).map_err(files_error) + } + pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result { crate::io::default_write_vectored(|b| self.write(b), bufs) } @@ -373,8 +447,8 @@ impl File { Ok(()) } - pub fn set_times(&self, _times: FileTimes) -> io::Result<()> { - Ok(()) + pub fn set_times(&self, times: FileTimes) -> io::Result<()> { + files::set_times(At::Fd(self.0), b"", false, ×.change()?).map_err(files_error) } } @@ -390,7 +464,7 @@ impl DirBuilder { } pub fn mkdir(&self, p: &Path) -> io::Result<()> { - files::mkdir(bytes(p)).map_err(files_error) + files::mkdir(At::Cwd, bytes(p)).map_err(files_error) } } @@ -410,22 +484,17 @@ fn kernel_file_type(ty: syscall::FileType) -> FileType { } pub fn readdir(p: &Path) -> io::Result { - let listing = files::read_dir(bytes(p)).map_err(files_error)?; - let mut entries = Vec::new(); - toyos::fs::for_each_entry(&listing, |entry| { - let (name, file_type) = (OsString::from(entry.name), FileType::of_kind(entry.kind)); - entries.push(DirEntry { dir_path: p.to_path_buf(), name, file_type }) - }) - .map_err(served_error)?; - Ok(ReadDir { entries, index: 0 }) + let how = Open { read: true, directory: true, cloexec: true, ..Open::default() }; + let dir = files::open(At::Cwd, bytes(p), how).map(File).map_err(files_error)?; + Ok(ReadDir { dir, dir_path: p.to_path_buf(), batch: Vec::new(), index: 0, cursor: Some(0) }) } pub fn unlink(p: &Path) -> io::Result<()> { - files::unlink(bytes(p)).map_err(files_error) + files::unlink(At::Cwd, bytes(p), false).map_err(files_error) } pub fn rename(old: &Path, new: &Path) -> io::Result<()> { - files::rename(bytes(old), bytes(new)).map_err(files_error) + files::rename((At::Cwd, bytes(old)), (At::Cwd, bytes(new))).map_err(files_error) } pub fn set_perm(_p: &Path, _perm: FilePermissions) -> io::Result<()> { @@ -437,7 +506,7 @@ pub fn set_perm_nofollow(_p: &Path, _perm: FilePermissions) -> io::Result<()> { } pub fn rmdir(p: &Path) -> io::Result<()> { - files::rmdir(bytes(p)).map_err(files_error) + files::unlink(At::Cwd, bytes(p), true).map_err(files_error) } pub fn remove_dir_all(path: &Path) -> io::Result<()> { @@ -462,25 +531,27 @@ pub fn exists(path: &Path) -> io::Result { } pub fn readlink(p: &Path) -> io::Result { - let target = files::read_link(bytes(p)).map_err(files_error)?; + let target = files::read_link(At::Cwd, bytes(p)).map_err(files_error)?; // SAFETY: a link's target is the bytes a path was written as. Ok(PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(target) })) } pub fn symlink(original: &Path, link: &Path) -> io::Result<()> { - files::symlink(bytes(original), bytes(link)).map_err(files_error) + files::symlink(bytes(original), (At::Cwd, bytes(link))).map_err(files_error) } -pub fn link(_src: &Path, _dst: &Path) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Unsupported, "no hard links on ToyOS")) +/// `dst` a second name of what `src` names, a symlink itself and not what it +/// leads to. +pub fn link(src: &Path, dst: &Path) -> io::Result<()> { + files::link((At::Cwd, bytes(src)), false, (At::Cwd, bytes(dst))).map_err(files_error) } pub fn stat(path: &Path) -> io::Result { - files::stat(bytes(path), true).map(FileAttr::of_kernel).map_err(files_error) + files::stat(At::Cwd, bytes(path), true).map(FileAttr::of).map_err(files_error) } pub fn lstat(path: &Path) -> io::Result { - files::stat(bytes(path), false).map(FileAttr::of_kernel).map_err(files_error) + files::stat(At::Cwd, bytes(path), false).map(FileAttr::of).map_err(files_error) } pub fn canonicalize(p: &Path) -> io::Result { @@ -512,12 +583,12 @@ pub fn copy(from: &Path, to: &Path) -> io::Result { Ok(total) } -pub fn set_times(_p: &Path, _times: FileTimes) -> io::Result<()> { - Ok(()) +pub fn set_times(p: &Path, times: FileTimes) -> io::Result<()> { + files::set_times(At::Cwd, bytes(p), true, ×.change()?).map_err(files_error) } -pub fn set_times_nofollow(_p: &Path, _times: FileTimes) -> io::Result<()> { - Ok(()) +pub fn set_times_nofollow(p: &Path, times: FileTimes) -> io::Result<()> { + files::set_times(At::Cwd, bytes(p), false, ×.change()?).map_err(files_error) } /// The whole of the file at `path` as a program image: what a spawn of a diff --git a/sdk/std/sys/pal/mod.rs b/sdk/std/sys/pal/mod.rs index 1f26646d173..42022f3e926 100644 --- a/sdk/std/sys/pal/mod.rs +++ b/sdk/std/sys/pal/mod.rs @@ -90,14 +90,16 @@ pub fn served_error(e: toyos::fs::Error) -> crate::io::Error { Error::Io => io::const_error!(Kind::Other, "the volume's device failed, or its structures do not decode"), Error::Stale => io::const_error!( Kind::StaleNetworkFileHandle, - "the file was removed, or its file server restarted, since it was opened", + "the file was removed or replaced, or its file server restarted, since it was opened", ), Error::UnknownFile => io::const_error!(Kind::InvalidInput, "the file server holds no such open file"), Error::InvalidPath => io::const_error!(Kind::InvalidInput, "the path is not one the file server takes"), Error::InvalidRequest => io::const_error!(Kind::InvalidInput, "the file server takes no such request"), Error::Unsupported => io::const_error!(Kind::Unsupported, "the volume's format has no such thing"), Error::OutOfResources => io::const_error!(Kind::OutOfMemory, "the kernel had no room for the request"), - Error::NoDirectory => io::const_error!(Kind::NotADirectory, "the connection was given files and no directory"), + Error::NoDirectory => { + io::const_error!(Kind::NotADirectory, "the connection was given open files and no directory") + } Error::Gone => io::const_error!(Kind::StaleNetworkFileHandle, "the file server is gone"), Error::NotHeld => io::const_error!(Kind::NotFound, "this process holds no such directory"), Error::Malformed => io::const_error!(Kind::InvalidData, "the file server answered outside its protocol"), @@ -118,6 +120,9 @@ pub fn files_error(e: toyos::files::Error) -> crate::io::Error { io::const_error!(io::ErrorKind::CrossesDevices, "the two paths are on different servers") } files::Error::NotAFile => io::const_error!(io::ErrorKind::InvalidInput, "the descriptor names no file"), + files::Error::NotADirectory => { + io::const_error!(io::ErrorKind::NotADirectory, "the descriptor names no directory") + } files::Error::Stream(refused) => crate::sys::net::stream_refused(refused), } } diff --git a/sdk/std/sys/time.rs b/sdk/std/sys/time.rs index b8f0041b87f..df99c290521 100644 --- a/sdk/std/sys/time.rs +++ b/sdk/std/sys/time.rs @@ -55,4 +55,10 @@ impl SystemTime { pub(crate) fn from_nanos(nanos: u64) -> SystemTime { SystemTime(Duration::from_nanos(nanos)) } + + /// Nanoseconds since the Unix epoch, as a file's time is kept: `None` + /// past what a `u64` counts, the year 2554. + pub(crate) fn nanos(&self) -> Option { + u64::try_from(self.0.as_nanos()).ok() + } } diff --git a/sdk/toyos/src/fd.rs b/sdk/toyos/src/fd.rs index 35c3dde19fa..850ad2218df 100644 --- a/sdk/toyos/src/fd.rs +++ b/sdk/toyos/src/fd.rs @@ -49,12 +49,15 @@ pub enum Error { /// The tag of a handle and of an object another library keeps. pub const UNTAGGED: u32 = 0; -/// The tag of a file a file server holds open (`crate::files`). +/// The tag of a file or directory a file server holds open (`crate::files`). pub const SERVED_FILE: u32 = 1; /// The tag of a stream's descriptor (`crate::net::stream`): a kernel handle, /// let go by the copy of this crate that holds the stream's end, which only /// that copy knows. pub const STREAM: u32 = 2; +/// The tag of a directory the kernel serves, which has no handle +/// (`crate::files`). +pub const KERNEL_DIR: u32 = 3; /// What a descriptor names. #[repr(C)] diff --git a/sdk/toyos/src/files.rs b/sdk/toyos/src/files.rs index d1967952498..d7b3f0a91bd 100644 --- a/sdk/toyos/src/files.rs +++ b/sdk/toyos/src/files.rs @@ -4,23 +4,33 @@ //! served by a file server ([`crate::fs`]). A program or a library is read //! here into an image the kernel pages it from ([`read_image`]). //! -//! **A path goes to the capability with the longest matching prefix**, at -//! most [`MAX_CAPABILITY_DEPTH`] components deep, relative to it; a path under -//! no capability the process holds is the kernel's. An absolute link a server -//! meets is followed here, in this process's own table, at most [`MAX_LINKS`] -//! times. A file server that restarts is survived: its capability is connected -//! again ([`crate::fs::Dir`]). +//! **An absolute path, or one from the working directory, goes to the +//! capability with the longest matching prefix**, at most +//! [`MAX_CAPABILITY_DEPTH`] components deep, relative to it, made absolute and +//! its `.` and `..` taken lexically; a path under no capability the process +//! holds is the kernel's. **A relative path from a descriptor ([`At::Fd`]) +//! goes to whoever serves that directory**: its server resolves the path, +//! `..` by the directory itself. An absolute link a server meets is followed +//! here, in this process's own table, at most [`MAX_LINKS`] times. A file +//! server that restarts is survived: its capability is connected again +//! ([`crate::fs::Dir`]), and what a descriptor held on it answers `Stale`. +//! +//! **A directory the kernel serves has no handle**: a descriptor of one names +//! its absolute path, and a path from it is that path's lexically +//! ([`crate::fd::KERNEL_DIR`]). The kernel keeps no hard link, unnamed file or +//! times a caller sets, and refuses each `NotSupported`. //! //! **One copy of this crate keeps this state**, the copy that owns the //! descriptor table ([`crate::fd`]), and every copy reaches it through //! `toyos_fs_*`: std and libc see one set of connections and one table. A -//! file a server holds open is a descriptor naming an object of this module's -//! ([`crate::fd::SERVED_FILE`]); a kernel file is one naming its handle. +//! file or directory a server holds open is a descriptor naming an object of +//! this module's ([`crate::fd::SERVED_FILE`]); a kernel file is one naming its +//! handle. use alloc::string::String; use alloc::vec::Vec; -use toyos_abi::syscall::{SeekFrom, Stat, SyscallError}; +use toyos_abi::syscall::{FileType, SeekFrom, SyscallError}; use crate::fd::{self, Fd}; use crate::fs; @@ -43,10 +53,83 @@ pub struct Open { pub create: bool, pub create_new: bool, pub truncate: bool, + /// Refused unless the path names a directory. + pub directory: bool, + /// A file with no name in the directory the path names, which only + /// [`link`] ever names: POSIX's `O_TMPFILE`. + pub unnamed: bool, /// The descriptor is not inherited (`toyos::fd`'s flag). pub cloexec: bool, } +/// Where a path is resolved from: the working directory, or a directory a +/// descriptor names. An absolute path is resolved from neither, and the empty +/// path from a descriptor is what it names itself. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum At { + Cwd, + Fd(Fd), +} + +impl At { + /// The C interface's word: a descriptor, or [`AT_CWD`]. + const fn word(self) -> Fd { + match self { + At::Cwd => AT_CWD, + At::Fd(fd) => fd, + } + } + + #[cfg_attr(not(all(feature = "fd-table", not(test))), allow(dead_code))] + const fn of(word: Fd) -> Self { + match word { + AT_CWD => At::Cwd, + fd => At::Fd(fd), + } + } +} + +/// [`At::Cwd`] across the C interface; no descriptor is negative. +const AT_CWD: Fd = -1; + +/// What a file is, whoever serves it. A time is nanoseconds since the Unix +/// epoch, 0 where its volume keeps none; `volume` and `node` mean what +/// `toyos_abi::syscall::Stat`'s do. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Stat { + pub file_type: FileType, + pub size: u64, + /// The names it has. + pub nlink: u64, + pub atime: u64, + pub mtime: u64, + pub ctime: u64, + pub volume: u64, + pub node: u64, +} + +impl Stat { + /// What the kernel answers about a file or a handle of its own: one name, + /// and of the times only when it was last written. + pub fn of_kernel(stat: toyos_abi::syscall::Stat) -> Self { + Self { + file_type: stat.file_type, + size: stat.size, + nlink: 1, + atime: 0, + mtime: stat.mtime, + ctime: 0, + volume: stat.volume, + node: stat.node, + } + } + + fn empty() -> Self { + Self::of_kernel(toyos_abi::syscall::Stat::default()) + } +} + /// Why a call was not answered. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Error { @@ -60,13 +143,16 @@ pub enum Error { NotUtf8, /// More absolute links on the way than are followed. Loop, - /// A rename between two servers, or between a server and the kernel. + /// A rename or a link between two servers, or between a server and the + /// kernel. CrossesServers, /// A descriptor that names no file, but an object of a library's own. NotAFile, /// A read or a write of a stream's descriptor, refused as the stream /// refuses it (`crate::net::stream`). Stream(crate::net::stream::Refused), + /// A path resolved from a descriptor that names no directory. + NotADirectory, } // The C interface's words: a negative answer is an `Error`. @@ -76,6 +162,7 @@ const NOT_UTF8: i64 = -3; const LOOP: i64 = -4; const CROSSES: i64 = -5; const NOT_A_FILE: i64 = -6; +const NOT_A_DIRECTORY: i64 = -7; const KERNEL: i64 = 0x100; const SERVED: i64 = 0x200; const STREAM: i64 = 0x300; @@ -89,6 +176,7 @@ fn encode(e: Error) -> i64 { Error::Loop => LOOP, Error::CrossesServers => CROSSES, Error::NotAFile => NOT_A_FILE, + Error::NotADirectory => NOT_A_DIRECTORY, Error::Kernel(e) => -(KERNEL + (u64::MAX - e.to_u64()) as i64), Error::Served(e) => -(SERVED + i64::from(e.byte())), Error::Stream(refused) => match crate::net::stream::Refused::byte(refused) { @@ -109,6 +197,7 @@ fn decode(word: i64) -> Result { LOOP => Err(Error::Loop), CROSSES => Err(Error::CrossesServers), NOT_A_FILE => Err(Error::NotAFile), + NOT_A_DIRECTORY => Err(Error::NotADirectory), _ => { let code = -word; let kernel = (KERNEL..SERVED).contains(&code).then(|| SyscallError::from_u64(u64::MAX - (code - KERNEL) as u64)).flatten(); @@ -131,44 +220,49 @@ const SEEK_START: u32 = 0; const SEEK_CURRENT: u32 = 1; const SEEK_END: u32 = 2; +/// What [`set_times`] sets: the wire's own words (`crate::fs::Change`). +pub use crate::fs::{Change, Time}; + extern "C" { - fn toyos_fs_open(path: *const u8, len: usize, how: Open) -> i64; - fn toyos_fs_stat(path: *const u8, len: usize, follow: bool, out: *mut Stat) -> i64; - fn toyos_fs_fstat(fd: Fd, out: *mut Stat) -> i64; + fn toyos_fs_open(at: Fd, path: *const u8, len: usize, how: Open) -> i64; + fn toyos_fs_stat(at: Fd, path: *const u8, len: usize, follow: bool, out: *mut Stat) -> i64; fn toyos_fs_read(fd: Fd, buf: *mut u8, len: usize) -> i64; fn toyos_fs_write(fd: Fd, buf: *const u8, len: usize) -> i64; + fn toyos_fs_pread(fd: Fd, buf: *mut u8, len: usize, offset: u64) -> i64; + fn toyos_fs_pwrite(fd: Fd, buf: *const u8, len: usize, offset: u64) -> i64; fn toyos_fs_seek(fd: Fd, whence: u32, delta: i64) -> i64; fn toyos_fs_truncate(fd: Fd, size: u64) -> i64; fn toyos_fs_fsync(fd: Fd) -> i64; fn toyos_fs_stream(fd: Fd) -> i64; - fn toyos_fs_mkdir(path: *const u8, len: usize) -> i64; - fn toyos_fs_rmdir(path: *const u8, len: usize) -> i64; - fn toyos_fs_unlink(path: *const u8, len: usize) -> i64; - fn toyos_fs_rename(from: *const u8, from_len: usize, to: *const u8, to_len: usize) -> i64; - fn toyos_fs_symlink(target: *const u8, target_len: usize, link: *const u8, link_len: usize) -> i64; - fn toyos_fs_readlink(path: *const u8, len: usize, out: *mut u8, cap: usize) -> i64; - fn toyos_fs_readdir(path: *const u8, len: usize, out: *mut u8, cap: usize) -> i64; - fn toyos_fs_is_served(path: *const u8, len: usize) -> i64; + fn toyos_fs_mkdir(at: Fd, path: *const u8, len: usize) -> i64; + fn toyos_fs_unlink(at: Fd, path: *const u8, len: usize, dir: bool) -> i64; + fn toyos_fs_rename(from_at: Fd, from: *const u8, from_len: usize, to_at: Fd, to: *const u8, to_len: usize) -> i64; + fn toyos_fs_link(from_at: Fd, from: *const u8, from_len: usize, follow: bool, to_at: Fd, to: *const u8, to_len: usize) -> i64; + fn toyos_fs_symlink(target: *const u8, target_len: usize, at: Fd, link: *const u8, link_len: usize) -> i64; + fn toyos_fs_readlink(at: Fd, path: *const u8, len: usize, out: *mut u8, cap: usize) -> i64; + fn toyos_fs_set_times(at: Fd, path: *const u8, len: usize, follow: bool, flags: u64, atime: u64, mtime: u64) -> i64; + fn toyos_fs_readdir(fd: Fd, cursor: u64, out: *mut u8, cap: usize, next: *mut u64) -> i64; + fn toyos_fs_is_served(at: Fd, path: *const u8, len: usize) -> i64; fn toyos_fs_chdir(path: *const u8, len: usize) -> i64; } -/// Open `path`: a descriptor, the lowest free. -pub fn open(path: &[u8], how: Open) -> Result { +/// Open `path` from `at`: a descriptor, the lowest free. A directory opens to +/// read, and names a directory later paths are resolved from. +pub fn open(at: At, path: &[u8], how: Open) -> Result { // SAFETY: `path` is live for the call, and `how` is a plain value. - decode(unsafe { toyos_fs_open(path.as_ptr(), path.len(), how) }).map(|fd| fd as Fd) + decode(unsafe { toyos_fs_open(at.word(), path.as_ptr(), path.len(), how) }).map(|fd| fd as Fd) } -/// What `path` names: the link itself where `follow` is false. -pub fn stat(path: &[u8], follow: bool) -> Result { - let mut out = Stat::default(); +/// What `path` from `at` names: the link itself where `follow` is false. +pub fn stat(at: At, path: &[u8], follow: bool) -> Result { + let mut out = Stat::empty(); // SAFETY: both live for the call. - decode(unsafe { toyos_fs_stat(path.as_ptr(), path.len(), follow, &mut out) }).map(|_| out) + decode(unsafe { toyos_fs_stat(at.word(), path.as_ptr(), path.len(), follow, &mut out) }).map(|_| out) } +/// What the descriptor names. pub fn fstat(fd: Fd) -> Result { - let mut out = Stat::default(); - // SAFETY: `out` is live for the call. - decode(unsafe { toyos_fs_fstat(fd, &mut out) }).map(|_| out) + stat(At::Fd(fd), b"", false) } pub fn read(fd: Fd, buf: &mut [u8]) -> Result { @@ -181,6 +275,19 @@ pub fn write(fd: Fd, buf: &[u8]) -> Result { decode(unsafe { toyos_fs_write(fd, buf.as_ptr(), buf.len()) }).map(|n| n as usize) } +/// Read at `offset`, leaving the descriptor's offset where it is. +pub fn pread(fd: Fd, buf: &mut [u8], offset: u64) -> Result { + // SAFETY: `buf` is live and writable for the call. + decode(unsafe { toyos_fs_pread(fd, buf.as_mut_ptr(), buf.len(), offset) }).map(|n| n as usize) +} + +/// Write at `offset`, leaving the descriptor's offset where it is, whether or +/// not it appends. +pub fn pwrite(fd: Fd, buf: &[u8], offset: u64) -> Result { + // SAFETY: `buf` is live for the call. + decode(unsafe { toyos_fs_pwrite(fd, buf.as_ptr(), buf.len(), offset) }).map(|n| n as usize) +} + /// Move the descriptor's offset: where it landed. pub fn seek(fd: Fd, pos: SeekFrom) -> Result { let (whence, delta) = match pos { @@ -210,56 +317,80 @@ pub fn stream(fd: Fd) -> Result { decode(unsafe { toyos_fs_stream(fd) }).map(|h| OwnedHandle(toyos_abi::RawHandle(h as u32))) } -macro_rules! path_call { - ($($(#[$doc:meta])* $name:ident => $sys:ident;)+) => {$( - $(#[$doc])* - pub fn $name(path: &[u8]) -> Result<(), Error> { - // SAFETY: `path` is live for the call. - decode(unsafe { $sys(path.as_ptr(), path.len()) }).map(drop) - } - )+}; +pub fn mkdir(at: At, path: &[u8]) -> Result<(), Error> { + // SAFETY: `path` is live for the call. + decode(unsafe { toyos_fs_mkdir(at.word(), path.as_ptr(), path.len()) }).map(drop) +} + +/// Remove the name `path`: a directory's, which must be empty, only where +/// `dir`, and then nothing else. +pub fn unlink(at: At, path: &[u8], dir: bool) -> Result<(), Error> { + // SAFETY: `path` is live for the call. + decode(unsafe { toyos_fs_unlink(at.word(), path.as_ptr(), path.len(), dir) }).map(drop) } -path_call! { - mkdir => toyos_fs_mkdir; - rmdir => toyos_fs_rmdir; - unlink => toyos_fs_unlink; - /// Make `path` the working directory, which a file server judges when it - /// serves it and the kernel when it does. - chdir => toyos_fs_chdir; +/// Make `path` the working directory, which a file server judges when it +/// serves it and the kernel when it does. +pub fn chdir(path: &[u8]) -> Result<(), Error> { + // SAFETY: `path` is live for the call. + decode(unsafe { toyos_fs_chdir(path.as_ptr(), path.len()) }).map(drop) } /// `from` renamed `to`, both on one server. -pub fn rename(from: &[u8], to: &[u8]) -> Result<(), Error> { +pub fn rename(from: (At, &[u8]), to: (At, &[u8])) -> Result<(), Error> { + // SAFETY: both live for the call. + let word = unsafe { toyos_fs_rename(from.0.word(), from.1.as_ptr(), from.1.len(), to.0.word(), to.1.as_ptr(), to.1.len()) }; + decode(word).map(drop) +} + +/// Give what `from` names, a link in its last component followed where +/// `follow`, the name `to` too, on one server. +pub fn link(from: (At, &[u8]), follow: bool, to: (At, &[u8])) -> Result<(), Error> { // SAFETY: both live for the call. - decode(unsafe { toyos_fs_rename(from.as_ptr(), from.len(), to.as_ptr(), to.len()) }).map(drop) + let word = unsafe { + toyos_fs_link(from.0.word(), from.1.as_ptr(), from.1.len(), follow, to.0.word(), to.1.as_ptr(), to.1.len()) + }; + decode(word).map(drop) } /// Make `link` a symlink holding `target`, stored as written. -pub fn symlink(target: &[u8], link: &[u8]) -> Result<(), Error> { +pub fn symlink(target: &[u8], link: (At, &[u8])) -> Result<(), Error> { // SAFETY: both live for the call. - decode(unsafe { toyos_fs_symlink(target.as_ptr(), target.len(), link.as_ptr(), link.len()) }).map(drop) + decode(unsafe { toyos_fs_symlink(target.as_ptr(), target.len(), link.0.word(), link.1.as_ptr(), link.1.len()) }).map(drop) } /// What the link at `path` holds. -pub fn read_link(path: &[u8]) -> Result, Error> { +pub fn read_link(at: At, path: &[u8]) -> Result, Error> { let mut out = alloc::vec![0u8; toyos_abi::syscall::MAX_PATH]; // SAFETY: both live for the call; `out` is writable. - let n = decode(unsafe { toyos_fs_readlink(path.as_ptr(), path.len(), out.as_mut_ptr(), out.len()) })? as usize; + let n = decode(unsafe { toyos_fs_readlink(at.word(), path.as_ptr(), path.len(), out.as_mut_ptr(), out.len()) })? as usize; out.truncate(n); Ok(out) } -/// The listing of the directory at `path`, in [`crate::fs::for_each_entry`]'s -/// encoding whoever serves it. -pub fn read_dir(path: &[u8]) -> Result, Error> { +/// Make `change`'s times to what `path` from `at` names, following a link in +/// its last component where `follow`; the empty path from a descriptor is +/// what it names. Its size is [`truncate`]'s. +pub fn set_times(at: At, path: &[u8], follow: bool, change: &Change) -> Result<(), Error> { + let mut request = fs::Request::new(); + change.encode(&mut request); + let (flags, atime, mtime) = (request.flags & !fs::SET_SIZE, request.atime, request.mtime); + // SAFETY: `path` is live for the call. + decode(unsafe { toyos_fs_set_times(at.word(), path.as_ptr(), path.len(), follow, flags, atime, mtime) }).map(drop) +} + +/// The listing a descriptor of a directory opened, from `cursor` (0 at its +/// start), in [`crate::fs::for_each_entry`]'s encoding whoever serves it: +/// the batch and the cursor to ask from next, `None` after the last. +pub fn read_dir(fd: Fd, cursor: u64) -> Result<(Vec, Option), Error> { let mut out = alloc::vec![0u8; 64 * 1024]; loop { - // SAFETY: both live for the call; `out` is writable. - let n = decode(unsafe { toyos_fs_readdir(path.as_ptr(), path.len(), out.as_mut_ptr(), out.len()) })? as usize; + let mut next = fs::READDIR_END; + // SAFETY: all live for the call; `out` and `next` are writable. + let n = decode(unsafe { toyos_fs_readdir(fd, cursor, out.as_mut_ptr(), out.len(), &mut next) })? as usize; if n <= out.len() { out.truncate(n); - return Ok(out); + return Ok((out, (next != fs::READDIR_END).then_some(next))); } out.resize(n, 0); } @@ -269,7 +400,7 @@ pub fn read_dir(path: &[u8]) -> Result, Error> { /// process's own, wherever it is served: what a spawn names as its program /// (`SpawnArgs::image`) and [`dl_open`] loads, since the kernel opens neither. pub fn read_image(path: &[u8]) -> Result> { - let fd = open(path, Open { read: true, cloexec: true, ..Open::default() }).map_err(ImageRefused::Read)?; + let fd = open(At::Cwd, path, Open { read: true, cloexec: true, ..Open::default() }).map_err(ImageRefused::Read)?; let image = match fstat(fd) { Ok(stat) => Image::read(stat.size, |buf| read(fd, buf)), Err(e) => Err(ImageRefused::Read(e)), @@ -303,8 +434,13 @@ pub fn dl_open(path: &[u8]) -> Result { /// Whether `path` is on a file server rather than the kernel. pub fn is_served(path: &[u8]) -> bool { + is_served_at(At::Cwd, path) == Ok(true) +} + +/// Whether `path` from `at` is on a file server rather than the kernel. +pub fn is_served_at(at: At, path: &[u8]) -> Result { // SAFETY: `path` is live for the call. - decode(unsafe { toyos_fs_is_served(path.as_ptr(), path.len()) }) == Ok(1) + decode(unsafe { toyos_fs_is_served(at.word(), path.as_ptr(), path.len()) }).map(|n| n == 1) } /// `path`, absolute against `cwd` and without `.` or `..`. @@ -351,6 +487,7 @@ mod tests { Error::Loop, Error::CrossesServers, Error::NotAFile, + Error::NotADirectory, ]; every.extend((0..=255).filter_map(|code| SyscallError::from_u64(u64::MAX - code)).map(Error::Kernel)); every.extend((0..=255u8).filter_map(fs::Error::from_byte).map(Error::Served)); diff --git a/sdk/toyos/src/files/owner.rs b/sdk/toyos/src/files/owner.rs index 15538db7c68..45f97a98c7c 100644 --- a/sdk/toyos/src/files/owner.rs +++ b/sdk/toyos/src/files/owner.rs @@ -6,12 +6,12 @@ use alloc::string::String; use alloc::sync::Arc; use alloc::vec::Vec; -use toyos_abi::syscall::{self, FileType, OpenFlags, SeekFrom, Stat, SyscallError}; +use toyos_abi::syscall::{self, FileType, OpenFlags, SeekFrom, SyscallError}; use toyos_abi::RawHandle; -use super::{encode, Error, Open, MAX_CAPABILITY_DEPTH, MAX_LINKS, SEEK_CURRENT, SEEK_END, SEEK_START}; -use crate::fd::{self, Fd, Object, SERVED_FILE}; -use crate::fs::{self, Dir, Refused}; +use super::{encode, At, Error, Open, Stat, MAX_CAPABILITY_DEPTH, MAX_LINKS, SEEK_CURRENT, SEEK_END, SEEK_START}; +use crate::fd::{self, Fd, Object, KERNEL_DIR, SERVED_FILE}; +use crate::fs::{self, Base, Change, Dir, Fid, Refused}; use crate::sync::{Lock, Turnstile}; /// One directory capability this process holds, connected: threads sharing @@ -23,39 +23,43 @@ struct Capability { /// Every capability this process has looked for, found or not. static CAPABILITIES: Lock>>> = Lock::new(BTreeMap::new()); -/// Where a path is served. -enum Route { +/// Where a path is resolved: by the kernel, from its absolute path; or by a +/// server, from a directory of its own. +enum Place { Kernel(String), - Served(Arc, String), + Served(Arc, Base, String), } -/// A file a file server holds open, which a descriptor names. -struct OpenFile { - cap: Arc, - fid: u64, - generation: u64, +/// What a descriptor of this module's names. +enum Held { + /// A file or directory a file server holds open. + Served { cap: Arc, fid: Fid }, + /// A directory the kernel serves, by its absolute path. + KernelDir(String), } -impl Drop for OpenFile { +impl Drop for Held { fn drop(&mut self) { - self.cap.dir.lock().close(self.fid, self.generation); + if let Held::Served { cap, fid } = self { + cap.dir.lock().close(*fid); + } } } -/// The open files descriptors name: a served file's object word is its -/// index here, and the slot is emptied when the table lets the object go. -static OPEN: Lock>>> = Lock::new(Vec::new()); +/// What descriptors name: a held object's word is its index here, and the +/// slot is emptied when the table lets the object go. +static OPEN: Lock>>> = Lock::new(Vec::new()); -/// A descriptor's object let go: the open file goes with it, closed outside +/// A descriptor's object let go: what it held goes with it, closed outside /// the lock, as a close asks the server. -extern "C" fn release_file(word: u64) { - let file = usize::try_from(word).ok().and_then(|i| OPEN.lock().get_mut(i).and_then(Option::take)); - assert!(file.is_some(), "toyos::files: a released open file {word} that was not held"); - drop(file); +extern "C" fn release_held(word: u64) { + let held = usize::try_from(word).ok().and_then(|i| OPEN.lock().get_mut(i).and_then(Option::take)); + assert!(held.is_some(), "toyos::files: a released object {word} that was not held"); + drop(held); } -/// The open file `word` names, while a descriptor names it. -fn open_file(word: u64) -> Option> { +/// What `word` names, while a descriptor names it. +fn held(word: u64) -> Option> { OPEN.lock().get(usize::try_from(word).ok()?)?.clone() } @@ -87,97 +91,143 @@ fn capability(prefix: &str) -> Result>, Error> { Ok(CAPABILITIES.lock().entry(String::from(prefix)).or_insert(found).clone()) } -/// Which server `abs` is on, and the path there. -fn route(abs: &str) -> Result { +/// Which server the absolute, lexically clean `abs` is on, and the path there. +fn route(abs: &str) -> Result { let parts: Vec<&str> = abs.split('/').filter(|p| !p.is_empty()).collect(); for depth in (1..=parts.len().min(MAX_CAPABILITY_DEPTH)).rev() { let prefix = alloc::format!("/{}", parts[..depth].join("/")); if let Some(cap) = capability(&prefix)? { - return Ok(Route::Served(cap, parts[depth..].join("/"))); + return Ok(Place::Served(cap, Base::Root, parts[depth..].join("/"))); + } + } + Ok(Place::Kernel(String::from(abs))) +} + +/// Where `path` from `at` is resolved. A descriptor that names no directory +/// is [`Error::NotADirectory`] for any path but an absolute one. +fn place(at: At, path: &[u8]) -> Result { + let path = utf8(path)?; + let fd = match at { + _ if path.starts_with('/') => return route(&super::absolute("/", path)), + At::Cwd => return route(&super::absolute(&cwd(), path)), + At::Fd(fd) => fd, + }; + let named = fd::with(fd, |o| match o.tag { + SERVED_FILE | KERNEL_DIR if o.kernel_handle().is_none() => held(o.word), + _ => None, + }); + match named.map_err(Error::Fd)?.as_deref() { + Some(Held::Served { cap, fid }) => { + let rel = fs::wire_path(path).ok_or(Error::Served(fs::Error::NameTooLong))?; + Ok(Place::Served(Arc::clone(cap), Base::Fid(*fid), rel)) } + Some(Held::KernelDir(dir)) => route(&super::absolute(dir, path)), + None => Err(Error::NotADirectory), } - Ok(Route::Kernel(String::from(abs))) } -/// Ask whoever serves `path`, following an absolute link a server meets to -/// wherever it lands in this process's own table. `served` runs on the -/// capability's turn, so nothing it does may reach the descriptor table, +/// Ask whoever serves `path` from `at`, following an absolute link a server +/// meets to wherever it lands in this process's own table. `served` runs on +/// the capability's turn, so nothing it does may reach the descriptor table, /// whose release of an open file takes that turn again. fn on_path( + at: At, path: &[u8], mut kernel: impl FnMut(&str) -> Result, - mut served: impl FnMut(&Arc, &mut Dir, &str) -> Result, + mut served: impl FnMut(&Arc, &mut Dir, Base, &str) -> Result, ) -> Result { - let mut abs = super::absolute(&cwd(), utf8(path)?); + let mut place = place(at, path)?; for _ in 0..MAX_LINKS { - let (cap, rel) = match route(&abs)? { - Route::Kernel(abs) => return kernel(&abs).map_err(Error::Kernel), - Route::Served(cap, rel) => (cap, rel), + let (cap, base, rel) = match place { + Place::Kernel(abs) => return kernel(&abs).map_err(Error::Kernel), + Place::Served(cap, base, rel) => (cap, base, rel), }; let mut dir = cap.dir.lock(); - match served(&cap, &mut dir, &rel) { + match served(&cap, &mut dir, base, &rel) { Ok(answer) => return Ok(answer), Err(Refused::Error(e)) => return Err(Error::Served(e)), - Err(Refused::Link(len)) => { + Err(Refused::Follow(len)) => { let mut buf = [0u8; fs::MAX_PATH]; - let target = dir.link_target(len, &mut buf); - abs = super::absolute("/", utf8(target)?); + let target = dir.follow_target(len, &mut buf); + let target = super::absolute("/", utf8(target)?); + drop(dir); + place = route(&target)?; } } } Err(Error::Loop) } -/// `kernel` on `fd`'s handle, held for the call, or `served` on its open file. +/// What `fd` names, held for the call: a kernel handle, or an object of this +/// module's. +enum Named { + Handle(RawHandle), + Held(Arc), +} + +/// `kernel` on `fd`'s handle, held for the call, or `served` on what a server +/// holds open for it. A kernel directory answers `kernel_dir`. fn on_fd( fd: Fd, kernel: impl FnOnce(RawHandle) -> Result, - served: impl FnOnce(&mut Dir, u64, u64) -> Result, + kernel_dir: impl FnOnce(&str) -> Result, + served: impl FnOnce(&mut Dir, Fid) -> Result, ) -> Result { - // A kernel call is made while the descriptor's object is held; an open - // file is taken out of it, counted, and asked after. + // A kernel call is made while the descriptor's object is held; an object + // of this module's is taken out of it, counted, and asked after. let named = fd::with(fd, |o| match (o.kernel_handle(), o.tag) { (Some(handle), _) => Ok(Err(kernel(handle).map_err(Error::Kernel))), - (None, SERVED_FILE) => { - Ok(Ok(open_file(o.word).expect("toyos::files: a held descriptor names an open file"))) + (None, SERVED_FILE | KERNEL_DIR) => { + Ok(Ok(held(o.word).expect("toyos::files: a held descriptor names an object of this module's"))) } (None, _) => Err(Error::NotAFile), }); match named.map_err(Error::Fd)?? { Err(answer) => answer, - Ok(file) => served(&mut file.cap.dir.lock(), file.fid, file.generation).map_err(Error::Served), + Ok(held) => match &*held { + Held::Served { cap, fid } => served(&mut cap.dir.lock(), *fid).map_err(Error::Served), + Held::KernelDir(path) => kernel_dir(path), + }, } } -/// What an open opened, before a descriptor names it. -enum Opened { - Kernel(RawHandle), - Served(Arc, fs::Opened), +/// What `fd` names, for a call that asks about it rather than through it. +fn named(fd: Fd) -> Result { + let named = fd::with(fd, |o| match (o.kernel_handle(), o.tag) { + (Some(handle), _) => Ok(Named::Handle(handle)), + (None, SERVED_FILE | KERNEL_DIR) => { + Ok(Named::Held(held(o.word).expect("toyos::files: a held descriptor names an object of this module's"))) + } + (None, _) => Err(Error::NotAFile), + }); + named.map_err(Error::Fd)? } -/// A descriptor for an open file a server answered: on a full table the -/// file is closed again, which takes its capability's turn, so no turn may be -/// held here. -fn served_fd(cap: Arc, opened: fs::Opened, cloexec: bool) -> Result { - let file = Arc::new(OpenFile { cap, fid: opened.fid, generation: opened.generation }); +/// A descriptor for `held`; on a full table it is let go again, which takes +/// its capability's turn, so no turn may be held here. +fn install(held: Held, cloexec: bool) -> Result { + let tag = match held { + Held::Served { .. } => SERVED_FILE, + Held::KernelDir(_) => KERNEL_DIR, + }; + let held = Arc::new(held); let word = { let mut open = OPEN.lock(); match open.iter().position(Option::is_none) { Some(free) => { - open[free] = Some(file); + open[free] = Some(held); free } None => { - open.push(Some(file)); + open.push(Some(held)); open.len() - 1 } } }; - let object = Object::tagged(SERVED_FILE, word as u64, release_file); - fd::install(object, !cloexec, 0) + fd::install(Object::tagged(tag, word as u64, release_held), !cloexec, 0) } -/// What a server answered, as the kernel's `Stat` says it. +/// What a server answered, as this module says it. fn stat_of(stat: fs::Stat) -> Stat { let file_type = match stat.kind { fs::KIND_FILE => FileType::File, @@ -185,7 +235,16 @@ fn stat_of(stat: fs::Stat) -> Stat { fs::KIND_SYMLINK => FileType::Symlink, _ => FileType::Unknown, }; - Stat { file_type, size: stat.size, mtime: stat.mtime, volume: stat.volume, node: stat.node } + Stat { + file_type, + size: stat.size, + nlink: stat.nlink, + atime: stat.atime, + mtime: stat.mtime, + ctime: stat.ctime, + volume: stat.volume, + node: stat.node, + } } fn answer(result: Result) -> i64 { @@ -204,23 +263,48 @@ unsafe fn bytes<'a>(ptr: *const u8, len: usize) -> &'a [u8] { unsafe { core::slice::from_raw_parts(ptr, len) } } +/// What an open opened, before a descriptor names it. +enum Opened { + Kernel(RawHandle), + Held(Held), +} + +/// The kernel's open of `abs`; a directory, which the kernel opens no handle +/// on, is its path. +fn kernel_open(abs: &str, how: Open) -> Result { + if how.unnamed { + return Err(SyscallError::NotSupported); + } + let writes = how.write || how.append || how.create || how.create_new || how.truncate; + let is_dir = || syscall::stat(abs.as_bytes(), true).map(|st| st.file_type == FileType::Directory); + if !writes && is_dir().unwrap_or(false) { + return Ok(Opened::Held(Held::KernelDir(String::from(abs)))); + } + if how.directory { + // Anything else at the name is no directory, and nothing there is + // refused as the kernel refuses it. + is_dir()?; + return Err(SyscallError::InvalidArgument); + } + let mut flags = OpenFlags(0); + if how.read { flags |= OpenFlags::READ; } + if how.write || how.append { flags |= OpenFlags::WRITE; } + if how.append { flags |= OpenFlags::APPEND; } + if how.create || how.create_new { flags |= OpenFlags::CREATE; } + if how.create_new { flags |= OpenFlags::EXCLUSIVE; } + if how.truncate { flags |= OpenFlags::TRUNCATE; } + syscall::open(abs.as_bytes(), flags).map(Opened::Kernel) +} + #[no_mangle] -unsafe extern "C" fn toyos_fs_open(path: *const u8, len: usize, how: Open) -> i64 { +unsafe extern "C" fn toyos_fs_open(at: Fd, path: *const u8, len: usize, how: Open) -> i64 { // SAFETY: the caller's. let path = unsafe { bytes(path, len) }; let opened = on_path( + At::of(at), path, - |abs| { - let mut flags = OpenFlags(0); - if how.read { flags |= OpenFlags::READ; } - if how.write || how.append { flags |= OpenFlags::WRITE; } - if how.append { flags |= OpenFlags::APPEND; } - if how.create || how.create_new { flags |= OpenFlags::CREATE; } - if how.create_new { flags |= OpenFlags::EXCLUSIVE; } - if how.truncate { flags |= OpenFlags::TRUNCATE; } - syscall::open(abs.as_bytes(), flags).map(Opened::Kernel) - }, - |cap, dir, rel| { + |abs| kernel_open(abs, how), + |cap, dir, base, rel| { let mut flags = 0; if how.read { flags |= fs::O_READ; } if how.write { flags |= fs::O_WRITE; } @@ -228,15 +312,20 @@ unsafe extern "C" fn toyos_fs_open(path: *const u8, len: usize, how: Open) -> i6 if how.create { flags |= fs::O_CREATE; } if how.create_new { flags |= fs::O_CREATE_NEW; } if how.truncate { flags |= fs::O_TRUNCATE; } - dir.open(rel, flags).map(|opened| Opened::Served(Arc::clone(cap), opened)) + if how.directory { flags |= fs::O_DIRECTORY; } + let opened = match how.unnamed { + true => dir.create_unnamed(base, rel, flags)?, + false => dir.open(base, rel, flags)?, + }; + Ok(Opened::Held(Held::Served { cap: Arc::clone(cap), fid: opened.fid })) }, ); - // Installed once the turn is let go: on a full table the open file is - // closed again (`served_fd`), and a kernel one by the table. + // Installed once the turn is let go: on a full table what was opened is + // closed again (`install`), and a kernel handle by the table. let installed = opened.and_then(|opened| { match opened { Opened::Kernel(handle) => fd::install(Object::handle(handle), !how.cloexec, 0), - Opened::Served(cap, opened) => served_fd(cap, opened, how.cloexec), + Opened::Held(held) => install(held, how.cloexec), } .map_err(Error::Fd) }); @@ -244,20 +333,26 @@ unsafe extern "C" fn toyos_fs_open(path: *const u8, len: usize, how: Open) -> i6 } #[no_mangle] -unsafe extern "C" fn toyos_fs_stat(path: *const u8, len: usize, follow: bool, out: *mut Stat) -> i64 { +unsafe extern "C" fn toyos_fs_stat(at: Fd, path: *const u8, len: usize, follow: bool, out: *mut Stat) -> i64 { // SAFETY: the caller's. let path = unsafe { bytes(path, len) }; - let stat = on_path(path, |abs| syscall::stat(abs.as_bytes(), follow), |_, dir, rel| dir.stat(rel, follow).map(stat_of)); - answer(stat.map(|stat| { - // SAFETY: the caller's live place. - unsafe { out.write(stat) }; - 0 - })) -} - -#[no_mangle] -unsafe extern "C" fn toyos_fs_fstat(fd: Fd, out: *mut Stat) -> i64 { - let stat = on_fd(fd, syscall::fstat, |dir, fid, generation| dir.fstat(fid, generation).map(stat_of)); + let stat = match (At::of(at), path.is_empty()) { + // What the descriptor names itself. + (At::Fd(fd), true) => match named(fd) { + Ok(Named::Handle(handle)) => syscall::fstat(handle).map(Stat::of_kernel).map_err(Error::Kernel), + Ok(Named::Held(held)) => match &*held { + Held::Served { cap, fid } => cap.dir.lock().fstat(*fid).map(stat_of).map_err(Error::Served), + Held::KernelDir(dir) => syscall::stat(dir.as_bytes(), true).map(Stat::of_kernel).map_err(Error::Kernel), + }, + Err(e) => Err(e), + }, + (at, _) => on_path( + at, + path, + |abs| syscall::stat(abs.as_bytes(), follow).map(Stat::of_kernel), + |_, dir, base, rel| dir.stat(base, rel, follow).map(stat_of), + ), + }; answer(stat.map(|stat| { // SAFETY: the caller's live place. unsafe { out.write(stat) }; @@ -275,6 +370,11 @@ fn on_stream(fd: Fd, f: impl FnOnce(RawHandle) -> Result(_: &str) -> Result { + Err(Error::Served(fs::Error::IsADirectory)) +} + #[no_mangle] unsafe extern "C" fn toyos_fs_read(fd: Fd, buf: *mut u8, len: usize) -> i64 { // SAFETY: the caller lends `len` writable bytes for the call, and only @@ -284,7 +384,7 @@ unsafe extern "C" fn toyos_fs_read(fd: Fd, buf: *mut u8, len: usize) -> i64 { if let Some(n) = on_stream(fd, |h| crate::net::stream::read(h, out(), blocking)) { return answer(n.map(|n| n as u64)); } - let n = on_fd(fd, |h| syscall::read(h, out()), |dir, fid, generation| dir.read(fid, generation, out())); + let n = on_fd(fd, |h| syscall::read(h, out()), is_a_directory, |dir, fid| dir.read(fid, out())); answer(n.map(|n| n as u64)) } @@ -296,7 +396,24 @@ unsafe extern "C" fn toyos_fs_write(fd: Fd, buf: *const u8, len: usize) -> i64 { if let Some(n) = on_stream(fd, |h| crate::net::stream::write(h, buf, blocking)) { return answer(n.map(|n| n as u64)); } - let n = on_fd(fd, |h| syscall::write(h, buf), |dir, fid, generation| dir.write(fid, generation, buf).map(|w| w.len)); + let n = on_fd(fd, |h| syscall::write(h, buf), is_a_directory, |dir, fid| dir.write(fid, buf).map(|w| w.len)); + answer(n.map(|n| n as u64)) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_pread(fd: Fd, buf: *mut u8, len: usize, offset: u64) -> i64 { + // SAFETY: the caller lends `len` writable bytes for the call, and only + // one of the closures runs. + let out = || unsafe { core::slice::from_raw_parts_mut(buf, len) }; + let n = on_fd(fd, |h| syscall::pread(h, out(), offset), is_a_directory, |dir, fid| dir.pread(fid, offset, out())); + answer(n.map(|n| n as u64)) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_pwrite(fd: Fd, buf: *const u8, len: usize, offset: u64) -> i64 { + // SAFETY: the caller's. + let buf = unsafe { bytes(buf, len) }; + let n = on_fd(fd, |h| syscall::pwrite(h, buf, offset), is_a_directory, |dir, fid| dir.pwrite(fid, offset, buf)); answer(n.map(|n| n as u64)) } @@ -308,57 +425,47 @@ extern "C" fn toyos_fs_seek(fd: Fd, whence: u32, delta: i64) -> i64 { SEEK_END => (SeekFrom::End(delta), fs::SEEK_END), _ => return encode(Error::Kernel(SyscallError::InvalidArgument)), }; - answer(on_fd(fd, |h| syscall::seek(h, kernel), |dir, fid, generation| dir.seek(fid, generation, served, delta))) + // A kernel directory has no offset to move. + let kernel_dir = |_: &str| Err(Error::Kernel(SyscallError::InvalidArgument)); + answer(on_fd(fd, |h| syscall::seek(h, kernel), kernel_dir, |dir, fid| dir.seek(fid, served, delta))) } #[no_mangle] extern "C" fn toyos_fs_truncate(fd: Fd, size: u64) -> i64 { - answer(on_fd(fd, |h| syscall::ftruncate(h, size), |dir, fid, generation| dir.truncate(fid, generation, size)).map(|()| 0)) + answer(on_fd(fd, |h| syscall::ftruncate(h, size), is_a_directory, |dir, fid| dir.truncate(fid, size)).map(|()| 0)) } #[no_mangle] extern "C" fn toyos_fs_fsync(fd: Fd) -> i64 { - answer(on_fd(fd, syscall::fsync, |dir, fid, generation| dir.fsync(fid, generation)).map(|()| 0)) + // A kernel directory holds nothing its files' syncs do not write. + answer(on_fd(fd, syscall::fsync, |_| Ok(()), |dir, fid| dir.fsync(fid)).map(|()| 0)) } #[no_mangle] extern "C" fn toyos_fs_stream(fd: Fd) -> i64 { - let handle = on_fd(fd, syscall::dup, |dir, fid, generation| dir.stream(fid, generation).map(|pipe| pipe.into_raw())); + let handle = on_fd(fd, syscall::dup, is_a_directory, |dir, fid| dir.stream(fid).map(|pipe| pipe.into_raw())); answer(handle.map(|h| u64::from(h.0))) } -/// A call on one path, with nothing to answer but whether it was made. -/// -/// # Safety -/// `path` names `len` readable bytes for the call. -unsafe fn path_call( - path: *const u8, - len: usize, - kernel: impl FnMut(&str) -> Result<(), SyscallError>, - served: impl FnMut(&mut Dir, &str) -> Result<(), Refused>, -) -> i64 { - let mut served = served; - // SAFETY: the caller's. - let path = unsafe { bytes(path, len) }; - answer(on_path(path, kernel, |_, dir, rel| served(dir, rel)).map(|()| 0)) -} - #[no_mangle] -unsafe extern "C" fn toyos_fs_mkdir(path: *const u8, len: usize) -> i64 { +unsafe extern "C" fn toyos_fs_mkdir(at: Fd, path: *const u8, len: usize) -> i64 { // SAFETY: the caller's. - unsafe { path_call(path, len, |abs| syscall::mkdir(abs.as_bytes()), |dir, rel| dir.mkdir(rel)) } -} - -#[no_mangle] -unsafe extern "C" fn toyos_fs_rmdir(path: *const u8, len: usize) -> i64 { - // SAFETY: the caller's. - unsafe { path_call(path, len, |abs| syscall::rmdir(abs.as_bytes()), |dir, rel| dir.rmdir(rel)) } + let path = unsafe { bytes(path, len) }; + let made = on_path(At::of(at), path, |abs| syscall::mkdir(abs.as_bytes()), |_, dir, base, rel| dir.mkdir(base, rel)); + answer(made.map(|()| 0)) } #[no_mangle] -unsafe extern "C" fn toyos_fs_unlink(path: *const u8, len: usize) -> i64 { +unsafe extern "C" fn toyos_fs_unlink(at: Fd, path: *const u8, len: usize, is_dir: bool) -> i64 { // SAFETY: the caller's. - unsafe { path_call(path, len, |abs| syscall::delete(abs.as_bytes()), |dir, rel| dir.unlink(rel)) } + let path = unsafe { bytes(path, len) }; + let removed = on_path( + At::of(at), + path, + |abs| if is_dir { syscall::rmdir(abs.as_bytes()) } else { syscall::delete(abs.as_bytes()) }, + |_, dir, base, rel| dir.unlink(base, rel, is_dir), + ); + answer(removed.map(|()| 0)) } #[no_mangle] @@ -367,8 +474,13 @@ unsafe extern "C" fn toyos_fs_chdir(path: *const u8, len: usize) -> i64 { let path = unsafe { bytes(path, len) }; let changed = utf8(path).and_then(|path| { let abs = super::absolute(&cwd(), path); - if let Route::Served(..) = route(&abs)? { - let stat = on_path(abs.as_bytes(), |abs| syscall::stat(abs.as_bytes(), true), |_, dir, rel| dir.stat(rel, true).map(stat_of))?; + if let Place::Served(..) = route(&abs)? { + let stat = on_path( + At::Cwd, + abs.as_bytes(), + |abs| syscall::stat(abs.as_bytes(), true).map(Stat::of_kernel), + |_, dir, base, rel| dir.stat(base, rel, true).map(stat_of), + )?; if stat.file_type != FileType::Directory { return Err(Error::Served(fs::Error::NotADirectory)); } @@ -378,55 +490,87 @@ unsafe extern "C" fn toyos_fs_chdir(path: *const u8, len: usize) -> i64 { answer(changed.map(|()| 0)) } +/// Two paths one call names, on one server: both where the kernel serves +/// them, or the capability and both bases. +enum Pair { + Kernel(String, String), + Served(Arc, (Base, String), (Base, String)), +} + +fn pair(from: (At, &[u8]), to: (At, &[u8])) -> Result { + match (place(from.0, from.1)?, place(to.0, to.1)?) { + (Place::Kernel(a), Place::Kernel(b)) => Ok(Pair::Kernel(a, b)), + (Place::Served(ca, ba, a), Place::Served(cb, bb, b)) if Arc::ptr_eq(&ca, &cb) => Ok(Pair::Served(ca, (ba, a), (bb, b))), + _ => Err(Error::CrossesServers), + } +} + +/// A served answer to a call no link may redirect: a link to follow out of +/// the directory leads to another server. +fn crossing(refused: Refused) -> Error { + match refused { + Refused::Error(e) => Error::Served(e), + Refused::Follow(_) => Error::CrossesServers, + } +} + #[no_mangle] -unsafe extern "C" fn toyos_fs_rename(from: *const u8, from_len: usize, to: *const u8, to_len: usize) -> i64 { +unsafe extern "C" fn toyos_fs_rename(from_at: Fd, from: *const u8, from_len: usize, to_at: Fd, to: *const u8, to_len: usize) -> i64 { // SAFETY: the caller's. let (from, to) = unsafe { (bytes(from, from_len), bytes(to, to_len)) }; - let renamed = (|| { - let cwd = cwd(); - let (from, to) = (super::absolute(&cwd, utf8(from)?), super::absolute(&cwd, utf8(to)?)); - match (route(&from)?, route(&to)?) { - // The kernel refuses a rename between its own mounts as - // unsupported, which is a rename across servers as POSIX reads it. - (Route::Kernel(a), Route::Kernel(b)) => syscall::rename(a.as_bytes(), b.as_bytes()).map_err(|e| match e { - SyscallError::NotSupported => Error::CrossesServers, - e => Error::Kernel(e), - }), - (Route::Served(ca, a), Route::Served(cb, b)) if Arc::ptr_eq(&ca, &cb) => { - ca.dir.lock().rename(&a, &b).map_err(|refused| match refused { - Refused::Error(e) => Error::Served(e), - // A link out of the directory leads to another server. - Refused::Link(_) => Error::CrossesServers, - }) - } - _ => Err(Error::CrossesServers), - } - })(); + let renamed = pair((At::of(from_at), from), (At::of(to_at), to)).and_then(|pair| match pair { + // The kernel refuses a rename between its own mounts as + // unsupported, which is a rename across servers as POSIX reads it. + Pair::Kernel(a, b) => syscall::rename(a.as_bytes(), b.as_bytes()).map_err(|e| match e { + SyscallError::NotSupported => Error::CrossesServers, + e => Error::Kernel(e), + }), + Pair::Served(cap, (ba, a), (bb, b)) => cap.dir.lock().rename((ba, &a), (bb, &b)).map_err(crossing), + }); answer(renamed.map(|()| 0)) } #[no_mangle] -unsafe extern "C" fn toyos_fs_symlink(target: *const u8, target_len: usize, link: *const u8, link_len: usize) -> i64 { +unsafe extern "C" fn toyos_fs_link( + from_at: Fd, + from: *const u8, + from_len: usize, + follow: bool, + to_at: Fd, + to: *const u8, + to_len: usize, +) -> i64 { + // SAFETY: the caller's. + let (from, to) = unsafe { (bytes(from, from_len), bytes(to, to_len)) }; + let linked = pair((At::of(from_at), from), (At::of(to_at), to)).and_then(|pair| match pair { + Pair::Kernel(..) => Err(Error::Kernel(SyscallError::NotSupported)), + Pair::Served(cap, (ba, a), (bb, b)) => cap.dir.lock().link((ba, &a), follow, (bb, &b), None).map_err(crossing), + }); + answer(linked.map(|()| 0)) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_symlink(target: *const u8, target_len: usize, at: Fd, link: *const u8, link_len: usize) -> i64 { // SAFETY: the caller's. let (target, link) = unsafe { (bytes(target, target_len), bytes(link, link_len)) }; let made = utf8(target).and_then(|target| { - on_path(link, |abs| syscall::symlink(target.as_bytes(), abs.as_bytes()), |_, dir, rel| { - dir.symlink(target, rel).map_err(Refused::Error) + on_path(At::of(at), link, |abs| syscall::symlink(target.as_bytes(), abs.as_bytes()), |_, dir, base, rel| { + dir.symlink(target, (base, rel)) }) }); answer(made.map(|()| 0)) } #[no_mangle] -unsafe extern "C" fn toyos_fs_readlink(path: *const u8, len: usize, out: *mut u8, cap: usize) -> i64 { +unsafe extern "C" fn toyos_fs_readlink(at: Fd, path: *const u8, len: usize, out: *mut u8, cap: usize) -> i64 { // SAFETY: the caller's. let path = unsafe { bytes(path, len) }; // SAFETY: the caller lends `cap` writable bytes for the call, and each // closure makes the slice only for its own run. let target = || unsafe { core::slice::from_raw_parts_mut(out, cap) }; - let n = on_path(path, |abs| syscall::readlink(abs.as_bytes(), target()), |_, dir, rel| { + let n = on_path(At::of(at), path, |abs| syscall::readlink(abs.as_bytes(), target()), |_, dir, base, rel| { let mut buf = [0u8; fs::MAX_PATH]; - let n = dir.read_link(rel, &mut buf)?; + let n = dir.read_link(base, rel, &mut buf)?; let out = target(); let n = n.min(out.len()); out[..n].copy_from_slice(&buf[..n]); @@ -435,62 +579,89 @@ unsafe extern "C" fn toyos_fs_readlink(path: *const u8, len: usize, out: *mut u8 answer(n.map(|n| n as u64)) } -/// A listing too large for the first buffer is asked again at the size it -/// named; a directory that outgrows its own listing this many times is -/// refused rather than waited on. -const LIST_ATTEMPTS: usize = 4; - #[no_mangle] -unsafe extern "C" fn toyos_fs_readdir(path: *const u8, len: usize, out: *mut u8, cap: usize) -> i64 { +unsafe extern "C" fn toyos_fs_set_times(at: Fd, path: *const u8, len: usize, follow: bool, flags: u64, atime: u64, mtime: u64) -> i64 { // SAFETY: the caller's. let path = unsafe { bytes(path, len) }; - let listing = on_path( - path, - |abs| { - let mut buf = alloc::vec![0u8; 64 * 1024]; - for _ in 0..LIST_ATTEMPTS { - let n = syscall::readdir(abs.as_bytes(), &mut buf)?; - if n <= buf.len() { - // The kernel's listing, in the served one's encoding. - let (mut out, mut at) = (Vec::new(), 0); - while let Some(d) = syscall::dirent(&buf[..n], &mut at) { - let kind = if d.is_dir { fs::KIND_DIR } else { fs::KIND_FILE }; - let name = core::str::from_utf8(d.name).unwrap_or(""); - let entry = fs::Entry { kind, size: 0, node: d.node, name }; - fs::encode_entry(&mut out, &entry).ok_or(SyscallError::InvalidArgument)?; - } - return Ok(out); - } - buf.resize(n, 0); - } - Err(SyscallError::ResourceExhausted) + let request = fs::Request { flags, atime, mtime, ..fs::Request::new() }; + let Some(change) = Change::decode(&request).filter(|c| c.size.is_none()) else { + return encode(Error::Served(fs::Error::InvalidRequest)); + }; + let unsupported = || Err(Error::Kernel(SyscallError::NotSupported)); + let set = match (At::of(at), path.is_empty()) { + // What the descriptor names itself. + (At::Fd(fd), true) => match named(fd) { + Ok(Named::Held(held)) => match &*held { + Held::Served { cap, fid } => cap.dir.lock().set_attr(Base::Fid(*fid), "", follow, &change).map_err(crossing), + Held::KernelDir(_) => unsupported(), + }, + Ok(Named::Handle(_)) => unsupported(), + Err(e) => Err(e), }, - |_, dir, rel| { - let mut buf = alloc::vec![0u8; 64 * 1024]; - for _ in 0..LIST_ATTEMPTS { - let n = dir.read_dir(rel, &mut buf)?; - if n <= buf.len() { - buf.truncate(n); - return Ok(buf); - } - buf.resize(n, 0); + (at, _) => on_path(at, path, |_| Err(SyscallError::NotSupported), |_, dir, base, rel| dir.set_attr(base, rel, follow, &change)), + }; + answer(set.map(|()| 0)) +} + +/// A kernel listing too large for the first buffer is asked again at the size +/// it named; a directory that outgrows its own listing this many times is +/// refused rather than waited on. +const LIST_ATTEMPTS: usize = 4; + +/// The kernel's listing of `abs`, in the served one's encoding. +fn kernel_listing(abs: &str) -> Result, Error> { + let mut buf = alloc::vec![0u8; 64 * 1024]; + for _ in 0..LIST_ATTEMPTS { + let n = syscall::readdir(abs.as_bytes(), &mut buf).map_err(Error::Kernel)?; + if n <= buf.len() { + let (mut out, mut at) = (Vec::new(), 0); + while let Some(d) = syscall::dirent(&buf[..n], &mut at) { + // The kernel tells a directory from anything else, and no more. + let kind = if d.is_dir { fs::KIND_DIR } else { 0 }; + let name = core::str::from_utf8(d.name).unwrap_or(""); + let entry = fs::Entry { kind, size: 0, node: d.node, name }; + fs::encode_entry(&mut out, &entry).ok_or(Error::Kernel(SyscallError::InvalidArgument))?; } - Err(Refused::Error(fs::Error::LimitExceeded)) + return Ok(out); + } + buf.resize(n, 0); + } + Err(Error::Kernel(SyscallError::ResourceExhausted)) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_readdir(fd: Fd, cursor: u64, out: *mut u8, cap: usize, next: *mut u64) -> i64 { + let listing = on_fd( + fd, + // A kernel handle is a file, never a directory. + |_| Err(SyscallError::InvalidArgument), + // The kernel answers a directory whole, as one batch. + |abs| match cursor { + 0 => kernel_listing(abs).map(|listing| (listing, None)), + _ => Ok((Vec::new(), None)), + }, + |dir, fid| { + let mut batch = Vec::new(); + dir.read_dir(fid, cursor, cap, &mut batch).map(|next| (batch, next)) }, ); - answer(listing.map(|listing| { + answer(listing.map(|(listing, after)| { if listing.len() <= cap { - // SAFETY: the caller lends `cap` writable bytes for the call. - unsafe { core::ptr::copy_nonoverlapping(listing.as_ptr(), out, listing.len()) }; + // SAFETY: the caller lends `cap` writable bytes and one `u64` for + // the call. + unsafe { + core::ptr::copy_nonoverlapping(listing.as_ptr(), out, listing.len()); + next.write(after.unwrap_or(fs::READDIR_END)); + } } listing.len() as u64 })) } #[no_mangle] -unsafe extern "C" fn toyos_fs_is_served(path: *const u8, len: usize) -> i64 { +unsafe extern "C" fn toyos_fs_is_served(at: Fd, path: *const u8, len: usize) -> i64 { // SAFETY: the caller's. let path = unsafe { bytes(path, len) }; - let served = utf8(path).and_then(|path| route(&super::absolute(&cwd(), path))); - answer(served.map(|route| u64::from(matches!(route, Route::Served(..))))) + let served = place(At::of(at), path); + answer(served.map(|place| u64::from(matches!(place, Place::Served(..))))) } diff --git a/sdk/toyos/src/fs.rs b/sdk/toyos/src/fs.rs index 782986feba5..cf4e245e8ea 100644 --- a/sdk/toyos/src/fs.rs +++ b/sdk/toyos/src/fs.rs @@ -10,25 +10,33 @@ //! connection and nothing the client says. A program names a file only under a //! directory it holds, and the kernel's part is who holds which connector. //! -//! **The server resolves; the client only asks.** A path on the wire is -//! relative to the capability's directory and canonical — no empty, `.` or -//! `..` component — and the server refuses any other ([`canonical`] is the one -//! rule both ends apply). A symlink whose target stays inside the directory is -//! followed by the server; one that is absolute is answered [`LINK`] with the -//! target and the rest of the path in the window, and resolved again in the -//! client's own table, so it reaches nothing the client does not hold. +//! **A path is resolved from a directory the connection holds**: [`ROOT`], the +//! grant's directory, or a directory it opened, by its file id. The server +//! resolves; the client only asks. A path on the wire is [`relative`]: no +//! empty or `.` component. A `..` is the parent of the directory it is met in, +//! which the server knows by the directory itself and not by the path spelled +//! to it; one that would climb above the grant's directory is refused +//! [`Error::Escapes`]. A symlink whose target stays inside the directory is +//! followed by the server; one that is absolute is answered [`FOLLOW`] with +//! the target and the rest of the path, and resolved again in the client's own +//! table, so it reaches nothing the client does not hold. //! -//! **An open file is a description the server keeps**: the file, the access -//! the open fixed and the offset. A connection names descriptions by id, and -//! [`DUP`] and [`EXPORT`] make second ids that share one description's offset, -//! on the same connection or on a connection made for another process. An -//! exported connection was given files and no directory: it names no path. +//! **An open file or directory is a description the server keeps**: the +//! node, the access the open fixed and the offset. A connection names +//! descriptions by id, and [`DUP`] and [`EXPORT`] make second ids that share +//! one description's offset, on the same connection or on a connection made +//! for another process. An exported connection was given descriptions and no +//! directory: a path on it is refused [`Error::NoDirectory`]. //! -//! **One connection per directory per process, a request and then its -//! reply.** The client lends the server a [`WINDOW_BYTES`] region at connect -//! ([`HELLO`]); paths, data and listings travel through it and the frames stay -//! small and fixed. The window is shared memory both sides write, so each side -//! copies what it reads out of it once and acts on the copy. +//! **A request carries a tag and its span of the window.** The client lends +//! the server a [`WINDOW_BYTES`] region at connect ([`HELLO`]); a request's +//! paths, data and answer travel in the span it names, and the frames stay +//! small and fixed. The reply carries the request's tag. A client keeps at +//! most [`MAX_OUTSTANDING`] requests unanswered on one connection and lets no +//! two of their spans overlap; a server reads a request's input out of its +//! span before it writes its answer there. The window is shared memory both +//! sides write, so each side copies what it reads out of it once and acts on +//! the copy. //! //! **A server that ends is survivable and not invisible.** [`Dir`] connects //! again through the same connector — the supervisor keeps a file server's ports open @@ -63,49 +71,102 @@ pub use toyos_abi::syscall::MAX_PATH; /// truncate past it where the file is. pub const MAX_FILE_BYTES: u64 = (u32::MAX as u64 + 1) * 4096; -// Requests, by frame type. +/// The requests a client may have unanswered on one connection at once. +pub const MAX_OUTSTANDING: usize = 16; + +/// The file id of the directory the connection's grant names. No open answers +/// it, and it is never closed. +pub const ROOT: u64 = 0; + +// Requests, by frame type. A request naming a path names it in `fid` and the +// first `len` bytes of its span: from the directory `fid` is, or with no path +// the file or directory `fid` is itself. `fid2` and `len2` bytes after the +// first path name a second the same way. + +/// Lend the window: it travels in the frame. `value` answers the rights the +/// grant gives ([`RIGHT_WRITE`]). pub const HELLO: u32 = 1; +/// Open the path for [`O_READ`] and the other `O_` `flags`: `value` answers +/// the new file id, and the reply what it opened. A directory opens only to +/// read. pub const OPEN: u32 = 2; -pub const CLOSE: u32 = 3; -pub const READ: u32 = 4; -pub const WRITE: u32 = 5; -pub const STAT: u32 = 6; -pub const LSTAT: u32 = 7; -pub const FSTAT: u32 = 8; -pub const TRUNCATE: u32 = 9; -pub const FSYNC: u32 = 10; -pub const READDIR: u32 = 11; -pub const MKDIR: u32 = 12; -pub const RMDIR: u32 = 13; -pub const UNLINK: u32 = 14; -pub const RENAME: u32 = 15; -pub const SYMLINK: u32 = 16; -pub const READLINK: u32 = 17; -pub const STREAM: u32 = 18; -pub const SYNC: u32 = 19; -/// A second id on the description `fid` names: `value`. -pub const DUP: u32 = 20; +/// A file with no name, in the directory the path names, opened by `flags` as +/// [`OPEN`] opens: a later [`LINK`] gives it one, and nothing else ever does. +pub const CREATE_UNNAMED: u32 = 3; +pub const CLOSE: u32 = 4; +/// Read the span's length at the description's offset into the span, and move +/// the offset past it: `value` answers how much. +pub const READ: u32 = 5; +/// Write the span at the description's offset, or at the end for a file opened +/// to append, and move the offset past it: `value` answers how much, and +/// `value2` the offset after it. +pub const WRITE: u32 = 6; +/// [`READ`] at `offset`, which the description's offset does not follow. +pub const PREAD: u32 = 7; +/// [`WRITE`] at `offset`, whatever the description appends to, and leaving its +/// offset where it was. +pub const PWRITE: u32 = 8; /// Move the description's offset: `flags` is [`SEEK_START`], [`SEEK_CURRENT`] /// or [`SEEK_END`], `offset` the signed distance from it; `value` is where it /// landed. -pub const SEEK: u32 = 21; +pub const SEEK: u32 = 9; +/// A second id on the description `fid` names: `value`. +pub const DUP: u32 = 10; /// A connection to this server holding the descriptions of the `len` ids in -/// the window, little-endian `u64`s, as ids 1.. in that order: the reply +/// the span, little-endian `u64`s, as ids 1.. in that order: the reply /// carries its other end. It spends a served slot of this connection's share /// from here, and names no path. -pub const EXPORT: u32 = 22; +pub const EXPORT: u32 = 11; +/// What the path names, a symlink's target where `flags` has [`FOLLOW_LAST`]. +pub const STAT: u32 = 12; +/// Change what the path names, [`FOLLOW_LAST`] as for [`STAT`]: its size to +/// `offset` ([`SET_SIZE`], only through an id opened to write and no path), +/// its access time to `atime` and its modification time to `mtime`, or either +/// to the server's now. +pub const SETATTR: u32 = 13; +/// Make what this file was written durable on the volume. +pub const FSYNC: u32 = 14; +/// Make the whole volume durable. +pub const SYNC: u32 = 15; /// The free space of the volume behind the capability ([`Room`]). -pub const ROOM: u32 = 23; +pub const ROOM: u32 = 16; +/// A pipe whose bytes the server writes to the file at the description's +/// offset: the reply carries its write end. +pub const STREAM: u32 = 17; +/// The entries of the directory `fid` opened from the cursor `offset`, 0 at +/// its start, [`for_each_entry`]'s encoding in the span: `value` answers its +/// length and `value2` the cursor to ask from next, [`READDIR_END`] after the +/// last. A first entry past the span is refused [`Error::LimitExceeded`]. +pub const READDIR: u32 = 18; +pub const MKDIR: u32 = 19; +/// Remove the name: a directory's only with [`UNLINK_DIR`], which removes +/// nothing else. +pub const UNLINK: u32 = 20; +/// Give the first path's node the second path's name, replacing a file or a +/// symlink there; never a directory. +pub const RENAME: u32 = 21; +/// Make the first path a symlink holding the second's bytes, stored as +/// written. +pub const SYMLINK: u32 = 22; +/// What the symlink at the path holds, into the span: `value` its length. +pub const READLINK: u32 = 23; +/// Give the first path's node, [`FOLLOW_LAST`] as for [`STAT`], the second path's +/// name too, which must be free; with [`LINK_REPLACE`], take the name from +/// the node the description `expected` names, and only if it still names it. +pub const LINK: u32 = 24; pub const SEEK_START: u64 = 0; pub const SEEK_CURRENT: u64 = 1; pub const SEEK_END: u64 = 2; +/// [`READDIR`]'s cursor after a directory's last entry. +pub const READDIR_END: u64 = u64::MAX; + // Replies, by frame type. pub const REPLY: u32 = 0x100; -/// The path met an absolute symlink: the window holds the path to resolve +/// The path met an absolute symlink: the span holds the path to resolve /// instead, `value` bytes long. -pub const LINK: u32 = 0x101; +pub const FOLLOW: u32 = 0x101; // Open flags. pub const O_READ: u64 = 1; @@ -114,15 +175,32 @@ pub const O_APPEND: u64 = 4; pub const O_CREATE: u64 = 8; pub const O_TRUNCATE: u64 = 16; pub const O_CREATE_NEW: u64 = 32; -pub const O_KNOWN: u64 = O_READ | O_WRITE | O_APPEND | O_CREATE | O_TRUNCATE | O_CREATE_NEW; +/// Refused [`Error::NotADirectory`] unless the path names a directory. +pub const O_DIRECTORY: u64 = 64; +pub const O_KNOWN: u64 = O_READ | O_WRITE | O_APPEND | O_CREATE | O_TRUNCATE | O_CREATE_NEW | O_DIRECTORY; + +/// [`STAT`]'s, [`SETATTR`]'s and [`LINK`]'s: follow a symlink in the path's +/// last component. +pub const FOLLOW_LAST: u64 = 1; +// [`SETATTR`]'s, beside [`FOLLOW_LAST`]. +pub const SET_SIZE: u64 = 2; +pub const SET_ATIME: u64 = 4; +pub const SET_ATIME_NOW: u64 = 8; +pub const SET_MTIME: u64 = 16; +pub const SET_MTIME_NOW: u64 = 32; +pub const SETATTR_KNOWN: u64 = FOLLOW_LAST | SET_SIZE | SET_ATIME | SET_ATIME_NOW | SET_MTIME | SET_MTIME_NOW; +/// [`LINK`]'s, beside [`FOLLOW_LAST`]. +pub const LINK_REPLACE: u64 = 2; +pub const LINK_KNOWN: u64 = FOLLOW_LAST | LINK_REPLACE; +/// [`UNLINK`]'s. +pub const UNLINK_DIR: u64 = 1; // Node kinds. pub const KIND_FILE: u64 = 1; pub const KIND_DIR: u64 = 2; pub const KIND_SYMLINK: u64 = 3; -/// [`HELLO`]'s answer: the capability's rights in `value`, and in `value2` the -/// volume the server's files are on: its pid, which no other process ever holds. +/// [`HELLO`]'s answer: the capability's rights in `value`. pub const RIGHT_WRITE: u64 = 1; /// Set by the client on every volume a server answers ([`Stat::volume`]): a @@ -194,6 +272,7 @@ macro_rules! errors { }; } + errors! { wire { NotFound = 1, @@ -245,12 +324,14 @@ errors! { TooManyStreams = 21, /// The device did not do it, or the volume's own structures do not decode. Io = 22, - /// The file was removed or renamed over while it was open, or was opened - /// on an earlier connection to a server that has since restarted. + /// The file was removed or renamed over while it was open, a + /// [`LINK_REPLACE`] found its name no longer on the node it expected, or + /// the id was answered on an earlier connection to a server that has + /// since restarted. Stale = 23, /// A file id the connection does not hold. UnknownFile = 24, - /// A path that is not UTF-8, not [`canonical`], or not inside the window. + /// A path that is not UTF-8, not [`relative`], or not inside the span. InvalidPath = 25, /// A request whose words the protocol gives no meaning: an open flag it /// does not have, a length past the window, an empty name or target. @@ -260,7 +341,7 @@ errors! { /// The kernel had no room for what the request needs: a handle, memory, /// a place in a queue. OutOfResources = 28, - /// A path on a connection that was given files and no directory. + /// A path on a connection that was given descriptions and no directory. NoDirectory = 32, } local { @@ -287,26 +368,42 @@ impl Error { } } + ipc_payload! { - /// Every request's words; what each means is its frame type's. Paths are - /// in the window: the first `len` bytes, and for [`RENAME`] and - /// [`SYMLINK`] a second `len2` bytes after it. + /// Every request's words; what each means is its frame type's. `tag` is + /// the client's and comes back on the reply; `at` and `span` are the + /// request's bytes of the window. pub struct Request { + pub tag: u64, pub fid: u64, + pub fid2: u64, + /// [`LINK_REPLACE`]'s description. + pub expected: u64, pub offset: u64, + pub at: u64, + pub span: u64, pub len: u64, pub len2: u64, pub flags: u64, + pub atime: u64, + pub mtime: u64, } - /// Every reply's words. `status` is 0 or an [`Error`]'s [`Error::code`]. + /// Every reply's words. `status` is 0 or an [`Error`]'s [`Error::code`]; + /// a reply that answers what a node is fills the words from `kind` on + /// ([`Stat`]). pub struct Reply { + pub tag: u64, pub status: u64, - pub kind: u64, pub value: u64, pub value2: u64, + pub kind: u64, + pub size: u64, + pub nlink: u64, + pub atime: u64, pub mtime: u64, - /// A stat-bearing reply's file on the server's volume (`Stat::node`). + pub ctime: u64, + pub volume: u64, pub node: u64, } } @@ -319,19 +416,34 @@ impl Default for Request { impl Request { pub const fn new() -> Self { - Self { fid: 0, offset: 0, len: 0, len2: 0, flags: 0 } + Self { tag: 0, fid: ROOT, fid2: ROOT, expected: 0, offset: 0, at: 0, span: 0, len: 0, len2: 0, flags: 0, atime: 0, mtime: 0 } } } impl Reply { pub const fn ok() -> Self { - Self { status: 0, kind: 0, value: 0, value2: 0, mtime: 0, node: 0 } + Self { tag: 0, status: 0, value: 0, value2: 0, kind: 0, size: 0, nlink: 0, atime: 0, mtime: 0, ctime: 0, volume: 0, node: 0 } } /// A server's refusal; one of the client end's own words is never one. pub fn refused(e: Error) -> Self { let status = e.code().unwrap_or_else(|| panic!("toyos::fs: a server refused with {e:?}, which is the client's")); - Self { status, kind: 0, value: 0, value2: 0, mtime: 0, node: 0 } + Self { status, ..Self::ok() } + } + + /// An answer of what a node is, `volume` as the server numbers it. + pub fn stat(stat: &Stat) -> Self { + Self { + kind: stat.kind, + size: stat.size, + nlink: stat.nlink, + atime: stat.atime, + mtime: stat.mtime, + ctime: stat.ctime, + volume: stat.volume, + node: stat.node, + ..Self::ok() + } } /// The reply, or the refusal its status names; a status this protocol @@ -413,22 +525,36 @@ impl<'a> Grant<'a> { } } -/// Whether `path` is a relative path the wire may carry: components of at -/// least one byte, none of them `.` or `..`, no leading or trailing `/`, and -/// the empty path for the capability's own directory. +/// Whether `path` is a path the wire may carry from a directory: components of +/// at least one byte, none of them `.`, no leading or trailing `/`, and the +/// empty path for the directory itself. A `..` is the server's to resolve. +pub fn relative(path: &str) -> bool { + path.len() <= MAX_PATH && (path.is_empty() || path.split('/').all(|c| !c.is_empty() && c != ".")) +} + +/// Whether `path` is [`relative`] and climbs nowhere: no `..` either. What a +/// grant's root is. pub fn canonical(path: &str) -> bool { - path.len() <= MAX_PATH - && (path.is_empty() || path.split('/').all(|c| !c.is_empty() && c != "." && c != "..")) + relative(path) && (path.is_empty() || path.split('/').all(|c| c != "..")) +} + +/// `path` as the wire carries it from a directory: its empty and `.` +/// components dropped, its `..` kept. `None` for one longer than a path. +pub fn wire_path(path: &str) -> Option { + let parts: Vec<&str> = path.split('/').filter(|c| !c.is_empty() && *c != ".").collect(); + let joined = parts.join("/"); + (joined.len() <= MAX_PATH).then_some(joined) } -// **Who owns the window when.** The server, from the moment a request is sent -// until its reply is received; the client, from then until it sends the next. -// The send and the receive are syscalls, which order the two sides, so honest -// peers never touch the window at once. A dishonest one races only bytes it -// could have written anyway: each side copies what it reads out of the window -// once, into memory of its own, and acts on that copy alone. Nothing here polls -// the window or waits on it, so it is copied as plain bytes — one fetch of each, -// as a volatile loop would be, and no reference is ever formed over it. +// **Who owns the window when.** The server owns a request's span from the +// moment it is sent until its reply is received; the client owns it from then +// until it sends another request over it. The send and the receive are +// syscalls, which order the two sides, so honest peers never touch one span +// at once. A dishonest one races only bytes it could have written anyway: +// each side copies what it reads out of the window once, into memory of its +// own, and acts on that copy alone. Nothing here polls the window or waits on +// it, so it is copied as plain bytes — one fetch of each, as a volatile loop +// would be, and no reference is ever formed over it. /// Copy `data` into `window` at `offset`. pub fn window_put(window: Window, offset: usize, data: &[u8]) { @@ -446,13 +572,20 @@ pub fn window_take(window: Window, offset: usize, out: &mut [u8]) { unsafe { core::ptr::copy_nonoverlapping(source.as_ptr(), out.as_mut_ptr(), out.len()) } } -/// What a file or directory is, and which: `volume` and `node` mean what -/// `toyos_abi::syscall::Stat`'s do, `volume` with [`SERVED_VOLUME`] set. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// What a file or directory is, and which. `volume` and `node` mean what +/// `toyos_abi::syscall::Stat`'s do, `volume` with [`SERVED_VOLUME`] set once +/// the client has it. A time is nanoseconds since the Unix epoch, and 0 one +/// the volume does not keep. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Stat { pub kind: u64, pub size: u64, + /// The names it has. + pub nlink: u64, + pub atime: u64, pub mtime: u64, + /// When what it is last changed: its contents, a name or an attribute. + pub ctime: u64, pub volume: u64, pub node: u64, } @@ -476,22 +609,93 @@ pub struct Written { pub offset: u64, } +/// A file id, and the connection it was answered on: [`Dir::generation`] +/// then. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Fid { + pub id: u64, + pub generation: u64, +} + /// An open answered. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Opened { - pub fid: u64, + pub fid: Fid, pub stat: Stat, - /// The connection this id belongs to: [`Dir::generation`] when it opened. - pub generation: u64, +} + +/// Where a path a request names is resolved from. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Base { + /// The capability's own directory, on whichever connection is current. + Root, + /// A directory this connection opened; with the empty path, the file or + /// directory the id names itself. + Fid(Fid), +} + +/// A time [`Dir::set_attr`] sets. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Time { + /// The server's clock as it sets it. + Now, + /// Nanoseconds since the Unix epoch. + At(u64), +} + +/// What [`Dir::set_attr`] changes; `None` leaves it. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Change { + pub size: Option, + pub atime: Option