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