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;