1use std::io;
24use std::path::{Path, PathBuf};
25
26use kernel::{AxCode, AxError};
27
28#[derive(Debug, thiserror::Error)]
30pub enum StorageError {
31 #[error("{op} failed at {path}: {source}")]
32 Io {
33 op: &'static str,
34 path: PathBuf,
35 #[source]
36 source: io::Error,
37 },
38 #[error("ledger at {path} was written by a newer sprawling (v{v})")]
39 VersionAhead { path: PathBuf, v: u64 },
40 #[error("ledger segment {path} is damaged at line {line}")]
41 Envelope {
42 path: PathBuf,
43 line: u64,
44 #[source]
45 source: AxError,
46 },
47 #[error("could not build a ledger draft")]
48 Draft {
49 #[source]
50 source: AxError,
51 },
52 #[error("cas object {hash} is not in the store")]
53 CasMissing { hash: String },
54 #[error("cas object {hash} at {path} does not hash to its address")]
55 CasCorrupt { hash: String, path: PathBuf },
56 #[error("range does not fit cas object {hash}")]
57 RangeOutOfBounds { hash: String },
58 #[error("seq {seq} is not in the ledger index")]
59 SeqMissing { seq: u64 },
60 #[error("checkpoint {op} failed: {detail}")]
61 Checkpoint { op: &'static str, detail: String },
62 #[error("staged content matches secret shapes at {locations}")]
65 SecretEgress { locations: String },
66 #[error("bundle {op} refused: {detail}")]
70 Bundle { op: &'static str, detail: String },
71 #[error("worktree {op} failed: {detail}")]
74 Worktree { op: &'static str, detail: String },
75 #[error("worktree {name} cannot be opened: {detail}")]
79 WorktreeBusy { name: String, detail: String },
80 #[error("worktree {name} is behind the trunk: {detail}")]
84 MergeStale { name: String, detail: String },
85 #[error("merging would discard uncommitted changes to {}", paths.join(", "))]
88 MergeWouldDiscard { paths: Vec<String> },
89 #[error("{op} refused at {path}: that name is a {kind}")]
92 Alias {
93 op: &'static str,
94 path: PathBuf,
95 kind: crate::alias::AliasKind,
96 },
97 #[error("{} already exists", path.display())]
101 NameTaken { path: PathBuf },
102 #[error("the ledger at {dir} is held by another writer")]
105 LedgerHeld { dir: PathBuf },
106 #[error("the ledger at {} lost its barrier at seq {}", dir.display(), at.value())]
110 LedgerBroken { dir: PathBuf, at: kernel::Seq },
111 #[error("the ledger stopped taking writes because its chain failed verification")]
114 ChainHalted {
115 #[source]
116 source: AxError,
117 },
118 #[error("the ledger takes no line until the history it opened from is proved")]
121 Unproven,
122 #[error("{op} failed at {path}: {source}")]
125 Snapshot {
126 op: &'static str,
127 path: PathBuf,
128 #[source]
129 source: io::Error,
130 },
131}
132
133impl StorageError {
134 pub fn into_ax(self) -> AxError {
136 match self {
137 StorageError::Io { op, path, source } => {
140 AxError::failure(AxCode::StorageFatal, op, path.display().to_string())
141 .with_recovery(format!(
142 "storage failed ({source}); halt cleanly — reopening will tail-truncate"
143 ))
144 }
145 StorageError::VersionAhead { path, v } => AxError::failure(
146 AxCode::LogVersionUnsupported,
147 "open ledger",
148 format!("{} (written by a newer sprawling, v{v})", path.display()),
149 )
150 .with_recovery(format!(
151 "open this ledger with the sprawling that wrote it; original path: {}",
152 path.display()
153 )),
154 StorageError::Envelope { path, line, source } => AxError::failure(
155 AxCode::LogVersionUnsupported,
156 "open ledger",
157 format!("{}:{line}", path.display()),
158 )
159 .with_recovery(format!(
160 "non-tail damage cannot be auto-repaired ({source}); inspect the segment"
161 )),
162 StorageError::Draft { source } | StorageError::ChainHalted { source } => source,
163 StorageError::Unproven => AxError::failure(
164 AxCode::HistoryUnproven,
165 "append to the ledger",
166 "the history this city opened from is still being proved",
167 )
168 .with_recovery(
169 "the city is still proving the history it opened from; send it again once the log says the history is proved",
170 ),
171 StorageError::Worktree { op, detail } => {
172 AxError::failure(AxCode::StorageFatal, op, detail).with_recovery(
173 "the repository or the filesystem refused; fix that, then claim again",
174 )
175 }
176 StorageError::WorktreeBusy { name, detail } => AxError::failure(
177 AxCode::WorktreeBusy,
178 "open a worktree",
179 format!("{name}: {detail}"),
180 )
181 .with_recovery("release a tree and claim again, or work on a node whose tree is free"),
182 StorageError::MergeStale { name, detail } => AxError::failure(
183 AxCode::VersionConflict,
184 "merge a node's work",
185 format!("{name}: {detail}"),
186 )
187 .with_recovery(
188 "rebuild this node's tree on the trunk as it stands, then have it verified again",
189 ),
190 StorageError::MergeWouldDiscard { paths } => AxError::failure(
191 AxCode::VersionConflict,
192 "merge a node's work",
193 format!("uncommitted changes to {}", paths.join(", ")),
194 )
195 .with_recovery(
196 "commit or set aside these changes in the city folder, then merge again",
197 ),
198 StorageError::CasMissing { hash } => {
199 AxError::failure(AxCode::PathNotFound, "read cas object", hash)
200 .with_recovery("the locator outlived its object; re-materialize or drop it")
201 }
202 StorageError::CasCorrupt { hash, path } => AxError::failure(
203 AxCode::CasCorrupt,
204 "read cas object",
205 format!("{hash} at {}", path.display()),
206 )
207 .with_recovery("bit rot or outside tampering; restore from export or drop the object"),
208 StorageError::RangeOutOfBounds { hash } => {
209 AxError::failure(AxCode::InvalidArgs, "read cas range", hash)
210 .with_recovery("range exceeds the object; ask within its size")
211 }
212 StorageError::Snapshot { op, path, source } => {
213 AxError::failure(AxCode::StorageFatal, op, path.display().to_string())
214 .with_recovery(format!(
215 "storage failed ({source}); a snapshot is rebuilt from the ledger, so \
216 remove {} and free the disk; the next start folds from genesis",
217 path.display()
218 ))
219 }
220 StorageError::SeqMissing { seq } => {
223 AxError::failure(AxCode::InvalidArgs, "read ledger line", seq.to_string())
224 .with_recovery("ask for a seq the ledger actually holds")
225 }
226 StorageError::Bundle { op, detail } => {
227 AxError::failure(AxCode::ConfigInvalid, op, detail).with_recovery(
228 "restore into an empty directory, and copy the bundle again if it is short",
229 )
230 }
231 StorageError::Checkpoint { op, detail } => {
232 AxError::failure(AxCode::WorktreeBusy, op, detail)
233 .with_recovery("resolve the repository state, then retry the wave")
234 }
235 StorageError::SecretEgress { locations } => {
236 AxError::failure(AxCode::SecretEgress, "commit checkpoint", locations)
237 .with_recovery("remove the secret from the staged files, then retry")
238 }
239 StorageError::LedgerHeld { dir } => AxError::failure(
240 AxCode::LedgerHeld,
241 "open the ledger for writing",
242 dir.display().to_string(),
243 )
244 .with_recovery(
245 "another sprawling process is serving or changing this city; close it \
246 (/quit in its terminal, or Ctrl-C where it runs with no console), then run this again",
247 ),
248 StorageError::LedgerBroken { dir, at } => AxError::failure(
249 AxCode::StorageFatal,
250 "append to the ledger",
251 format!("{} at seq {}", dir.display(), at.value()),
252 )
253 .with_recovery(
254 "a write to this ledger failed earlier; free the disk or fix the device, \
255 then restart sprawling so that opening the ledger repairs its tail",
256 ),
257 StorageError::Alias { op, path, kind } => {
258 AxError::failure(AxCode::OutsideWriteDomain, op, path.display().to_string())
259 .with_recovery(format!(
260 "a {kind} at this name reaches a file other than this name; \
261 replace it with a plain file, then retry"
262 ))
263 }
264 StorageError::NameTaken { path } => AxError::failure(
265 AxCode::VersionConflict,
266 "create a file",
267 path.display().to_string(),
268 )
269 .with_recovery(
270 "something already stands at this name; read it and change it against its \
271 version, or create the file under another name",
272 ),
273 }
274 }
275}
276
277pub(crate) fn io_err(op: &'static str, path: &Path) -> impl FnOnce(io::Error) -> StorageError {
282 let path = path.to_path_buf();
283 move |source| StorageError::Io { op, path, source }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::StorageError;
289
290 #[test]
294 fn a_snapshot_recovery_reads_as_one_sentence() {
295 let refused = StorageError::Snapshot {
296 op: "write a snapshot",
297 path: "snap".into(),
298 source: std::io::Error::other("disk full"),
299 }
300 .into_ax();
301 assert_eq!(
302 refused.recovery(),
303 "storage failed (disk full); a snapshot is rebuilt from the ledger, so remove snap \
304 and free the disk; the next start folds from genesis"
305 );
306 }
307}