1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
# API and ownership
The public entry point is `src/root.zig`. `Msg` covers every base request and
response, excluding the reserved, nonexistent Terror. Decode returns borrowed
strings/data. Encode checks complete output size before writing; input payloads
may already occupy their exact final position in the output frame, but arbitrary
input/output overlap is not supported. Stat uses the protocol's inner length;
Rstat and Twstat add and validate the outer length separately.
`Client` supports 16 ordinary outstanding requests and one additional flush.
Replies can arrive out of order. Tags remain reserved while flushes are pending,
even when the original request has already completed. Multiple flushes and
flushing a flush are accounted for. A completed result's slices remain valid
until the next `take()` or `hangup()`. `push()` only appends and never compacts.
Submit copies request payloads into the output buffer. An iounit returned by an
open/create is per fid; the caller must apply it when selecting atomic I/O sizes.
`maxRead()` and `maxWrite()` describe frame limits, not that per-fid guarantee.
Renegotiation requires an empty client request window.
`Server` supports 64 ordinary outstanding requests and one additional flush.
Receive errors terminate the connection. A full request table produces NoTags;
output backpressure instead returns null without consuming the next request.
`receive()` yields at most one frame. `release()` ends its borrow and advances
input. A backend retaining any request data must copy it before release.
A successful reply copies output and frees the request tag. An output-capacity
error leaves that tag pending so the backend can drain and retry.
The server validates transaction structure, not filesystem policy. Backends
own fid maps, authentication state, file handles, access modes, complete directory
records, atomic wstat, and cancellations. They must retire canceled work before
reusing its tag; a tag alone cannot distinguish a stale backend completion from a
new request. A version event requires backend cancellation and fid cleanup before
`negotiate()`. A partially sent response drains before a new version is accepted.
An Rflush promises no later reply to oldtag. Replying Rerror to a flush is refused.
Caller-provided input/output buffers must be disjoint and remain stable for the
session. No protocol API takes an allocator. Server and client request tables are
fixed-size. Copies and work per frame are bounded by buffer size and fixed table
capacities; transports and application backends can have their own allocations.
# Transports
`transport.readFrame` and `writeFrame` adapt `std.Io.Reader` and `std.Io.Writer`.
The writer remains buffered until its owner flushes it. Reader errors leave the
stream unsuitable for further frame processing; close the connection.
`transport.connect` and `listen` use `std.Io.net` for TCP and Unix streams. The
POSIX `connectFd`, `listenFd`, `acceptFd`, `read`, `write`, and `wait` APIs fit
caller-owned poll loops. They configure nonblocking descriptors and close-on-exec;
read/write return null for retry, and read returns zero for EOF. `wait` takes an
absolute monotonic deadline. The application closes descriptors, limits its
connections, selects addresses and deadlines, and manages Unix path permissions
and removal. No mounting or namespace policy is performed.
`http.WebSocket` adapts established HTTP streams without owning their sockets
or buffers. `http.Client` negotiates HTTP/1.1 upgrades using `std.http.Client`,
including its certificate-verified HTTPS support. `http.Bridge` relays between
an accepted WebSocket and arbitrary standard readers/writers, including UART
adapters. The runnable application's web UI, device leases, deadlines, and
origin policy remain in `web/`. See [HTTP ownership and usage](http.md).
`Quic(OpenSSL, alpn)` is an optional OpenSSL 3.6+ adapter with a single ordered
bidirectional stream per connection. It preserves the previous Pardes transport:
ephemeral self-signed certificates, no peer authentication. Applications needing
identity verification must provide that policy before using it across a trust
boundary. OpenSSL allocations and handshake costs are outside the allocation-free
protocol core. Accepted connections must close before their shared listener.
# File server engine
`fs.Server(Backend, Options)` is a file server built on `Server`: it owns the
fid table, permission checks, the directory-read cursor, flush and hangup, and
asks a backend only for filesystem operations. The backend contract is
`fs.Req` (`Op`: lookup, getattr, setattr, open, read, write, release, readdir;
each names a node, and open/read/write/readdir/release carry the handle open
returned) answered by `fs.Reply` (`Status`, an errno from `fs.E`, `fs.Attr`,
the open handle, the written count) with a read's or readdir's bytes passed
beside the reply. `fs.ReplyWith(Payload)` is the same reply carrying an
application-defined locator for those bytes, which the engine ignores; a
backend declares `Req` and `Reply` as those types. A readdir answers records
of `node:u64le dir:u8 len:u8 name`, which the engine encodes into whole Stat
entries directly in the output frame.
Every 9P request is a job: attach, walk, open, read, readdir, write, clunk,
remove, stat and wstat become one or more backend requests issued in order
(a walk asks one lookup per element; an open with OTRUNC asks setattr then
open; a clunk of an open fid asks release). `next()` yields the next backend
request and null while one is outstanding, the output is full, or no whole
frame has arrived; the backend answers by the request's tag through
`reply()`, at once or later. Tags are unique per connection and never zero.
A stale tag (flushed, hung up) is ignored; the backend must retire canceled
work before answering.
A read, readdir or write may answer `Status.again`: the job parks in a slot,
the engine moves on, and `retry()` re-issues each parked request once per
round, oldest first, until it completes; a parked write keeps a copy of its
data up to `park_data_max` bytes. Any other operation answering `again`, or
a park with no free slot, fails with EAGAIN. Tflush answers a parked
request's tag with EINTR before its Rflush and drops the slot; a flush of an
unknown tag is just Rflush. `hangup()` marks every open fid orphaned and
`next()` then yields the release each one owes before anything else, so a
dropped connection still pays the backend; a Tversion does the same before
negotiating.
Every table is sized at comptime by `fs.Options`: `fid_capacity` (256),
`slot_capacity` (32), `park_data_max` (128), `name_capacity` (28, at most
255; zero keeps no names, a stat's name is then the one its getattr
answers and the backend judges name lengths), `username_capacity` (28) and
`fid_index` (off: an open-addressing index from fid number to slot, two
bytes per bucket, for tables of thousands of fids; `InitOptions.seed`
salts it); the defaults are the editor's, except the name capacity, which
is the board's. `msize_min` (217) is the smallest negotiable msize, one
full Rwalk. Auth and attaching a named tree are refused, and so are
create, remove and any wstat other than a zero-length truncation unless
the backend declares the feature: a backend may carry `pub const features:
fs.Features` naming `create` (Tcreate becomes an `open` with `create`,
`data` the name, `perm` and `omode`, answered with the new node's `attr`
and handle), `remove` (Tremove, and ORCLOSE on clunk or hangup, become a
`release` with `remove`; its error is the Rremove's), `wstat` (a Twstat
becomes a `setattr` whose `set` names the changed fields: `data` the new
name, `perm`, `mtime`, `length`; the fields the engine owns must be "don't
care") and `references` (every lookup result, "." and a fid clone
included, is a reference the backend gets exactly one `release` for, with
`opened` telling whether an open handle goes with it). An `Attr` may add
a qid `version`, `atime`, a qid `path` distinct from the node, and the
append-only and exclusive-use bits; a reply's `ename` replaces
`errString(errno)` as the Rerror text. Rerror strings the engine emits on
its own are the ones Linux v9fs maps back to errnos (`fs.errString`). The
engine allocates nothing and makes no OS calls; a session is `push()`,
`retry()`, `next()`, `reply()`, `output()`, `wrote()`; `fidCount()` counts
the fids held.
# Related programs
Programs built on the library ship from this repository as `cloud9/<name>/`,
sibling directories of `src/` with `9`-prefixed names (`web/` builds `9web`;
`9proc/` and `9ns/` build `9proc-demo` and `9ns`), never inside it: each has
its own `src/`, `test/`, `docs/`, README and a `build.zig` fragment
(`pub fn add(b, ctx)`) that the root `build.zig` imports, passes the resolved
target, optimize mode and the `cloud9` module to, and enables with a
`-D<name>` toggle. Fragments register namespaced steps (`<name>`,
`<name>-test`, ...), never call `standardTargetOptions`, and use root-relative
`b.path("<name>/...")`. The library keeps its contract: mounting, namespaces,
threads, allocation and process policy stay in the program. `9proc` is
also exported as a module next to `cloud9` for dependents. New related
programs follow this layout.
# Pardes integration
Pardes consumes the sibling package through build.zig.zon. Its control tree
(`src/ninep/`) is an `fs.Server` backend using the contract types above; its
`src/9p.zig` names the editor's and the board's `fs.Options`.
`src/9p_io.zig` retains mounting, discovery, the editor's event-loop scheduling,
connection limits, and error presentation. Protocol bytes, session validation,
the file-server engine, and TCP/Unix/QUIC transport implementation come from
cloud9. Pardes selects the existing `pardes-9p` ALPN for compatibility.
The separate `05-zig-p4` firmware build adds cloud9 to the GPIO application's
module map. UART hardware access remains in the board firmware. No hardware was
flashed by this change.
|