Skip to main content

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}