git_policy_cli/subvolume_error.rs
1//! What:
2//! The failure type of the subvolume migration and the test hook that simulates a kill.
3//! Why:
4//! Every step names what it did to which path and why it stopped, so the report can tell the person the cause, the
5//! state the journal holds, and the way forward. The interrupt hook stops a run at a named point exactly as a kill
6//! would (all state is on disk or in the journal; nothing else survives a kill), so the resume controls can stop at
7//! every journal step without killing the test process. Release code passes `Interrupt::never()`.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! class MigrateError extends Error {}
12//! const interrupt = { point: undefined as string | undefined, after: 0 }; // tests only
13//! ```
14
15use super::fs_primitive_error::PrimitiveError;
16use std::cell::Cell;
17use std::fmt;
18use std::path::PathBuf;
19
20/// What:
21/// Why a migration step stopped.
22/// `String` is owned text; `PathBuf` is an owned path.
23/// Why:
24/// Each variant has its own wording and its own remedy in the report.
25///
26/// In TS you'd write (pseudocode):
27/// ```ts
28/// type MigrateError = { kind: 'io'; ... } | { kind: 'primitive'; ... } | { kind: 'unreadable-directory'; ... } | ...;
29/// ```
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub enum MigrateError {
32 /// An ordinary file operation failed.
33 Io {
34 /// What was being done, in words.
35 action: String,
36 /// The path involved.
37 path: PathBuf,
38 /// The operating system's words.
39 reason: String,
40 },
41 /// A kernel primitive failed (ioctl, clone, exchange).
42 Primitive(PrimitiveError),
43 /// A directory this user cannot list or search; the migration stops instead of leaving its subtree out.
44 UnreadableDirectory {
45 /// The directory.
46 path: PathBuf,
47 /// The operating system's words.
48 reason: String,
49 },
50 /// The journal file is unreadable or has an unknown shape.
51 Journal(String),
52 /// The run stopped at the named test point.
53 Interrupted(String),
54}
55
56impl MigrateError {
57 /// What:
58 /// An I/O failure with the action and path, from a `std::io::Error`.
59 /// Why:
60 /// One constructor keeps every message in the same shape.
61 ///
62 /// In TS you'd write (pseudocode):
63 /// ```ts
64 /// const io = (action: string, path: string, error: Error) => new MigrateError(...);
65 /// ```
66 pub fn io(action: &str, path: &std::path::Path, error: &std::io::Error) -> MigrateError {
67 return MigrateError::Io { action: action.to_string(), path: path.to_path_buf(), reason: error.to_string() };
68 }
69}
70
71/// Prints the cause in plain words.
72impl fmt::Display for MigrateError {
73 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 MigrateError::Io { action, path, reason } => return write!(formatter, "cannot {action} {}: {reason}", path.display()),
76 MigrateError::Primitive(error) => return write!(formatter, "{error}"),
77 MigrateError::UnreadableDirectory { path, reason } => {
78 return write!(
79 formatter,
80 "the directory {} cannot be listed or searched by this user ({reason}); copying around it would leave its content out of the new checkout",
81 path.display()
82 );
83 }
84 MigrateError::Journal(message) => return write!(formatter, "{message}"),
85 MigrateError::Interrupted(point) => return write!(formatter, "stopped at the test point {point}"),
86 }
87 }
88}
89
90/// A migration error is an error value.
91impl std::error::Error for MigrateError {}
92
93/// A primitive failure is a migration failure.
94impl From<PrimitiveError> for MigrateError {
95 fn from(error: PrimitiveError) -> MigrateError {
96 return MigrateError::Primitive(error);
97 }
98}
99
100/// What:
101/// The simulated kill: stop the run after a named point has been reached a number of times.
102/// `Cell<u64>` is a number that can change behind a shared borrow (TS has no distinction; everything mutates).
103/// Why:
104/// A control sets the point, runs, sees the `Interrupted` error, and runs again with `Interrupt::never()`.
105///
106/// In TS you'd write (pseudocode):
107/// ```ts
108/// class Interrupt { check(point: string): void { if (point === this.point && ++this.hits > this.after) throw ...; } }
109/// ```
110#[derive(Debug)]
111pub struct Interrupt {
112 /// The point name and how many earlier hits to let pass.
113 target: Option<(String, u64)>,
114 /// Hits seen so far.
115 hits: Cell<u64>,
116}
117
118impl Interrupt {
119 /// A run that is never stopped: what the release code uses.
120 pub fn never() -> Interrupt {
121 return Interrupt { target: None, hits: Cell::new(0) };
122 }
123
124 /// A run that stops at the `after`-th (counting from 0) hit of `point`.
125 pub fn at(point: &str, after: u64) -> Interrupt {
126 return Interrupt { target: Some((point.to_string(), after)), hits: Cell::new(0) };
127 }
128
129 /// What:
130 /// Pass the named point; fail with `Interrupted` when it is the chosen hit.
131 /// Why:
132 /// The steps call it between their durable actions.
133 ///
134 /// In TS you'd write (pseudocode):
135 /// ```ts
136 /// interrupt.check('after-copy');
137 /// ```
138 pub fn check(&self, point: &str) -> Result<(), MigrateError> {
139 let Some((name, after)) = &self.target else { return Ok(()) };
140 if name != point {
141 return Ok(());
142 }
143 let seen: u64 = self.hits.get();
144 self.hits.set(seen + 1);
145 if seen == *after {
146 return Err(MigrateError::Interrupted(point.to_string()));
147 }
148 return Ok(());
149 }
150}