Skip to main content

quipu_core/
error.rs

1use std::fmt;
2
3pub type Result<T> = std::result::Result<T, Error>;
4
5#[derive(Debug)]
6pub enum Error {
7    Io(std::io::Error),
8    /// A record failed its CRC or length check while reading a segment.
9    Corrupt {
10        segment: String,
11        offset: u64,
12        reason: String,
13    },
14    Encode(String),
15    Crypto(String),
16    /// Schema/registry misuse: unknown type, missing field, kind mismatch, ...
17    Schema(String),
18    NotFound(String),
19    /// Another process already holds the store root open.
20    Locked(String),
21    /// A pagination cursor failed to decode, or was issued under different
22    /// query semantics (e.g. the opposite sort order). Client error.
23    InvalidCursor(String),
24}
25
26impl fmt::Display for Error {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Error::Io(e) => write!(f, "io error: {e}"),
30            Error::Corrupt {
31                segment,
32                offset,
33                reason,
34            } => {
35                write!(
36                    f,
37                    "corrupt record in {segment} at offset {offset}: {reason}"
38                )
39            }
40            Error::Encode(m) => write!(f, "encode/decode error: {m}"),
41            Error::Crypto(m) => write!(f, "crypto error: {m}"),
42            Error::Schema(m) => write!(f, "schema error: {m}"),
43            Error::NotFound(m) => write!(f, "not found: {m}"),
44            Error::Locked(root) => write!(
45                f,
46                "store root '{root}' is locked by another process (the store is single-process)"
47            ),
48            Error::InvalidCursor(m) => write!(f, "invalid cursor: {m}"),
49        }
50    }
51}
52
53impl Error {
54    /// True when the underlying cause is an out-of-space condition (ENOSPC,
55    /// surfaced as [`std::io::ErrorKind::StorageFull`] or raw OS error 28 on
56    /// Linux/macOS).
57    ///
58    /// Disk-full is a *persistent* failure: unlike a transient I/O hiccup,
59    /// retrying the same write is pointless until space is actually freed.
60    /// Callers should skip retry/backoff loops for these and go straight to
61    /// their fallback path.
62    pub fn is_storage_full(&self) -> bool {
63        match self {
64            Error::Io(e) => io_is_storage_full(e),
65            _ => false,
66        }
67    }
68}
69
70/// ENOSPC detection on a raw [`std::io::Error`]. Both checks are needed:
71/// `ErrorKind::StorageFull` covers errors std already classified, the raw
72/// code covers errors wrapped from syscalls std maps to `Other`/`Uncategorized`
73/// on some platforms.
74pub fn io_is_storage_full(e: &std::io::Error) -> bool {
75    e.kind() == std::io::ErrorKind::StorageFull || e.raw_os_error() == Some(ENOSPC)
76}
77
78/// POSIX `ENOSPC` ("no space left on device") — 28 on Linux and macOS.
79const ENOSPC: i32 = 28;
80
81impl std::error::Error for Error {}
82
83impl From<std::io::Error> for Error {
84    fn from(e: std::io::Error) -> Self {
85        Error::Io(e)
86    }
87}
88
89impl From<bincode::Error> for Error {
90    fn from(e: bincode::Error) -> Self {
91        Error::Encode(e.to_string())
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use std::io::ErrorKind;
99
100    #[test]
101    fn classifies_enospc_by_raw_os_error() {
102        let e = Error::Io(std::io::Error::from_raw_os_error(ENOSPC));
103        assert!(e.is_storage_full());
104    }
105
106    #[test]
107    fn classifies_enospc_by_error_kind() {
108        // injected error with the right kind but no raw OS code — the path a
109        // wrapped/synthetic StorageFull error takes
110        let e = Error::Io(std::io::Error::new(ErrorKind::StorageFull, "disk full"));
111        assert!(e.is_storage_full());
112    }
113
114    #[test]
115    fn other_errors_are_not_storage_full() {
116        for e in [
117            Error::Io(std::io::Error::from_raw_os_error(13)), // EACCES
118            Error::Io(std::io::Error::new(ErrorKind::PermissionDenied, "nope")),
119            Error::Schema("x".into()),
120            Error::NotFound("x".into()),
121            Error::Encode("x".into()),
122        ] {
123            assert!(!e.is_storage_full(), "{e} misclassified as storage-full");
124        }
125    }
126}