Skip to main content

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}