1use thiserror::Error;
2pub mod map;
3
4#[derive(Debug, Error)]
5pub enum ApiError {
6 #[error("policy violation: {0}")]
7 PolicyViolation(String),
8 #[error("locking timeout: {0}")]
9 LockingTimeout(String),
10 #[error("filesystem error: {0}")]
11 FilesystemError(String),
12 #[error("cross-filesystem degraded path not allowed: {0}")]
13 ExdevDegraded(String),
14 #[error("smoke tests failed")]
15 SmokeFailed,
16 #[error("ownership check failed: {0}")]
17 OwnershipError(String),
18 #[error("attestation failed: {0}")]
19 AttestationFailed(String),
20}
21
22#[must_use]
25pub fn infer_summary_error_ids(errors: &[String]) -> Vec<&'static str> {
26 let mut out: Vec<&'static str> = Vec::new();
27 let joined = errors.join("; ").to_lowercase();
28 if joined.contains("smoke") {
29 out.push(id_str(ErrorId::E_SMOKE));
30 }
31 if joined.contains("lock") {
32 out.push(id_str(ErrorId::E_LOCKING));
33 }
34 if joined.contains("ownership") {
35 out.push(id_str(ErrorId::E_OWNERSHIP));
36 }
37 if joined.contains("exdev") {
38 out.push(id_str(ErrorId::E_EXDEV));
39 }
40 if joined.contains("xdev")
42 || joined.contains("cross-device")
43 || joined.contains("cross device")
44 || joined.contains("os error 18")
45 || joined.contains("errno 18")
46 {
47 out.push(id_str(ErrorId::E_EXDEV));
48 }
49 if joined.contains("atomic") || joined.contains("symlink") {
50 out.push(id_str(ErrorId::E_ATOMIC_SWAP));
51 }
52 if joined.contains("backup") && joined.contains("missing") {
53 out.push(id_str(ErrorId::E_BACKUP_MISSING));
54 }
55 if joined.contains("restore") && joined.contains("failed") {
56 out.push(id_str(ErrorId::E_RESTORE_FAILED));
57 }
58 if out.is_empty() {
59 out.push(id_str(ErrorId::E_POLICY));
60 } else {
61 out.push(id_str(ErrorId::E_POLICY));
63 }
64 let mut seen = std::collections::HashSet::new();
66 out.into_iter().filter(|id| seen.insert(*id)).collect()
67}
68
69impl From<crate::types::errors::Error> for ApiError {
70 fn from(e: crate::types::errors::Error) -> Self {
71 use crate::types::errors::ErrorKind::{InvalidPath, Io, Policy};
72 match e.kind {
73 InvalidPath | Io => ApiError::FilesystemError(e.msg),
74 Policy => ApiError::PolicyViolation(e.msg),
75 }
76 }
77}
78
79#[allow(
82 non_camel_case_types,
83 reason = "Error IDs must match SPEC/error_codes.toml format"
84)]
85#[derive(Clone, Copy, Debug)]
86pub enum ErrorId {
87 E_POLICY,
88 E_OWNERSHIP,
89 E_LOCKING,
90 E_ATOMIC_SWAP,
91 E_EXDEV,
92 E_BACKUP_MISSING,
93 E_RESTORE_FAILED,
94 E_SMOKE,
95 E_GENERIC,
96}
97
98#[must_use]
99pub const fn id_str(id: ErrorId) -> &'static str {
100 match id {
101 ErrorId::E_POLICY => "E_POLICY",
102 ErrorId::E_OWNERSHIP => "E_OWNERSHIP",
103 ErrorId::E_LOCKING => "E_LOCKING",
104 ErrorId::E_ATOMIC_SWAP => "E_ATOMIC_SWAP",
105 ErrorId::E_EXDEV => "E_EXDEV",
106 ErrorId::E_BACKUP_MISSING => "E_BACKUP_MISSING",
107 ErrorId::E_RESTORE_FAILED => "E_RESTORE_FAILED",
108 ErrorId::E_SMOKE => "E_SMOKE",
109 ErrorId::E_GENERIC => "E_GENERIC",
110 }
111}
112
113#[must_use]
114pub const fn exit_code_for(id: ErrorId) -> i32 {
115 match id {
116 ErrorId::E_POLICY => 10,
117 ErrorId::E_OWNERSHIP => 20,
118 ErrorId::E_LOCKING => 30,
119 ErrorId::E_ATOMIC_SWAP => 40,
120 ErrorId::E_EXDEV => 50,
121 ErrorId::E_BACKUP_MISSING => 60,
122 ErrorId::E_RESTORE_FAILED => 70,
123 ErrorId::E_SMOKE => 80,
124 ErrorId::E_GENERIC => 1,
125 }
126}
127
128#[must_use]
129pub fn exit_code_for_id_str(s: &str) -> Option<i32> {
130 match s {
131 "E_POLICY" => Some(10),
132 "E_OWNERSHIP" => Some(20),
133 "E_LOCKING" => Some(30),
134 "E_ATOMIC_SWAP" => Some(40),
135 "E_EXDEV" => Some(50),
136 "E_BACKUP_MISSING" => Some(60),
137 "E_RESTORE_FAILED" => Some(70),
138 "E_SMOKE" => Some(80),
139 _ => None,
140 }
141}