1use std::io::Error;
9
10#[derive(Debug, thiserror::Error)]
12pub enum BackupError {
13 #[error("{0}")]
15 Io(#[from] Error),
16
17 #[error("archive: {0}")]
19 ObjectStore(#[from] krabka_object_store::ObjectStoreError),
20
21 #[error("{context}: {source}")]
23 Json {
24 context: String,
26 source: serde_json::Error,
28 },
29
30 #[error("{context}: {}", unsupported_version_reason(found.as_deref(), *expected))]
33 UnsupportedVersion {
34 context: String,
36 found: Option<String>,
39 expected: i16,
41 },
42
43 #[error("{0}")]
45 InvalidArgument(String),
46
47 #[error("integrity failure: {0}")]
49 Integrity(String),
50
51 #[error("{0}")]
53 NoSuchCapture(String),
54
55 #[error("cluster: {0}")]
57 Cluster(String),
58
59 #[error("stale metadata checkpoint: {0}")]
63 StaleMetadata(String),
64}
65
66pub const EXIT_BAD_ARGUMENT: i32 = 2;
68pub const EXIT_UNREADABLE: i32 = 4;
70pub const EXIT_INTEGRITY: i32 = 5;
72pub const EXIT_CLUSTER: i32 = 6;
74pub const EXIT_STALE_METADATA: i32 = 7;
76
77impl From<krabka_object_store::ArchiveArgsError> for BackupError {
79 fn from(error: krabka_object_store::ArchiveArgsError) -> Self {
80 Self::InvalidArgument(error.to_string())
81 }
82}
83
84fn unsupported_version_reason(found: Option<&str>, expected: i16) -> String {
86 found.map_or_else(
87 || {
88 "no \"version\" field: the capture predates krabka 1.0, which this build does not \
89 read"
90 .to_owned()
91 },
92 |found| format!("unsupported version {found}: this build reads version {expected}"),
93 )
94}
95
96impl BackupError {
97 #[must_use]
99 pub fn exit_code(&self) -> i32 {
100 match self {
101 Self::InvalidArgument(_) => EXIT_BAD_ARGUMENT,
102 Self::Io(_)
103 | Self::ObjectStore(_)
104 | Self::Json { .. }
105 | Self::UnsupportedVersion { .. }
106 | Self::NoSuchCapture(_) => EXIT_UNREADABLE,
107 Self::Integrity(_) => EXIT_INTEGRITY,
108 Self::Cluster(_) => EXIT_CLUSTER,
109 Self::StaleMetadata(_) => EXIT_STALE_METADATA,
110 }
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use {
117 super::{
118 BackupError, EXIT_BAD_ARGUMENT, EXIT_CLUSTER, EXIT_INTEGRITY, EXIT_STALE_METADATA,
119 EXIT_UNREADABLE,
120 },
121 assert2::check,
122 std::io::Error,
123 };
124
125 #[yare::parameterized(
126 invalid_argument = { BackupError::InvalidArgument("bad".to_owned()), EXIT_BAD_ARGUMENT },
127 io = { BackupError::Io(Error::other("disk")), EXIT_UNREADABLE },
128 no_such_capture = { BackupError::NoSuchCapture("none".to_owned()), EXIT_UNREADABLE },
129 unsupported_version = {
130 BackupError::UnsupportedVersion { context: "manifest.json".to_owned(), found: None, expected: 0 },
131 EXIT_UNREADABLE,
132 },
133 integrity = { BackupError::Integrity("digest".to_owned()), EXIT_INTEGRITY },
134 cluster = { BackupError::Cluster("refused".to_owned()), EXIT_CLUSTER },
135 stale_metadata = { BackupError::StaleMetadata("format-time".to_owned()), EXIT_STALE_METADATA },
136 )]
137 fn every_variant_maps_onto_its_documented_exit_code(error: BackupError, expected: i32) {
138 check!(error.exit_code() == expected, "for {error}");
139 }
140}