acta/error.rs
1//! The crate-wide error type.
2//!
3//! Error kinds follow one rule, so callers can act on them:
4//!
5//! - a field whose value contradicts a requirement of Acta v0.2 is
6//! [`ErrorKind::Corruption`];
7//! - a well-formed field whose value belongs to a format or frame envelope this
8//! crate does not implement is one of the `Unsupported*` kinds, because such a
9//! file may be perfectly valid for a reader that does implement it;
10//! - a declared size above a configured bound is [`ErrorKind::ResourceLimit`];
11//! - a file that ends before its schema frame is complete is
12//! [`ErrorKind::IncompleteTail`], because a later append may complete it;
13//! - a call that asks for something the snapshot does not contain is
14//! [`ErrorKind::InvalidArgument`], because the file is not implicated;
15//! - a caller-supplied expected schema that differs from the file's own is
16//! [`ErrorKind::SchemaMismatch`], because neither side is wrong on its own
17//! and a caller acts on this differently from a malformed argument.
18//!
19//! A writer that cannot safely continue after a partial I/O failure reports
20//! [`ErrorKind::Poisoned`]. A writer that cannot take the crate's cooperative
21//! exclusive lock because another writer holds it reports
22//! [`ErrorKind::WriterLocked`], which is separated from [`ErrorKind::Io`] so
23//! contention is retryable without inspecting a platform error number. A
24//! writer that builds a frame contradicting its own invariants reports
25//! [`ErrorKind::Internal`], which never describes a file.
26//!
27//! A long-lived reader whose path now names a different file than the one it
28//! opened reports [`ErrorKind::FileReplaced`]. The replacement file may be
29//! perfectly valid on its own; the kind exists so a caller can distinguish
30//! "the path changed identity underneath the reader" from corruption of the
31//! file the reader holds. A path that still names the reader's own file but
32//! has lost committed bytes reports [`ErrorKind::FileTruncated`], which is the
33//! same distinction drawn one step further: the file is neither damaged nor a
34//! stranger, it is simply shorter than the snapshot that describes it.
35//!
36//! Reserved fields never produce an error. Specification section 2 requires
37//! readers to ignore them, and every reserved field lies inside a CRC-covered
38//! region, so corruption there is already caught by the surrounding checksum.
39
40use std::error::Error as StdError;
41use std::fmt;
42use std::io;
43
44/// The result type returned by Acta operations.
45pub type Result<T> = std::result::Result<T, Error>;
46
47/// The broad category of an Acta failure.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[non_exhaustive]
50pub enum ErrorKind {
51 /// An operating-system I/O operation failed.
52 Io,
53 /// The bytes violate an implemented v0.2 invariant.
54 Corruption,
55 /// The file declares a format version this crate does not implement.
56 UnsupportedVersion,
57 /// The file declares a feature bit this v0.2 implementation does not know.
58 UnsupportedFeature,
59 /// The file declares a frame type or envelope version outside this stage.
60 UnsupportedFrame,
61 /// A declared size exceeds the configured [`crate::Limits`].
62 ResourceLimit,
63 /// The file ends before its schema frame is complete.
64 IncompleteTail,
65 /// The call asked for something this snapshot does not contain.
66 InvalidArgument,
67 /// A caller-supplied expected schema differs from the file's own schema.
68 SchemaMismatch,
69 /// Another writer already holds this crate's cooperative exclusive lock.
70 WriterLocked,
71 /// A writer has encountered a partial I/O failure and cannot continue.
72 Poisoned,
73 /// The path a long-lived reader refreshes now names a different file than
74 /// the one its snapshot came from.
75 FileReplaced,
76 /// The path a long-lived reader refreshes still names its own file, but
77 /// that file has shrunk below the committed boundary the snapshot holds.
78 ///
79 /// A repair that removed only an uncommitted tail shrinks the file back to
80 /// exactly that boundary and is accepted; this kind reports a file that
81 /// went below it and so no longer contains frames the reader already
82 /// committed.
83 FileTruncated,
84 /// A writer built something that contradicts its own invariants, which is a
85 /// bug in this crate rather than a problem with any file.
86 Internal,
87}
88
89/// The region of a file an error was detected in.
90#[derive(Debug, Clone, PartialEq, Eq)]
91#[non_exhaustive]
92pub enum ErrorContext {
93 /// The file as a whole, rather than one of its structures.
94 File,
95 Prologue,
96 Frame {
97 sequence: u64,
98 },
99 Prefix,
100 Header,
101 Payload,
102 Trailer,
103}
104
105/// An Acta error with a category, optional file offset, and parsing context.
106pub struct Error {
107 kind: ErrorKind,
108 message: String,
109 offset: Option<u64>,
110 context: Vec<ErrorContext>,
111 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
112}
113
114impl Error {
115 pub fn kind(&self) -> ErrorKind {
116 self.kind
117 }
118
119 pub fn message(&self) -> &str {
120 &self.message
121 }
122
123 /// The absolute file offset of the field or region that failed the check.
124 ///
125 /// A check over a single field reports that field's offset. A check over a
126 /// region, such as a CRC, reports the first byte of the covered region.
127 pub fn offset(&self) -> Option<u64> {
128 self.offset
129 }
130
131 /// The regions containing the failure, innermost first.
132 pub fn context(&self) -> &[ErrorContext] {
133 &self.context
134 }
135
136 pub(crate) fn corruption(message: impl Into<String>, offset: Option<u64>) -> Self {
137 Self::new(ErrorKind::Corruption, message, offset, None)
138 }
139
140 pub(crate) fn unsupported_version(message: impl Into<String>, offset: Option<u64>) -> Self {
141 Self::new(ErrorKind::UnsupportedVersion, message, offset, None)
142 }
143
144 pub(crate) fn unsupported_feature(message: impl Into<String>, offset: Option<u64>) -> Self {
145 Self::new(ErrorKind::UnsupportedFeature, message, offset, None)
146 }
147
148 pub(crate) fn unsupported_frame(message: impl Into<String>, offset: Option<u64>) -> Self {
149 Self::new(ErrorKind::UnsupportedFrame, message, offset, None)
150 }
151
152 pub(crate) fn resource_limit(message: impl Into<String>, offset: Option<u64>) -> Self {
153 Self::new(ErrorKind::ResourceLimit, message, offset, None)
154 }
155
156 pub(crate) fn incomplete_tail(message: impl Into<String>, offset: Option<u64>) -> Self {
157 Self::new(ErrorKind::IncompleteTail, message, offset, None)
158 }
159
160 pub(crate) fn invalid_argument(message: impl Into<String>) -> Self {
161 Self::new(ErrorKind::InvalidArgument, message, None, None)
162 }
163
164 pub(crate) fn schema_mismatch(message: impl Into<String>) -> Self {
165 Self::new(ErrorKind::SchemaMismatch, message, None, None)
166 }
167
168 pub(crate) fn writer_locked(message: impl Into<String>) -> Self {
169 Self::new(ErrorKind::WriterLocked, message, None, None)
170 }
171
172 pub(crate) fn poisoned(message: impl Into<String>) -> Self {
173 Self::new(ErrorKind::Poisoned, message, None, None)
174 }
175
176 pub(crate) fn file_replaced(message: impl Into<String>) -> Self {
177 Self::new(ErrorKind::FileReplaced, message, None, None)
178 }
179
180 pub(crate) fn file_truncated(message: impl Into<String>, offset: Option<u64>) -> Self {
181 Self::new(ErrorKind::FileTruncated, message, offset, None)
182 }
183
184 pub(crate) fn internal(message: impl Into<String>) -> Self {
185 Self::new(ErrorKind::Internal, message, None, None)
186 }
187
188 pub(crate) fn io(error: io::Error, offset: Option<u64>) -> Self {
189 Self::new(
190 ErrorKind::Io,
191 error.to_string(),
192 offset,
193 Some(Box::new(error)),
194 )
195 }
196
197 pub(crate) fn with_context(mut self, context: ErrorContext) -> Self {
198 self.context.push(context);
199 self
200 }
201
202 /// Name the structure an error came from, for a check that could not name
203 /// it itself.
204 ///
205 /// Some checks are deliberately unaware of their caller: the codecs do not
206 /// know which column they are decompressing, and should not have to. The
207 /// caller that does know prefixes the message rather than restating it.
208 pub(crate) fn with_message_prefix(mut self, prefix: impl AsRef<str>) -> Self {
209 self.message = format!("{}: {}", prefix.as_ref(), self.message);
210 self
211 }
212
213 fn new(
214 kind: ErrorKind,
215 message: impl Into<String>,
216 offset: Option<u64>,
217 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
218 ) -> Self {
219 Self {
220 kind,
221 message: message.into(),
222 offset,
223 context: Vec::new(),
224 source,
225 }
226 }
227}
228
229impl fmt::Debug for Error {
230 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
231 formatter
232 .debug_struct("Error")
233 .field("kind", &self.kind)
234 .field("message", &self.message)
235 .field("offset", &self.offset)
236 .field("context", &self.context)
237 .field(
238 "source",
239 &self.source.as_ref().map(|source| source.to_string()),
240 )
241 .finish()
242 }
243}
244
245impl fmt::Display for Error {
246 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
247 write!(formatter, "{}: {}", self.kind, self.message)?;
248 if let Some(offset) = self.offset {
249 write!(formatter, " at file offset 0x{offset:x}")?;
250 }
251 if !self.context.is_empty() {
252 write!(formatter, " (context: ")?;
253 for (index, context) in self.context.iter().enumerate() {
254 if index != 0 {
255 write!(formatter, ", ")?;
256 }
257 write!(formatter, "{context:?}")?;
258 }
259 write!(formatter, ")")?;
260 }
261 Ok(())
262 }
263}
264
265impl StdError for Error {
266 fn source(&self) -> Option<&(dyn StdError + 'static)> {
267 self.source
268 .as_ref()
269 .map(|source| &**source as &(dyn StdError + 'static))
270 }
271}
272
273impl fmt::Display for ErrorKind {
274 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
275 let label = match self {
276 Self::Io => "I/O error",
277 Self::Corruption => "corruption",
278 Self::UnsupportedVersion => "unsupported format version",
279 Self::UnsupportedFeature => "unsupported feature",
280 Self::UnsupportedFrame => "unsupported frame",
281 Self::ResourceLimit => "resource limit exceeded",
282 Self::IncompleteTail => "incomplete tail",
283 Self::InvalidArgument => "invalid argument",
284 Self::SchemaMismatch => "schema mismatch",
285 Self::WriterLocked => "writer locked",
286 Self::Poisoned => "poisoned writer",
287 Self::FileReplaced => "file replaced",
288 Self::FileTruncated => "file truncated",
289 Self::Internal => "internal writer error",
290 };
291 formatter.write_str(label)
292 }
293}