iqdb_persist/error.rs
1//! The iqdb-persist domain error.
2//!
3//! [`PersistError`] names every failure mode the persistence layer can
4//! surface. It mirrors [`iqdb_types::IqdbError`]'s shape (non-exhaustive
5//! enum, one variant per failure, [`error_forge::ForgeError`]
6//! integration) so the two errors compose into the same operator-facing
7//! structured-error events.
8//!
9//! Unlike `IqdbError`, this type is **not** `Copy` or `Clone`: the `Io`
10//! variant wraps a `std::io::Error` (which implements neither) and the
11//! `InvalidIndexType` variant carries an owned `String` for the tag the
12//! header surfaced.
13
14use std::path::PathBuf;
15
16use error_forge::ForgeError;
17use iqdb_types::{DistanceMetric, IqdbError};
18
19/// An error from an `iqdb-persist` save, load, or format operation.
20///
21/// Each variant identifies one specific failure. The enum is
22/// `#[non_exhaustive]`: future releases may add variants without it
23/// being a breaking change, so a `match` on it must include a wildcard
24/// arm.
25///
26/// # Examples
27///
28/// ```
29/// use iqdb_persist::PersistError;
30///
31/// let err = PersistError::ChecksumMismatch { expected: 0xDEADBEEF, computed: 0x00000000 };
32/// assert!(err.to_string().contains("checksum mismatch"));
33///
34/// let unsup = PersistError::Unsupported { feature: "compression", available_in: "v0.4" };
35/// assert!(unsup.to_string().contains("v0.4"));
36/// ```
37#[non_exhaustive]
38#[derive(Debug)]
39pub enum PersistError {
40 /// An OS-level I/O failure occurred while reading or writing a
41 /// snapshot file. `path` is the file whose operation failed;
42 /// `source` is the underlying `std::io::Error` and is reachable via
43 /// [`std::error::Error::source`].
44 Io {
45 /// The path whose I/O operation failed.
46 path: PathBuf,
47 /// The underlying I/O error.
48 source: std::io::Error,
49 },
50 /// The first eight bytes of the file did not match
51 /// [`crate::MAGIC`] — the file is not an iqdb snapshot.
52 BadMagic {
53 /// The eight magic bytes actually read from the file.
54 found: [u8; 8],
55 },
56 /// The header's format-version field is not one this build supports.
57 /// `found` is what the file declared; `supported` is the version this
58 /// build writes.
59 UnsupportedVersion {
60 /// The version the file declared.
61 found: u32,
62 /// The format version this build supports.
63 supported: u32,
64 },
65 /// The CRC32 of the payload bytes did not match the header's stored
66 /// value — the payload is corrupted or has been tampered with.
67 ChecksumMismatch {
68 /// The CRC32 the header claimed.
69 expected: u32,
70 /// The CRC32 actually computed over the payload bytes.
71 computed: u32,
72 },
73 /// The file ended before the full header could be read. `needed` is
74 /// the number of bytes the parser still wanted; `found` is how many
75 /// were available.
76 TruncatedHeader {
77 /// Bytes the parser still needed.
78 needed: usize,
79 /// Bytes that were available.
80 found: usize,
81 },
82 /// The file ended before the full payload could be read.
83 TruncatedPayload {
84 /// Payload bytes the parser still needed.
85 needed: u64,
86 /// Payload bytes that were available.
87 found: u64,
88 },
89 /// The header's metric tag does not correspond to any
90 /// [`iqdb_types::DistanceMetric`] variant this build knows about.
91 InvalidMetric {
92 /// The on-disk metric tag byte.
93 tag: u8,
94 },
95 /// A [`DistanceMetric`] this build has no on-disk tag for. Only
96 /// occurs on save if a newer `iqdb-types` introduced a metric
97 /// variant that this build of `iqdb-persist` predates —
98 /// `DistanceMetric` is `#[non_exhaustive]`.
99 UnsupportedMetric {
100 /// The metric that could not be encoded.
101 metric: DistanceMetric,
102 },
103 /// The header's index-type tag does not match the concrete `I`'s
104 /// [`crate::Persistable::INDEX_TYPE`].
105 InvalidIndexType {
106 /// The index-type tag the file declared.
107 found: String,
108 /// The index-type tag the caller's `I` requires.
109 expected: &'static str,
110 },
111 /// The payload bytes decoded successfully at the byte level but
112 /// produced a structurally invalid index.
113 InvalidPayload {
114 /// Short, stable identifier for the structural check that failed.
115 reason: &'static str,
116 },
117 /// A compression or decompression step failed: an invalid codec
118 /// parameter on save, or a codec error / length mismatch on load.
119 /// (Bulk on-disk corruption is caught earlier by the payload CRC32 and
120 /// surfaces as [`ChecksumMismatch`](Self::ChecksumMismatch).)
121 Compression {
122 /// Short, stable identifier for the codec failure.
123 reason: &'static str,
124 },
125 /// A nested [`IqdbError`] surfaced from a downstream construction
126 /// step — typically [`iqdb_index::Index::new`] or
127 /// [`iqdb_index::IndexCore::insert`] called from inside a
128 /// [`crate::Persistable::load_from`] impl.
129 IndexBuild(IqdbError),
130 /// A configuration value asked for a feature that this build does
131 /// not implement yet.
132 Unsupported {
133 /// Short, stable identifier for the unsupported feature.
134 feature: &'static str,
135 /// The version where the feature lands.
136 available_in: &'static str,
137 },
138}
139
140impl std::fmt::Display for PersistError {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 match self {
143 Self::Io { path, source } => {
144 write!(f, "I/O error on {}: {source}", path.display())
145 }
146 Self::BadMagic { found } => {
147 write!(f, "bad magic: not an iqdb snapshot (found {found:?})")
148 }
149 Self::UnsupportedVersion { found, supported } => {
150 write!(
151 f,
152 "unsupported format version: found {found}, supported {supported}",
153 )
154 }
155 Self::ChecksumMismatch { expected, computed } => {
156 write!(
157 f,
158 "checksum mismatch: header expected {expected:#010x}, computed {computed:#010x}",
159 )
160 }
161 Self::TruncatedHeader { needed, found } => {
162 write!(f, "truncated header: needed {needed} bytes, found {found}")
163 }
164 Self::TruncatedPayload { needed, found } => {
165 write!(f, "truncated payload: needed {needed} bytes, found {found}")
166 }
167 Self::InvalidMetric { tag } => {
168 write!(f, "invalid metric tag: {tag}")
169 }
170 Self::UnsupportedMetric { metric } => {
171 write!(f, "unsupported metric for this build: {metric:?}")
172 }
173 Self::InvalidIndexType { found, expected } => {
174 write!(
175 f,
176 "index type mismatch: file declared {found:?}, caller expected {expected:?}",
177 )
178 }
179 Self::InvalidPayload { reason } => {
180 write!(f, "invalid payload: {reason}")
181 }
182 Self::Compression { reason } => {
183 write!(f, "compression error: {reason}")
184 }
185 Self::IndexBuild(e) => write!(f, "index construction failed: {e}"),
186 Self::Unsupported {
187 feature,
188 available_in,
189 } => {
190 write!(
191 f,
192 "feature not supported in this build: {feature} (available in {available_in})",
193 )
194 }
195 }
196 }
197}
198
199impl std::error::Error for PersistError {
200 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
201 match self {
202 Self::Io { source, .. } => Some(source),
203 Self::IndexBuild(e) => Some(e),
204 _ => None,
205 }
206 }
207}
208
209impl ForgeError for PersistError {
210 fn kind(&self) -> &'static str {
211 match self {
212 Self::Io { .. } => "Io",
213 Self::BadMagic { .. } => "BadMagic",
214 Self::UnsupportedVersion { .. } => "UnsupportedVersion",
215 Self::ChecksumMismatch { .. } => "ChecksumMismatch",
216 Self::TruncatedHeader { .. } => "TruncatedHeader",
217 Self::TruncatedPayload { .. } => "TruncatedPayload",
218 Self::InvalidMetric { .. } => "InvalidMetric",
219 Self::UnsupportedMetric { .. } => "UnsupportedMetric",
220 Self::InvalidIndexType { .. } => "InvalidIndexType",
221 Self::InvalidPayload { .. } => "InvalidPayload",
222 Self::Compression { .. } => "Compression",
223 Self::IndexBuild(_) => "IndexBuild",
224 Self::Unsupported { .. } => "Unsupported",
225 }
226 }
227
228 fn caption(&self) -> &'static str {
229 match self {
230 Self::Io { .. } => "OS-level I/O failure on a snapshot file",
231 Self::BadMagic { .. } => "file is not an iqdb snapshot",
232 Self::UnsupportedVersion { .. } => {
233 "snapshot format version is not supported by this build"
234 }
235 Self::ChecksumMismatch { .. } => "payload CRC32 does not match the header",
236 Self::TruncatedHeader { .. } => "file ended before the full header could be read",
237 Self::TruncatedPayload { .. } => "file ended before the full payload could be read",
238 Self::InvalidMetric { .. } => {
239 "metric tag does not correspond to any known distance metric"
240 }
241 Self::UnsupportedMetric { .. } => "distance metric has no on-disk tag in this build",
242 Self::InvalidIndexType { .. } => {
243 "header's index-type tag does not match the caller's I"
244 }
245 Self::InvalidPayload { .. } => "payload bytes decoded to a structurally invalid index",
246 Self::Compression { .. } => "a compression or decompression step failed",
247 Self::IndexBuild(_) => "a downstream Index::new or insert returned an error",
248 Self::Unsupported { .. } => {
249 "the requested feature lands in a later version of iqdb-persist"
250 }
251 }
252 }
253}
254
255impl From<IqdbError> for PersistError {
256 fn from(value: IqdbError) -> Self {
257 Self::IndexBuild(value)
258 }
259}
260
261/// A specialized [`Result`](core::result::Result) whose error is
262/// [`PersistError`].
263///
264/// # Examples
265///
266/// ```
267/// use iqdb_persist::{Compression, PersistError, Result};
268///
269/// fn need_uncompressed(compression: Compression) -> Result<()> {
270/// if !matches!(compression, Compression::None) {
271/// return Err(PersistError::Unsupported {
272/// feature: "compression",
273/// available_in: "v0.4",
274/// });
275/// }
276/// Ok(())
277/// }
278///
279/// assert!(need_uncompressed(Compression::Lz4).is_err());
280/// assert!(need_uncompressed(Compression::None).is_ok());
281/// ```
282pub type Result<T> = core::result::Result<T, PersistError>;