Skip to main content

git_policy_cli/
fs_primitive_error.rs

1//! What:
2//!  The typed failure of one Linux filesystem primitive (`statfs`, a btrfs ioctl, `FICLONE`, `renameat2`):
3//!  which operation, on which path, and the operating system's error number.
4//! Why:
5//!  The primitives of the cheap-worktree ladder and the subvolume migration all fall through on failure, never panic;
6//!  the caller decides from the error number whether the next rung applies (the filesystem lacks the feature) or the
7//!  work must stop (a real fault), and the message names the operation and path a person can act on.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! class PrimitiveError extends Error { constructor(readonly operation: string, readonly path: string, readonly errno: number) { super(`...`); } }
12//! ```
13
14use std::ffi::CString;
15use std::fmt;
16use std::os::unix::ffi::OsStrExt;
17use std::path::{Path, PathBuf};
18
19/// What:
20///  One failed primitive call.
21///  `&'static str` is text compiled into the executable (sibling `String` is owned);
22///  `i32` is the C `errno` value (siblings `i64` and `u32` exist; the C API returns a 32-bit signed integer).
23/// Why:
24///   The operation names the call as the kernel documents it (for example `BTRFS_IOC_SNAP_CREATE_V2`), so a report
25///  can be searched in the kernel sources.
26///
27/// In TS you'd write (pseudocode):
28/// ```ts
29/// type PrimitiveError = { operation: string; path: string; errno: number };
30/// ```
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct PrimitiveError {
33    /// The call that failed, spelled as its kernel or libc name.
34    pub operation: &'static str,
35    /// The path the call was about.
36    pub path: PathBuf,
37    /// The `errno` the call left, 0 when the failure is not an operating-system error (a path with a NUL byte).
38    pub errno: i32,
39}
40
41impl PrimitiveError {
42    /// What:
43    ///  The error for the call that just failed, reading `errno` from the thread.
44    /// Why:
45    ///   `errno` is overwritten by the next library call, so it is read before anything else runs.
46    ///
47    /// In TS you'd write (pseudocode):
48    /// ```ts
49    /// const fail = (operation: string, path: string) => new PrimitiveError(operation, path, lastErrno());
50    /// ```
51    pub fn last_os_error(operation: &'static str, path: &Path) -> PrimitiveError {
52        let errno: i32 = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
53        return PrimitiveError { operation, path: path.to_path_buf(), errno };
54    }
55
56    /// What:
57    ///  Whether the filesystem lacks the feature, as opposed to a fault in this call.
58    /// Why:
59    ///   A ladder rung that gets `true` falls to the next rung; `false` is reported. Not a btrfs filesystem answers an
60    ///  ioctl with `ENOTTY`, a filesystem without reflinks answers `FICLONE` with `EOPNOTSUPP` or `EINVAL`, a clone
61    ///  across filesystems answers `EXDEV`, and an overlay over a layer without the feature answers `ENOSYS`.
62    ///
63    /// In TS you'd write (pseudocode):
64    /// ```ts
65    /// const lacksFeature = (e: PrimitiveError) => [ENOTTY, EOPNOTSUPP, EINVAL, EXDEV, ENOSYS].includes(e.errno);
66    /// ```
67    pub fn means_feature_missing(&self) -> bool {
68        return [libc::ENOTTY, libc::EOPNOTSUPP, libc::EINVAL, libc::EXDEV, libc::ENOSYS].contains(&self.errno);
69    }
70}
71
72/// Prints the call, the path and the system's own words for the error number.
73impl fmt::Display for PrimitiveError {
74    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75        return write!(
76            formatter,
77            "{} on {} failed: {} (errno {})",
78            self.operation,
79            self.path.display(),
80            std::io::Error::from_raw_os_error(self.errno),
81            self.errno
82        );
83    }
84}
85
86/// A primitive error is an error value.
87impl std::error::Error for PrimitiveError {}
88
89/// What:
90///  A path as the NUL-terminated C string the system calls take.
91/// Why:
92///   A path with an interior NUL byte can name no file, so it fails with `EINVAL` before any call, instead of
93///  reaching a system call with a truncated name.
94///
95/// In TS you'd write (pseudocode):
96/// ```ts
97/// function cPath(operation: string, path: string): Buffer { if (path.includes('\0')) throw new PrimitiveError(operation, path, EINVAL); return Buffer.from(path + '\0'); }
98/// ```
99pub fn c_path(operation: &'static str, path: &Path) -> Result<CString, PrimitiveError> {
100    return CString::new(path.as_os_str().as_bytes())
101        .map_err(|_| { return PrimitiveError { operation, path: path.to_path_buf(), errno: libc::EINVAL }; });
102}
103
104/// Primitive error controls stay out of the release executable.
105#[cfg(test)]
106#[path = "fs_primitive_error_tests.rs"]
107mod tests;