Expand description
Async, path-based client for a ZeroFS server, speaking 9P2000.L (plus the ZeroFS fast-path extensions, used automatically when the server offers them) over TCP or a unix socket.
The primary surface is one-shot path operations on a shared Client (
read, write, stat, rename, mkdir), with File and Dir handles only
where statefulness pays (chunked I/O, incremental listing, openat-style
child operations). Paths are bytes, as on POSIX and the 9P wire: every
path parameter is impl AsRef<Path>, so non-UTF-8 names need nothing
special. The API otherwise stays FFI-friendly (owned buffers, a flat
exhaustive error enum, Arc handles, explicit close, cancel-safe
futures) for the zerofs-ffi bindings layer built on top.
This is a trusted-network client: you assert your uid at connect time, NFS-style.
use zerofs_client::{Client, OpenOptions};
let fs = Client::connect("unix:/tmp/zerofs.9p.sock").await?;
fs.create_dir_all("/projects/demo", 0o755).await?;
fs.write("/projects/demo/hello.txt", b"hello from zerofs").await?;
let data = fs.read("/projects/demo/hello.txt").await?;
assert_eq!(&data[..], b"hello from zerofs");
for entry in fs.read_dir("/projects/demo").await? {
let size = entry.metadata.as_ref().map_or(0, |m| m.size);
println!("{size:>8} {}", entry.name);
}
let file = fs
.open("/projects/demo/big.bin", OpenOptions::read_write().create(true))
.await?;
file.write_at(0, &vec![0u8; 4 << 20]).await?;
file.sync_all().await?;
file.close().await;§Lifecycles
Close & drop. close() marks the handle closed immediately (later calls
return ZeroFsError::Closed); it always succeeds, is idempotent, and never
hangs. A handle’s server-side fid is released and its number recycled when
the handle is dropped (for scope-bound use, right after close()); the
janitor performs the clunk in the background. Dropping a handle you never
closed does the same. Fid numbers are always reused, so a long-running client
never exhausts them.
Cancellation. Every public future is cancel-safe: dropping it at any
await point leaks nothing (in-flight fids are reclaimed in the background).
There is deliberately no per-operation timeout parameter; bound waits with
tokio::time::timeout.
Connection loss. The underlying session reconnects forever with backoff and replays its state; while the server is unreachable, calls block rather than fail. An operation in flight at the instant of a disconnect is resent, so a non-idempotent op (rename, create, unlink) can apply twice across a reconnect.
Re-exports§
pub use stream::DirStream;
Modules§
- io
tokio-iofeature: aFileCursoradapter that turns the positionedFileAPI into a statefulAsyncRead + AsyncWrite + AsyncSeekstream, sotokio::io::copyand friends work against a ZeroFS file. Rust-only; never crosses the FFI boundary.- stream
streamfeature: afutures_core::Streamadapter overDirlisting, so a directory can be consumed withStreamExt(.next(),.collect(),try_for_each, …). Rust-only; never crosses the FFI boundary.
Structs§
- Bytes
- Re-exported so callers need not depend on
bytesdirectly; it is the return type of every read (cheap to clone and slice, derefs to&[u8]). A cheaply cloneable and sliceable chunk of contiguous memory. - Capabilities
- Live snapshot of negotiated session properties. msize and the extension level are re-negotiated on every transparent reconnect; treat this as advisory at the instant of the call, not a constant for the life of the client.
- Client
- One ZeroFS session, one identity. Share via
Arc; every method takes&selfand is safe to call concurrently. The underlying connection reconnects transparently, blocking calls through outages; bound waits with your async runtime’s timeout facilities (every future is cancel-safe). - Connect
Options - Options for
crate::Client::connect_with. All defaults are literal-expressible so uniffi record defaults can reproduce them in every binding;Noneidentity fields are resolved Rust-side (euid/egid/$USER) inside connect. - Dir
- An open directory: a pull-based listing cursor plus at-style child
operations taking ONE byte-exact name component (no
/or NUL). The*_atsuite is the FFI-clean escape hatch for non-UTF-8 names discovered viaDirEntry::name_bytes; chainDir::open_dir_atto reach arbitrary depth. - DirEntry
- One directory entry; the library filters out
.and... - File
- An open file. All I/O is positioned (pread/pwrite); there is no shared
cursor, so an
Arc<File>is safe to use from many tasks at once. - Metadata
- POSIX-shaped attributes; plain data record everywhere.
- Open
Options - Plain record so it crosses FFI as a dictionary; constructors are Rust sugar.
All field defaults are literals (bools false, mode 420 = 0o644), so
keyword-style construction in the bindings does the right thing. There is
deliberately no
appendflag: the server ignores open flags on writes, so appending is the explicitcrate::Client::appendcomposition. - SetAttrs
- Metadata changes;
Nonefields are untouched. All-Noneis a no-op. - StatFs
- Filesystem usage, from 9P statfs.
- Timestamp
- Nanosecond UNIX timestamp as explicit fields (predictable across all bindings).
Enums§
- File
Type - File type derived from the mode/dirent type.
- Node
Kind - Kind of special node for
mknod; a tagged enum so callers never packS_IF*bits or pass meaningless major/minor for fifos and sockets. - SetTime
- A time to set: the server’s current clock, or an explicit instant.
- Zero
FsError - The single error type: flat and exhaustive (deliberately NOT
#[non_exhaustive], sozerofs-ffican apply uniffi remote derives and match exhaustively; a new variant must break its build, not silently degrade to a catch-all).