bison_db/error.rs
1//! Error type for `bison-db`.
2//!
3//! Every fallible operation in the crate returns [`Result<T>`], an alias for
4//! `core::result::Result<T, Error>`. [`Error`] is a small, closed enum: each
5//! variant names a distinct failure class so callers can match on the cause and
6//! decide how to recover rather than parsing a message string.
7//!
8//! The type is hand-rolled rather than derived from a macro crate so it stays
9//! dependency-free and compiles unchanged under `no_std` (only [`Error::Io`] is
10//! gated behind the `std` feature, because it wraps [`std::io::Error`]).
11
12use core::fmt;
13
14/// A specialised [`Result`](core::result::Result) for `bison-db` operations.
15///
16/// Used throughout the public API so callers can rely on a single error type.
17///
18/// # Examples
19///
20/// ```
21/// use bison_db::{Document, Result, Value};
22///
23/// fn name_of(doc: &Document) -> Result<&str> {
24/// // `Value::as_str` borrows; the surrounding code propagates with `?`.
25/// Ok(doc.get("name").and_then(Value::as_str).unwrap_or("<unknown>"))
26/// }
27/// # let mut d = Document::new();
28/// # d.set("name", "bison");
29/// # assert_eq!(name_of(&d).unwrap(), "bison");
30/// ```
31pub type Result<T> = core::result::Result<T, Error>;
32
33/// The set of failures a `bison-db` operation can produce.
34///
35/// Variants are grouped by origin: I/O failures from the host filesystem, and
36/// format failures detected while decoding the on-disk log. A format failure
37/// always means the bytes on disk did not match the expectations encoded by the
38/// writer — never that the caller passed bad arguments to an in-memory type.
39#[derive(Debug)]
40#[non_exhaustive]
41pub enum Error {
42 /// An underlying filesystem operation failed.
43 ///
44 /// Wraps the originating [`std::io::Error`] so the caller can inspect the
45 /// [`std::io::ErrorKind`] (for example, [`PermissionDenied`] when opening a
46 /// read-only path, or [`NotFound`] for a missing parent directory).
47 ///
48 /// [`PermissionDenied`]: std::io::ErrorKind::PermissionDenied
49 /// [`NotFound`]: std::io::ErrorKind::NotFound
50 #[cfg(feature = "std")]
51 Io(std::io::Error),
52
53 /// The file does not begin with a valid `bison-db` header.
54 ///
55 /// Returned by [`Db::open`](crate::Db::open) when the target path exists, is
56 /// non-empty, but its magic bytes do not identify it as a bison-db store —
57 /// the usual cause of opening the wrong file by mistake.
58 BadMagic,
59
60 /// The on-disk format version is newer than this build understands.
61 ///
62 /// The contained value is the version stamped in the file header. A binary
63 /// built against an older release will refuse to read a file written by a
64 /// newer one rather than risk misinterpreting it.
65 UnsupportedVersion(u16),
66
67 /// A stored record failed its integrity check or was structurally invalid.
68 ///
69 /// This covers a CRC mismatch, an unknown value tag, a length field that
70 /// overruns the record, or a non-UTF-8 string — any signal that the bytes
71 /// were corrupted in place after being written. A clean torn write at the
72 /// very end of the log is *not* reported here: it is recovered silently by
73 /// truncating the partial tail (see [`Db::open`](crate::Db::open)).
74 ///
75 /// The contained `&'static str` is a fixed diagnostic label, never a
76 /// formatted message, so producing it allocates nothing.
77 Corrupt(&'static str),
78
79 /// A value was too large to encode within the configured record limit.
80 ///
81 /// Guards against a single document growing past
82 /// [`MAX_RECORD_BYTES`](crate::MAX_RECORD_BYTES), which would otherwise
83 /// force an unbounded allocation on the read path during recovery.
84 ValueTooLarge,
85}
86
87#[cfg(feature = "std")]
88impl From<std::io::Error> for Error {
89 #[inline]
90 fn from(err: std::io::Error) -> Self {
91 Error::Io(err)
92 }
93}
94
95impl fmt::Display for Error {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 match self {
98 #[cfg(feature = "std")]
99 Error::Io(err) => write!(f, "i/o error: {err}"),
100 Error::BadMagic => f.write_str("not a bison-db file: header magic did not match"),
101 Error::UnsupportedVersion(v) => {
102 write!(
103 f,
104 "on-disk format version {v} is newer than this build supports"
105 )
106 }
107 Error::Corrupt(what) => write!(f, "corrupt record: {what}"),
108 Error::ValueTooLarge => f.write_str("value exceeds the maximum record size"),
109 }
110 }
111}
112
113#[cfg(feature = "std")]
114impl std::error::Error for Error {
115 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
116 match self {
117 Error::Io(err) => Some(err),
118 _ => None,
119 }
120 }
121}
122
123#[cfg(not(feature = "std"))]
124impl core::error::Error for Error {}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_display_corrupt_includes_label() {
132 let err = Error::Corrupt("crc mismatch");
133 assert!(err.to_string().contains("crc mismatch"));
134 }
135
136 #[test]
137 fn test_display_unsupported_version_includes_number() {
138 let err = Error::UnsupportedVersion(9);
139 assert!(err.to_string().contains('9'));
140 }
141
142 #[cfg(feature = "std")]
143 #[test]
144 fn test_io_error_conversion_preserves_source() {
145 use std::error::Error as _;
146 let io = std::io::Error::other("disk on fire");
147 let err: Error = io.into();
148 assert!(err.source().is_some());
149 }
150}