3.1. fs:: - file system
The rok::std::fs module provides operations across the file system: files, directories, links, permissions and so on. The design starts from the capability-based model of WASI Preview 2; as a result, every op is performed only through a Dir handle.
3.1.0. Argument rules
- The path parameter
Pathin the signatures below is shorthand forP: AsPath. It accepts bothstrandPath(3.8.3a). - The data parameter
dataof write functions acceptsio::AsBytes(3.5).str,List<u8>andSlice<u8>can be passed.
The writing side accepts AsBytes; the read buffer is a List<u8> (filled by read(buf)).
3.1.1. Core types
The core types are Dir and File, which wrap OS handles; ReadDir and DirEntry for iterating directories; and Metadata and Permissions for metadata and permissions. How a file is opened is controlled in detail with the OpenOptions struct.
struct Dir { /* OS handle (file descriptor / HANDLE) */ }
struct File { /* OS handle */ }
struct ReadDir { /* iterator over Dir entries - Drop: closed when using releases it */ }
struct DirEntry { /* path + metadata cache */ }
struct Metadata { /* size, times, perm, ... */ }
struct Permissions { /* readable/writable/executable + mode bits */ }
struct OpenOptions {
read: bool;
write: bool;
append: bool;
truncate: bool;
create: bool;
create_new: bool;
mode: u32; // unix permission (default 0o666)
}
3.1.2. Dir capability
Dir is the unit of capability. Besides root(), the process default cap, the working directory, home and the OS temp directory are available as cwd(), home() and temp(). With a handle in hand, nearly every file system operation can be performed beneath it - opening files, creating, removing and renaming directories - and copies automatically use reflink paths such as copy_file_range / FICLONE / clonefile.
impl Dir {
fn root() -> Dir; // process default cap
fn cwd() throws (IoError) -> Dir; // current working dir
fn home() throws (IoError) -> Dir;
fn temp() throws (IoError) -> Dir; // OS temp dir
fn open(self, path: Path) throws (IoError) -> File; // open with default OpenOptions::read
fn open_with(self, path: Path, opts: OpenOptions) throws (IoError) -> File;
fn open_dir(self, path: Path) throws (IoError) -> Dir;
fn create(self, path: Path) throws (IoError) -> File; // open + create + truncate
fn read_dir(self) throws (IoError) -> ReadDir; // bound with using, closed on release; dropped via let, stays open until process exit
fn metadata(self, path: Path) throws (IoError) -> Metadata;
fn exists(self, path: Path) -> bool;
fn create_dir(self, path: Path) throws (IoError);
fn create_dir_all(self, path: Path) throws (IoError);
fn remove_file(self, path: Path) throws (IoError);
fn remove_dir(self, path: Path) throws (IoError);
fn remove_dir_all(self, path: Path) throws (IoError);
fn rename(self, from: Path, to: Path) throws (IoError);
fn copy(self, from: Path, to: Path) throws (IoError) -> u64;
// tries reflink first (macOS: clonefileat), falls back to byte copy
fn symlink(self, target: Path, link: Path) throws (IoError);
fn hardlink(self, target: Path, link: Path) throws (IoError);
}
Internally, Dir uses the openat family. A Dir other than Dir::root() rejects absolute paths and paths containing .., so access is confined beneath that directory. This check is on the path text, so following a symlink inside the directory can lead outside it. When full isolation is required, use the OS sandbox as well.
3.1.3. File methods
Besides ordinary read / write, File has read_to_str with UTF-8 validation, sync_all / sync_data for syncing to disk, set_len for adjusting length, and so on. Methods for querying and changing metadata and permissions are provided, and seek and try_clone allow fd-level manipulation. The file locking API stabilized in Rust 1.77 (2024) and memory mapping (mmap) are also provided.
impl File {
fn read(self, buf: List<u8>) throws (IoError) -> usize; // fills buf[0..n], 0 = end
fn read_to_end(self) throws (IoError) -> List<u8>;
fn read_to_str(self) throws (IoError) -> str; // includes UTF-8 validation
fn write<B: AsBytes>(self, data: B) throws (IoError) -> usize;
fn write_all<B: AsBytes>(self, data: B) throws (IoError);
fn close(self) throws (IoError);
fn flush(self) throws (IoError);
fn sync_all(self) throws (IoError); // fsync (data + metadata)
fn sync_data(self) throws (IoError); // fdatasync
fn set_len(self, size: u64) throws (IoError); // truncate
fn metadata(self) throws (IoError) -> Metadata;
fn permissions(self) throws (IoError) -> Permissions;
fn set_permissions(self, perm: Permissions) throws (IoError);
fn seek(self, pos: SeekFrom) throws (IoError) -> u64;
fn try_clone(self) throws (IoError) -> File; // dup fd
// file locking (Rust 1.77 stabilized, 2024)
fn lock_exclusive(self) throws (IoError); // flock LOCK_EX / LockFileEx
fn lock_shared(self) throws (IoError); // flock LOCK_SH
fn try_lock_exclusive(self) throws (IoError) -> bool;
fn unlock(self) throws (IoError);
// memory mapping (mmap)
unsafe fn mmap(self, len: usize, offset: u64) throws (IoError) -> Mmap;
}
enum SeekFrom {
Start(u64),
End(i64),
Current(i64),
}
File implements Drop from 2.13 marker, so when bound with a using block it is closed on leaving the block. A file opened without using is not closed automatically; call close() directly. OS handles (File, Dir, sockets, mmap, a child's pidfd) are released once, however many copies of the value close them.
Mmap
unsafe fn File::mmap(len, offset) maps len bytes at offset (a multiple of the page size) as a read-only shared mapping. It is unsafe because another writer of the file changes the bytes under the mapping, and truncating the file makes reads fault.
struct Mmap { /* address, length */ }
impl Mmap {
fn len(self) -> usize; // 0 after unmap
fn is_empty(self) -> bool;
fn get(self, i: usize) -> u8 | none; // none past the end
fn slice(self, start: usize, end: usize) -> List<u8>; // copy; clamped to the mapping
fn to_list(self) -> List<u8>; // copy of every byte
fn unmap(self);
}
impl Drop for Mmap { ... } // unmaps
A mapping is unmapped by unmap or on drop (leaving using); reads after that see an empty mapping.
3.1.4. Convenience functions (on Dir::root())
So that a Dir handle need not be carried around everywhere, convenience functions operating on Dir::root() are provided. Everyday tasks such as simple reads and writes, metadata queries and existence checks take one line, and frequently needed patterns such as atomic writes and recursive traversal are exposed in the same place.
fn read(path: Path) throws (IoError) -> List<u8>;
fn read_to_str(path: Path) throws (IoError) -> str;
fn write<B: AsBytes>(path: Path, data: B) throws (IoError);
fn write_atomic<B: AsBytes>(path: Path, data: B) throws (IoError);
// tempfile + fsync + rename + fsync(parent)
fn metadata(path: Path) throws (IoError) -> Metadata;
fn exists(path: Path) -> bool;
fn create_dir(path: Path) throws (IoError);
fn create_dir_all(path: Path) throws (IoError);
fn remove_file(path: Path) throws (IoError);
fn remove_dir_all(path: Path) throws (IoError);
fn rename(from: Path, to: Path) throws (IoError);
fn copy(from: Path, to: Path) throws (IoError) -> u64;
fn walk(root: Path) -> Walk; // implements Iterator<Item = Result<DirEntry, IoError>>
Metadata
impl Metadata {
fn len(self) -> u64;
fn is_file(self) -> bool;
fn is_dir(self) -> bool;
fn is_symlink(self) -> bool;
fn permissions(self) -> Permissions;
fn modified(self) throws (IoError) -> SystemTime; // last modification time
fn accessed(self) throws (IoError) -> SystemTime; // last access time
fn mtime(self) -> i64; // Unix seconds (raw value)
fn atime(self) -> i64;
fn ctime(self) -> i64;
}
Times are obtained as SystemTime (3.3.3). mtime / atime / ctime return the OS's Unix seconds as is.
walk
walk traverses depth-first. It yields root itself first, and the entries in each directory sorted by name. Elements are Result<DirEntry, IoError>; unreadable entries come out as Err and traversal continues.
write_atomic - the standard POSIX pattern
write_atomic encapsulates the standard pattern of creating a temporary file, writing the data, fsync, rename, and fsync of the parent directory, all in one call.
try fs::write_atomic("config.toml", new_config_bytes);
// internal behavior:
// 1. tempfile create (.config.toml.tmp.<uuid>)
// 2. write data
// 3. fsync(temp)
// 4. rename(temp, config.toml) ← atomic
// 5. fsync(parent dir)
Every robust application needs this pattern; in Rust the
tempfilecrate and in Gorenameiohave filled the gap in std. rok provides the same functionality directly in std.
3.1.4a. Asynchronous file I/O (*_async)
The functions and methods above block the calling OS thread. To give up the worker while waiting inside a task, await the *_async version of the same operation (same naming rule as 3.2.0 net). An ordinary function cannot suspend a stackless coroutine, so a sync call does not become async on its own inside a task.
// convenience functions - all async, throws (IoError) except exists_async
async fn open_async(path: Path) throws (IoError) -> File;
async fn create_async(path: Path) throws (IoError) -> File;
async fn open_with_async(path: Path, opts: OpenOptions) throws (IoError) -> File;
async fn read_async(path: Path) throws (IoError) -> List<u8>;
async fn read_to_str_async(path: Path) throws (IoError) -> str;
async fn write_async<B: AsBytes>(path: Path, data: B) throws (IoError);
async fn write_atomic_async<B: AsBytes>(path: Path, data: B) throws (IoError);
async fn metadata_async(path: Path) throws (IoError) -> Metadata;
async fn symlink_metadata_async(path: Path) throws (IoError) -> Metadata;
async fn exists_async(path: Path) -> bool;
async fn create_dir_async(path: Path) throws (IoError);
async fn create_dir_all_async(path: Path) throws (IoError);
async fn remove_file_async(path: Path) throws (IoError);
async fn remove_dir_async(path: Path) throws (IoError);
async fn remove_dir_all_async(path: Path) throws (IoError);
async fn rename_async(from: Path, to: Path) throws (IoError);
async fn copy_async(from: Path, to: Path) throws (IoError) -> u64;
async fn symlink_async(target: Path, link: Path) throws (IoError);
async fn hardlink_async(target: Path, link: Path) throws (IoError);
async fn read_dir_async(path: Path) throws (IoError) -> List<DirEntry>;
// reads everything at once (eager); throws the first entry error, if any
impl File {
async fn open_async(path: Path) throws (IoError) -> File;
async fn create_async(path: Path) throws (IoError) -> File;
async fn read_async(self, buf: List<u8>) throws (IoError) -> usize;
async fn read_to_end_async(self) throws (IoError) -> List<u8>;
async fn read_to_str_async(self) throws (IoError) -> str;
async fn write_async<B: AsBytes>(self, data: B) throws (IoError) -> usize;
async fn write_all_async<B: AsBytes>(self, data: B) throws (IoError);
async fn write_str_async(self, s: str) throws (IoError);
async fn sync_all_async(self) throws (IoError);
async fn sync_data_async(self) throws (IoError);
async fn set_len_async(self, size: u64) throws (IoError);
async fn metadata_async(self) throws (IoError) -> Metadata;
async fn lock_exclusive_async(self) throws (IoError);
async fn lock_shared_async(self) throws (IoError);
}
Dir also has a *_async version of every sync method.
impl Dir {
async fn cwd_async() throws (IoError) -> Dir;
async fn home_async() throws (IoError) -> Dir;
async fn temp_async() throws (IoError) -> Dir;
async fn open_async(self, path: Path) throws (IoError) -> File;
async fn create_async(self, path: Path) throws (IoError) -> File;
async fn open_with_async(self, path: Path, opts: OpenOptions) throws (IoError) -> File;
async fn open_dir_async(self, path: Path) throws (IoError) -> Dir;
async fn read_dir_async(self) throws (IoError) -> ReadDirStream;
async fn metadata_async(self, path: Path) throws (IoError) -> Metadata;
async fn symlink_metadata_async(self, path: Path) throws (IoError) -> Metadata;
async fn exists_async(self, path: Path) -> bool;
async fn create_dir_async(self, path: Path) throws (IoError);
async fn create_dir_all_async(self, path: Path) throws (IoError);
async fn remove_file_async(self, path: Path) throws (IoError);
async fn remove_dir_async(self, path: Path) throws (IoError);
async fn remove_dir_all_async(self, path: Path) throws (IoError);
async fn rename_async(self, from: Path, to: Path) throws (IoError);
async fn copy_async(self, from: Path, to: Path) throws (IoError) -> u64;
async fn symlink_async(self, target: Path, link: Path) throws (IoError);
async fn hardlink_async(self, target: Path, link: Path) throws (IoError);
async fn close_async(self) throws (IoError);
}
struct ReadDirStream { /* directory handle + batch read so far */ }
impl Stream for ReadDirStream { type Item = Result<DirEntry, IoError>; ... }
impl Drop for ReadDirStream { ... } // closes
The ReadDirStream of read_dir_async is a stream that reads entries 64 at a time on the blocking pool and yields them to for await. It closes the directory when read to the end or when leaving using. Unlike fs::read_dir_async(path) (List<DirEntry>), which reads everything at once, it reads large directories a little at a time.
let logs = try await Dir::root().open_dir_async("logs");
using entries = try await logs.read_dir_async() {
for await e in entries {
let entry = try e.get();
...
}
}
There is no dedicated append function. Open with open_with_async(path, OpenOptions::new().append(true)).
Behavior. Regular files have no readiness (they are always ready in kqueue / epoll), so each *_async call runs the blocking call of the same name on another OS thread (task::spawn_blocking) and suspends the task meanwhile. Results and errors are exactly those of the sync call.
Cancellation. If the waiting task is cancelled (2.18.4), a call that has already started runs to completion in the background and only its result is discarded. Side effects such as writes, deletions and renames therefore still happen.
async fn load(path: str) throws (IoError) -> Config {
let text = try await fs::read_to_str_async(path);
return try parse(text);
}
3.1.5. Examples
The following example walks through the typical flow, from simple reads to atomic writes, sandbox setup, recursive traversal and file locking for concurrent processes.
use rok::std::fs::*;
use rok::std::path::Path;
// simple reads
let bytes = try fs::read("data.txt");
let text = try fs::read_to_str("config.toml");
// safe write (atomic)
try fs::write_atomic("output.json", json_bytes);
// Dir capability - sandbox
let data_dir = try Dir::root().open_dir("data");
let f = try data_dir.open("a.txt"); // data/a.txt
let g = try data_dir.open("../etc/passwd"); // IoError - .. rejected (symlinks are not blocked)
// recursive traversal
for entry in fs::walk(Path::from("src")) {
let entry = try entry.get(); // element is a Result - get() returns to the error flow
if entry.path().extension() == "rok" {
process(entry.path());
}
}
// File locking (concurrent process)
using f = try fs::open_with(path, OpenOptions::default().write(true)) {
try f.lock_exclusive();
// critical section
} // ← automatic unlock + close
3.1.6. Module mapping / target availability
How each feature behaves on each target:
| Item | linux/macos/windows | wasi | Notes |
|---|---|---|---|
Dir/File basics | ✓ | ✓ | Natural fit for cap-based WASI |
write_atomic | ✓ | ✓ | Always available |
lock_exclusive | ✓ (flock/LockFileEx) | ✗ | Unsupported on wasm32 |
try_clone (dup) | ✓ | ✗ | Unsupported on wasm32 |
mmap | ✓ | ✗ | Unsupported on wasm32 |
copy reflink | ✓ (btrfs/APFS/XFS) | ✗ | automatic fallback to copy |
pidfd / pipe2 | linux only | ✗ | feature-gated |
walk | ✓ | ✓ |
In the browser (wasm32-unknown-unknown) every fs call reports Unsupported: the shipped JS host pre-opens / and refuses every call.