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