segment_buffer/error.rs
1//! Error types for segment-buffer.
2//!
3//! Errors carry the context an operator needs to diagnose a failure at 3am:
4//! the path to the offending segment file, the phase that failed, and the
5//! underlying cause. Use [`Result`](crate::Result) as the alias.
6//!
7//! # Matching on a failure to recover the offending path
8//!
9//! Every non-I/O variant carries the segment file's [`PathBuf`](std::path::PathBuf),
10//! so an operator can match on the variant and act (move the bad file aside,
11//! alert, etc.) without parsing the rendered message:
12//!
13//! ```
14//! use segment_buffer::{SegmentBuffer, SegmentConfig, SegmentError};
15//! use tempfile::tempdir;
16//!
17//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! let dir = tempdir()?;
19//! let buf: SegmentBuffer<u64> =
20//! SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
21//!
22//! // Drop a corrupt segment so the next read surfaces a typed error.
23//! std::fs::write(
24//! dir.path().join("seg_000000000000_000000000000.zst"),
25//! b"this is not zstd+CBOR",
26//! )?;
27//!
28//! match buf.read_from(0, 10) {
29//! Ok(_) => { /* happy path */ }
30//! Err(SegmentError::Cbor { path, phase, .. }) => {
31//! eprintln!(
32//! "CBOR {phase} failed on {}; quarantining",
33//! path.display()
34//! );
35//! let quarantined = format!("{}.quarantined", path.display());
36//! let _ = std::fs::rename(&path, quarantined);
37//! }
38//! Err(SegmentError::Cipher { path, .. }) => {
39//! eprintln!("cipher failure on {} — likely wrong key", path.display());
40//! }
41//! Err(SegmentError::Integrity { path, reason }) => {
42//! eprintln!("integrity failure on {}: {reason}", path.display());
43//! }
44//! Err(SegmentError::Io { path, source }) => {
45//! match path {
46//! Some(p) => eprintln!("I/O failure on {}: {source}", p.display()),
47//! None => eprintln!("unrelated I/O failure: {source}"),
48//! }
49//! }
50//! // `SegmentError` is `#[non_exhaustive]`, so a catch-all is required
51//! // for forward compatibility with future variants.
52//! Err(other) => {
53//! eprintln!("unhandled segment-buffer error: {other}");
54//! }
55//! }
56//! # Ok(())
57//! # }
58//! ```
59
60use std::path::PathBuf;
61
62/// Errors produced by segment-buffer operations.
63///
64/// Every variant carries the [`path`](Self::Cbor) of the segment file
65/// involved (when one is in scope), so an operator can act on the failure
66/// without spelunking through logs.
67#[derive(Debug, thiserror::Error)]
68#[non_exhaustive]
69pub enum SegmentError {
70 /// Filesystem I/O failure (directory creation, segment read/write, rename, etc.).
71 ///
72 /// Carries the offending `path` when one is in scope, plus the underlying
73 /// [`std::io::Error`] as `source`. When `?` propagates an `io::Error`
74 /// without context, `path` is `None`; use
75 /// [`with_path`](Self::with_path) (or construct the variant directly) to
76 /// attach the path at high-value call sites.
77 #[error("I/O error{path_clause}: {source}", path_clause = format_path_clause(path))]
78 Io {
79 /// Path of the file the I/O failed on, when known. `None` for
80 /// directory-create or unspecified-path failures.
81 path: Option<PathBuf>,
82 /// The underlying io::Error, reachable via [`std::error::Error::source`].
83 #[source]
84 source: std::io::Error,
85 },
86
87 /// CBOR serialization or deserialization of a segment file failed.
88 #[error("CBOR {phase} failed for {path}: {message}")]
89 Cbor {
90 /// Which direction failed: `"serialize"` (writing) or `"deserialize"` (reading).
91 phase: &'static str,
92 /// Path to the offending segment file.
93 path: PathBuf,
94 /// Underlying CBOR error message.
95 message: String,
96 },
97
98 /// Cipher encrypt or decrypt of a segment file failed (key mismatch, AEAD
99 /// tag invalid, cipher misconfiguration).
100 #[error("cipher error for {path}: {message}")]
101 Cipher {
102 /// Path to the offending segment file.
103 path: PathBuf,
104 /// Underlying cipher error message.
105 message: String,
106 },
107
108 /// Segment file failed an integrity check: truncated, too small for the
109 /// AEAD nonce, or unrecognized envelope.
110 #[error("integrity failure for {path}: {reason}")]
111 Integrity {
112 /// Path to the offending segment file.
113 path: PathBuf,
114 /// What failed, in one short phrase.
115 reason: &'static str,
116 },
117}
118
119impl SegmentError {
120 /// Attach a path to an existing [`SegmentError::Io`] variant. Returns the
121 /// error unchanged for other variants. Useful for upgrading a `?`-propagated
122 /// io::Error to carry path context at a high-value call site.
123 #[must_use = "the upgraded error is meaningless if discarded"]
124 pub fn with_path(self, path: impl Into<PathBuf>) -> Self {
125 match self {
126 SegmentError::Io { path: _, source } => SegmentError::Io {
127 path: Some(path.into()),
128 source,
129 },
130 other => other,
131 }
132 }
133}
134
135impl From<std::io::Error> for SegmentError {
136 fn from(source: std::io::Error) -> Self {
137 SegmentError::Io { path: None, source }
138 }
139}
140
141/// Helper used by the `#[error]` attribute on [`SegmentError::Io`]. Produces
142/// ` for <path.display()>` when `path` is `Some`, or the empty string when
143/// `path` is `None` — so the rendered message has no spurious " for " clause.
144fn format_path_clause(path: &Option<PathBuf>) -> String {
145 match path {
146 Some(p) => format!(" for {}", p.display()),
147 None => String::new(),
148 }
149}
150
151/// Result alias used throughout the crate.
152pub type Result<T> = std::result::Result<T, SegmentError>;