Skip to main content

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>;