Skip to main content

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}