Skip to main content

quiver_core/
error.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//! Error types for the storage engine.
3//!
4//! Library crates expose typed errors via `thiserror` (ADR-0017); the binary
5//! edges add human context with `anyhow`. Integrity failures get their own
6//! variants so a detected torn or tampered page aborts the read instead of
7//! silently serving bad data.
8
9use std::io;
10use std::path::{Path, PathBuf};
11
12/// Errors returned by the storage engine.
13#[derive(Debug, thiserror::Error)]
14#[non_exhaustive]
15pub enum CoreError {
16    /// An I/O operation failed against a known path.
17    #[error("i/o error at {path}: {source}")]
18    Io {
19        /// Path involved in the failed operation.
20        path: PathBuf,
21        /// The underlying OS error.
22        source: io::Error,
23    },
24
25    /// An I/O operation failed without an associated path.
26    #[error("i/o error: {0}")]
27    BareIo(#[from] io::Error),
28
29    /// A page or file carried the wrong magic number — a different file kind, or
30    /// not a Quiver file at all.
31    #[error("bad magic: expected {expected:#010x}, found {found:#010x}")]
32    BadMagic {
33        /// The magic the reader expected.
34        expected: u32,
35        /// The magic actually found on disk.
36        found: u32,
37    },
38
39    /// The on-disk format version is not understood by this build.
40    #[error("unsupported format version {found} (this build supports {supported})")]
41    UnsupportedVersion {
42        /// Version read from disk.
43        found: u16,
44        /// Highest version this build can read.
45        supported: u16,
46    },
47
48    /// A page failed its CRC32C check — corruption or tampering was detected.
49    #[error("page {page_id} failed crc check (header {expected:#010x}, computed {computed:#010x})")]
50    PageCorrupt {
51        /// Page id from the (possibly damaged) header.
52        page_id: u64,
53        /// CRC stored in the page header.
54        expected: u32,
55        /// CRC recomputed over the page contents.
56        computed: u32,
57    },
58
59    /// A page or file header was structurally invalid (impossible length,
60    /// unknown page type, out-of-order page id, …).
61    #[error("malformed page: {0}")]
62    MalformedPage(String),
63
64    /// Serialization or deserialization of a metadata structure failed.
65    #[error("serialization error: {0}")]
66    Serialization(#[from] postcard::Error),
67
68    /// A value exceeded a hard structural limit (e.g. a payload larger than a
69    /// page body, or a WAL record over the size cap).
70    #[error("value too large: {0}")]
71    TooLarge(String),
72
73    /// A referenced collection or record does not exist.
74    #[error("not found: {0}")]
75    NotFound(String),
76
77    /// A resource that must be unique already exists (e.g. a collection name).
78    #[error("already exists: {0}")]
79    AlreadyExists(String),
80
81    /// A caller supplied an invalid argument (e.g. a vector of the wrong dim).
82    #[error("invalid argument: {0}")]
83    InvalidArgument(String),
84}
85
86impl CoreError {
87    /// Build an [`CoreError::Io`] tagged with the path it occurred on.
88    #[must_use]
89    pub fn io(path: impl AsRef<Path>, source: io::Error) -> Self {
90        Self::Io {
91            path: path.as_ref().to_path_buf(),
92            source,
93        }
94    }
95}
96
97/// Convenience alias for storage-engine results.
98pub type Result<T> = std::result::Result<T, CoreError>;
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use std::io::ErrorKind;
104
105    // The operator-facing Display messages are produced only when an error is
106    // formatted (logged / returned to a caller), which the variant-matching tests
107    // elsewhere never trigger. Format every variant so a broken message surfaces
108    // here rather than in production logs.
109    #[test]
110    fn every_variant_formats_a_useful_message() {
111        let io = CoreError::io(
112            "/tmp/x",
113            io::Error::new(ErrorKind::PermissionDenied, "boom"),
114        );
115        assert_eq!(io.to_string(), "i/o error at /tmp/x: boom");
116
117        let bare: CoreError = io::Error::new(ErrorKind::UnexpectedEof, "eof").into();
118        assert_eq!(bare.to_string(), "i/o error: eof");
119
120        assert_eq!(
121            CoreError::BadMagic {
122                expected: 0xDEAD_BEEF,
123                found: 0x0000_0001
124            }
125            .to_string(),
126            "bad magic: expected 0xdeadbeef, found 0x00000001",
127        );
128        assert_eq!(
129            CoreError::UnsupportedVersion {
130                found: 9,
131                supported: 2
132            }
133            .to_string(),
134            "unsupported format version 9 (this build supports 2)",
135        );
136        assert_eq!(
137            CoreError::PageCorrupt {
138                page_id: 7,
139                expected: 0x0000_00ff,
140                computed: 0x0000_0100
141            }
142            .to_string(),
143            "page 7 failed crc check (header 0x000000ff, computed 0x00000100)",
144        );
145
146        assert_eq!(
147            CoreError::MalformedPage("len".into()).to_string(),
148            "malformed page: len"
149        );
150        assert_eq!(
151            CoreError::TooLarge("payload".into()).to_string(),
152            "value too large: payload"
153        );
154        assert_eq!(CoreError::NotFound("c".into()).to_string(), "not found: c");
155        assert_eq!(
156            CoreError::AlreadyExists("c".into()).to_string(),
157            "already exists: c"
158        );
159        assert_eq!(
160            CoreError::InvalidArgument("dim".into()).to_string(),
161            "invalid argument: dim",
162        );
163
164        // A real postcard failure flows through the `#[from]` conversion.
165        let de = postcard::from_bytes::<u32>(&[]).unwrap_err();
166        let err: CoreError = de.into();
167        assert!(err.to_string().starts_with("serialization error:"), "{err}");
168    }
169
170    #[test]
171    fn io_constructor_tags_the_path() {
172        let err = CoreError::io("data/seg.000", io::Error::from(ErrorKind::NotFound));
173        match err {
174            CoreError::Io { path, .. } => assert_eq!(path, PathBuf::from("data/seg.000")),
175            other => panic!("expected Io, got {other:?}"),
176        }
177    }
178}