zerofs_client/types.rs
1use ninep_proto::{Rstatfs, Stat};
2use std::time::{Duration, SystemTime, UNIX_EPOCH};
3
4/// Options for [`crate::Client::connect_with`]. All defaults are
5/// literal-expressible so uniffi record defaults can reproduce them in every
6/// binding; `None` identity fields are resolved Rust-side (euid/egid/`$USER`)
7/// inside connect.
8///
9/// There is deliberately no per-operation timeout: bound waits with your
10/// language's async facilities (`tokio::time::timeout`, `asyncio.wait_for`,
11/// `withTimeout`, `Promise.race`); every public future is cancel-safe.
12#[derive(Clone, Debug)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct ConnectOptions {
15 /// Numeric uid asserted at attach (`None` = process euid); the server
16 /// enforces permissions as this user.
17 pub uid: Option<u32>,
18 /// Group assigned to files/directories created through this client
19 /// (`None` = process egid).
20 pub gid: Option<u32>,
21 /// Username string sent at attach (`None` = `$USER`, else the uid rendered
22 /// as text); informational; `uid` is authoritative.
23 pub uname: Option<String>,
24 /// Attach name (export selector); empty selects the default export.
25 pub aname: String,
26 /// Requested 9P message size; the negotiated value appears in [`Capabilities`].
27 pub msize: u32,
28 /// Bound on the initial connect+attach; expiry surfaces as
29 /// [`crate::ZeroFsError::ConnectFailed`]. `None` = wait indefinitely.
30 pub connect_timeout_ms: Option<u32>,
31}
32
33impl Default for ConnectOptions {
34 fn default() -> Self {
35 Self {
36 uid: None,
37 gid: None,
38 uname: None,
39 aname: String::new(),
40 msize: 1024 * 1024,
41 connect_timeout_ms: Some(30_000),
42 }
43 }
44}
45
46/// Live snapshot of negotiated session properties. msize and the extension
47/// level are re-negotiated on every transparent reconnect; treat this as
48/// advisory at the instant of the call, not a constant for the life of the
49/// client.
50#[derive(Clone, Copy, Debug)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub struct Capabilities {
53 /// ZeroFS v1 extensions active: one-round-trip lookup+stat and readdirplus.
54 pub extensions_v1: bool,
55 /// ZeroFS v2 extensions active: stat-carrying create/open/setattr fast paths.
56 pub extensions_v2: bool,
57 /// Negotiated 9P message size in bytes.
58 pub msize: u32,
59 /// Largest single-message read payload (larger reads chunk transparently).
60 pub max_read_chunk: u32,
61 /// Largest single-message write payload (larger writes chunk transparently).
62 pub max_write_chunk: u32,
63}
64
65/// Plain record so it crosses FFI as a dictionary; constructors are Rust sugar.
66/// All field defaults are literals (bools false, mode 420 = 0o644), so
67/// keyword-style construction in the bindings does the right thing. There is
68/// deliberately no `append` flag: the server ignores open flags on writes, so
69/// appending is the explicit [`crate::Client::append`] composition.
70#[derive(Clone, Copy, Debug)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct OpenOptions {
73 /// Open for reading.
74 pub read: bool,
75 /// Open for writing.
76 pub write: bool,
77 /// Open-or-create. Not a single wire op: composed as open, falling back to
78 /// exclusive create, retrying on race.
79 pub create: bool,
80 /// Fail with `AlreadyExists` unless this call creates the file. This is
81 /// the atomic primitive (server creates are natively exclusive).
82 pub create_new: bool,
83 /// Truncate to zero length on open. For a pre-existing file this is an
84 /// extra setattr round trip after the open, NOT atomic with it.
85 pub truncate: bool,
86 /// Permission bits when the open creates the file. Default 0o644.
87 pub mode: u32,
88}
89
90impl Default for OpenOptions {
91 fn default() -> Self {
92 Self {
93 read: false,
94 write: false,
95 create: false,
96 create_new: false,
97 truncate: false,
98 mode: 0o644,
99 }
100 }
101}
102
103impl OpenOptions {
104 /// `read` only.
105 pub fn read_only() -> Self {
106 Self {
107 read: true,
108 ..Self::default()
109 }
110 }
111
112 /// `write` only.
113 pub fn write_only() -> Self {
114 Self {
115 write: true,
116 ..Self::default()
117 }
118 }
119
120 /// `read` + `write`.
121 pub fn read_write() -> Self {
122 Self {
123 read: true,
124 write: true,
125 ..Self::default()
126 }
127 }
128
129 /// Builder-style toggle for `create`.
130 pub fn create(mut self, yes: bool) -> Self {
131 self.create = yes;
132 self
133 }
134
135 /// Builder-style toggle for `create_new`.
136 pub fn create_new(mut self, yes: bool) -> Self {
137 self.create_new = yes;
138 self
139 }
140
141 /// Builder-style toggle for `truncate`.
142 pub fn truncate(mut self, yes: bool) -> Self {
143 self.truncate = yes;
144 self
145 }
146
147 /// Builder-style setter for the creation mode.
148 pub fn mode(mut self, mode: u32) -> Self {
149 self.mode = mode;
150 self
151 }
152}
153
154/// File type derived from the mode/dirent type.
155#[derive(Clone, Copy, Debug, PartialEq, Eq)]
156#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
157pub enum FileType {
158 /// Regular file.
159 File,
160 /// Directory.
161 Dir,
162 /// Symbolic link.
163 Symlink,
164 /// Named pipe (FIFO).
165 Fifo,
166 /// Unix-domain socket node.
167 Socket,
168 /// Character device.
169 CharDevice,
170 /// Block device.
171 BlockDevice,
172 /// Unrecognized type.
173 Unknown,
174}
175
176impl FileType {
177 pub(crate) fn from_mode(mode: u32) -> Self {
178 match mode & libc::S_IFMT as u32 {
179 x if x == libc::S_IFREG as u32 => Self::File,
180 x if x == libc::S_IFDIR as u32 => Self::Dir,
181 x if x == libc::S_IFLNK as u32 => Self::Symlink,
182 x if x == libc::S_IFIFO as u32 => Self::Fifo,
183 x if x == libc::S_IFSOCK as u32 => Self::Socket,
184 x if x == libc::S_IFCHR as u32 => Self::CharDevice,
185 x if x == libc::S_IFBLK as u32 => Self::BlockDevice,
186 _ => Self::Unknown,
187 }
188 }
189
190 pub(crate) fn from_dt(dt: u8) -> Self {
191 match dt {
192 libc::DT_REG => Self::File,
193 libc::DT_DIR => Self::Dir,
194 libc::DT_LNK => Self::Symlink,
195 libc::DT_FIFO => Self::Fifo,
196 libc::DT_SOCK => Self::Socket,
197 libc::DT_CHR => Self::CharDevice,
198 libc::DT_BLK => Self::BlockDevice,
199 _ => Self::Unknown,
200 }
201 }
202}
203
204/// Nanosecond UNIX timestamp as explicit fields (predictable across all bindings).
205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
206#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
207pub struct Timestamp {
208 /// Whole seconds since the UNIX epoch (negative before it).
209 pub secs: i64,
210 /// Nanoseconds within the second, `0..1_000_000_000`.
211 pub nanos: u32,
212}
213
214impl From<SystemTime> for Timestamp {
215 fn from(t: SystemTime) -> Self {
216 match t.duration_since(UNIX_EPOCH) {
217 Ok(d) => Timestamp {
218 secs: d.as_secs() as i64,
219 nanos: d.subsec_nanos(),
220 },
221 // Pre-epoch: split so `secs + nanos/1e9` still equals the instant
222 // (the inverse of the decoding in `systime`).
223 Err(e) => {
224 let d = e.duration();
225 let (secs, nanos) = (d.as_secs() as i64, d.subsec_nanos());
226 if nanos == 0 {
227 Timestamp {
228 secs: -secs,
229 nanos: 0,
230 }
231 } else {
232 Timestamp {
233 secs: -secs - 1,
234 nanos: 1_000_000_000 - nanos,
235 }
236 }
237 }
238 }
239 }
240}
241
242impl From<Timestamp> for SystemTime {
243 fn from(t: Timestamp) -> Self {
244 systime(t)
245 }
246}
247
248/// A time to set: the server's current clock, or an explicit instant.
249#[derive(Clone, Copy, Debug)]
250#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
251pub enum SetTime {
252 /// Use the server's current clock.
253 Now,
254 /// Set to this explicit instant.
255 At {
256 /// The instant to set.
257 time: Timestamp,
258 },
259}
260
261impl From<SystemTime> for SetTime {
262 fn from(t: SystemTime) -> Self {
263 SetTime::At { time: t.into() }
264 }
265}
266
267/// Metadata changes; `None` fields are untouched. All-`None` is a no-op.
268#[derive(Clone, Copy, Debug, Default)]
269#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
270pub struct SetAttrs {
271 /// New permission bits (low 12 bits used).
272 pub mode: Option<u32>,
273 /// New owner uid.
274 pub uid: Option<u32>,
275 /// New owner gid.
276 pub gid: Option<u32>,
277 /// New length (truncates or extends).
278 pub size: Option<u64>,
279 /// New access time.
280 pub atime: Option<SetTime>,
281 /// New modification time.
282 pub mtime: Option<SetTime>,
283}
284
285/// Kind of special node for `mknod`; a tagged enum so callers never pack
286/// `S_IF*` bits or pass meaningless major/minor for fifos and sockets.
287#[derive(Clone, Copy, Debug)]
288#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
289pub enum NodeKind {
290 /// Named pipe (FIFO).
291 Fifo,
292 /// Unix-domain socket node.
293 Socket,
294 /// Block device with the given major/minor numbers.
295 BlockDevice {
296 /// Device major number.
297 major: u32,
298 /// Device minor number.
299 minor: u32,
300 },
301 /// Character device with the given major/minor numbers.
302 CharDevice {
303 /// Device major number.
304 major: u32,
305 /// Device minor number.
306 minor: u32,
307 },
308}
309
310/// POSIX-shaped attributes; plain data record everywhere.
311#[derive(Clone, Copy, Debug)]
312#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
313pub struct Metadata {
314 /// Stable inode number (ZeroFS never reuses inode ids).
315 pub ino: u64,
316 /// File type.
317 pub file_type: FileType,
318 /// Full st_mode (type bits + permission bits).
319 pub mode: u32,
320 /// Hard-link count.
321 pub nlink: u64,
322 /// Owner uid.
323 pub uid: u32,
324 /// Owner gid.
325 pub gid: u32,
326 /// Size in bytes.
327 pub size: u64,
328 /// Preferred I/O block size reported by the server.
329 pub block_size: u64,
330 /// Number of 512-byte blocks allocated.
331 pub blocks: u64,
332 /// Device id for char/block nodes; 0 otherwise.
333 pub rdev: u64,
334 /// Last access time.
335 pub atime: Timestamp,
336 /// Last modification time.
337 pub mtime: Timestamp,
338 /// Last status-change time.
339 pub ctime: Timestamp,
340 /// RESERVED. The wire carries a creation time, but current ZeroFS servers
341 /// always report 0 (the epoch). Do not display as a real birth time yet.
342 pub btime: Timestamp,
343 /// RESERVED. The wire carries a content-change counter, but current ZeroFS
344 /// servers always report 0; do NOT build cache invalidation on it. For
345 /// change detection today, use `mtime`.
346 pub data_version: u64,
347}
348
349impl Metadata {
350 pub(crate) fn from_stat(st: &Stat) -> Self {
351 let ts = |secs: u64, nanos: u64| Timestamp {
352 secs: secs as i64,
353 nanos: nanos as u32,
354 };
355 Self {
356 ino: st.qid.path,
357 file_type: FileType::from_mode(st.mode),
358 mode: st.mode,
359 nlink: st.nlink,
360 uid: st.uid,
361 gid: st.gid,
362 size: st.size,
363 block_size: st.blksize,
364 blocks: st.blocks,
365 rdev: st.rdev,
366 atime: ts(st.atime_sec, st.atime_nsec),
367 mtime: ts(st.mtime_sec, st.mtime_nsec),
368 ctime: ts(st.ctime_sec, st.ctime_nsec),
369 btime: ts(st.btime_sec, st.btime_nsec),
370 data_version: st.data_version,
371 }
372 }
373
374 /// True if this is a regular file.
375 pub fn is_file(&self) -> bool {
376 self.file_type == FileType::File
377 }
378
379 /// True if this is a directory.
380 pub fn is_dir(&self) -> bool {
381 self.file_type == FileType::Dir
382 }
383
384 /// True if this is a symbolic link.
385 pub fn is_symlink(&self) -> bool {
386 self.file_type == FileType::Symlink
387 }
388
389 /// Permission bits only.
390 pub fn permissions(&self) -> u32 {
391 self.mode & 0o7777
392 }
393
394 /// `mtime` as a `SystemTime` (Rust-only sugar).
395 pub fn modified(&self) -> SystemTime {
396 systime(self.mtime)
397 }
398
399 /// `atime` as a `SystemTime` (Rust-only sugar).
400 pub fn accessed(&self) -> SystemTime {
401 systime(self.atime)
402 }
403}
404
405fn systime(t: Timestamp) -> SystemTime {
406 // Defend against out-of-range / un-normalized wire data: clamp sub-second
407 // nanos and saturate rather than panic on an unrepresentable instant.
408 let nanos = t.nanos.min(999_999_999);
409 if t.secs >= 0 {
410 UNIX_EPOCH
411 .checked_add(Duration::new(t.secs as u64, nanos))
412 .unwrap_or(UNIX_EPOCH)
413 } else {
414 let base = UNIX_EPOCH
415 .checked_sub(Duration::new(t.secs.unsigned_abs(), 0))
416 .unwrap_or(UNIX_EPOCH);
417 base.checked_add(Duration::new(0, nanos)).unwrap_or(base)
418 }
419}
420
421/// Filesystem usage, from 9P statfs.
422#[derive(Clone, Copy, Debug)]
423#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
424pub struct StatFs {
425 /// Optimal transfer block size.
426 pub block_size: u32,
427 /// Total data blocks in the filesystem.
428 pub blocks: u64,
429 /// Free blocks.
430 pub blocks_free: u64,
431 /// Free blocks available to unprivileged users.
432 pub blocks_available: u64,
433 /// Total inodes (files).
434 pub files: u64,
435 /// Free inodes.
436 pub files_free: u64,
437 /// Filesystem id.
438 pub filesystem_id: u64,
439 /// Maximum filename length.
440 pub max_name_len: u32,
441}
442
443impl StatFs {
444 pub(crate) fn from_wire(r: &Rstatfs) -> Self {
445 Self {
446 block_size: r.bsize,
447 blocks: r.blocks,
448 blocks_free: r.bfree,
449 blocks_available: r.bavail,
450 files: r.files,
451 files_free: r.ffree,
452 filesystem_id: r.fsid,
453 max_name_len: r.namelen,
454 }
455 }
456}
457
458/// One directory entry; the library filters out `.` and `..`.
459#[derive(Clone, Debug)]
460#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
461pub struct DirEntry {
462 /// Name decoded as UTF-8 (lossy: invalid bytes become U+FFFD).
463 pub name: String,
464 /// Exact on-wire name bytes; authoritative, feed into the `Dir::*_at`
465 /// methods verbatim.
466 pub name_bytes: Vec<u8>,
467 /// True when `name` losslessly round-trips to `name_bytes`.
468 pub name_is_utf8: bool,
469 /// Entry type, known without a stat.
470 pub file_type: FileType,
471 /// Stable inode number (ZeroFS never reuses inode ids).
472 pub ino: u64,
473 /// Full metadata when readdirplus is negotiated (v1+); `None` on plain
474 /// 9P2000.L.
475 pub metadata: Option<Metadata>,
476}
477
478impl DirEntry {
479 pub(crate) fn from_plus(e: &ninep_proto::DirEntryPlus) -> Self {
480 let name_bytes = e.name.data.clone();
481 Self {
482 name: String::from_utf8_lossy(&name_bytes).into_owned(),
483 name_is_utf8: std::str::from_utf8(&name_bytes).is_ok(),
484 file_type: FileType::from_mode(e.stat.mode),
485 ino: e.qid.path,
486 metadata: Some(Metadata::from_stat(&e.stat)),
487 name_bytes,
488 }
489 }
490
491 pub(crate) fn from_plain(e: &ninep_proto::DirEntry) -> Self {
492 let name_bytes = e.name.data.clone();
493 Self {
494 name: String::from_utf8_lossy(&name_bytes).into_owned(),
495 name_is_utf8: std::str::from_utf8(&name_bytes).is_ok(),
496 file_type: FileType::from_dt(e.type_),
497 ino: e.qid.path,
498 metadata: None,
499 name_bytes,
500 }
501 }
502}