Skip to main content

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}