Skip to main content

lsm_tree/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-present, fjall-rs
3// Copyright (c) 2026-present, Dmitry Prudnikov
4
5use crate::{Checksum, CompressionType, SeqNo};
6#[cfg(not(feature = "std"))]
7use alloc::string::String;
8
9/// Represents errors that can occur in the LSM-tree
10#[derive(Debug)]
11#[non_exhaustive]
12pub enum Error {
13    /// I/O error
14    Io(crate::io::Error),
15
16    /// Decompression failed
17    Decompress(CompressionType),
18
19    /// Invalid or unparsable data format version
20    InvalidVersion(u8),
21
22    /// Some required files could not be recovered from disk
23    Unrecoverable,
24
25    /// The operation was aborted on a caller's cooperative cancellation
26    /// request ([`RecoveryProgress::request_cancel`]).
27    ///
28    /// A repair cancelled BEFORE its manifest commit left the directory
29    /// untouched (the scan is read-only), so a retry re-derives everything
30    /// from the same bytes; a cancel requested after the commit is ignored
31    /// rather than reported, since the rebuilt manifest is already durable.
32    ///
33    /// [`RecoveryProgress::request_cancel`]: crate::RecoveryProgress::request_cancel
34    Cancelled,
35
36    /// The read resolved into an extent that was physically EXCISED — a
37    /// hole-punched region that reads back as zeros.
38    ///
39    /// Tight-space reclaim punches consumed extents in place, and manifest
40    /// recovery may keep a table whose surviving blocks are intact around
41    /// such a hole rather than discard the whole file. Reading one of the
42    /// punched blocks is a genuine, permanent loss of exactly those rows, and
43    /// it is reported as such instead of being smuggled out as a checksum
44    /// mismatch (which reads as "the bytes rotted") or, worse, as "no such
45    /// key" — the latter would fall through to a superseded version in a
46    /// lower level and silently resurrect it.
47    ///
48    /// This is the engine's equivalent of a filesystem returning `EIO` for a
49    /// bad extent while the rest of the file keeps reading.
50    Excised {
51        /// Byte offset of the excised block within the file.
52        offset: u64,
53    },
54
55    /// Checksum mismatch
56    ChecksumMismatch {
57        /// Checksum of loaded block
58        got: Checksum,
59
60        /// Checksum that was saved in block header
61        expected: Checksum,
62    },
63
64    /// A memtable entry's per-KV digest, computed at insert under
65    /// [`KvChecksumComputePoint::AtInsert`](crate::runtime_config::KvChecksumComputePoint::AtInsert),
66    /// did not match a recompute over the entry's current bytes at flush.
67    ///
68    /// This is the memtable-residence RAM-corruption signal: the entry's
69    /// logical content (`value_type`, `seqno`, key, or value) changed while it
70    /// sat in the memtable, between insert and flush. Distinct from
71    /// [`Self::ChecksumMismatch`] (on-disk block bytes) — this catches a flip
72    /// that happens entirely in RAM, before any block is written.
73    MemtableKvChecksumMismatch {
74        /// Sequence number of the entry whose digest diverged (locates it).
75        seqno: u64,
76
77        /// Digest recomputed over the entry's current memtable bytes at flush.
78        got: u64,
79
80        /// Digest computed and stored when the entry was inserted.
81        expected: u64,
82    },
83
84    /// A memtable entry carried an insert-time per-KV digest
85    /// ([`KvChecksumComputePoint::AtInsert`](crate::runtime_config::KvChecksumComputePoint::AtInsert))
86    /// tagged with an algorithm `AtInsert` never stores: a non-4-byte or
87    /// unknown algorithm wire tag.
88    ///
89    /// `AtInsert` only ever writes a 4-byte algorithm tag (`Xxh3Low32` /
90    /// `Crc32c`), so a digest-bearing node tagged otherwise means the node's
91    /// algorithm metadata was corrupted in RAM during memtable residence.
92    /// Distinct from [`Self::MemtableKvChecksumMismatch`] (the digest value
93    /// diverged): here the algorithm itself is unusable, so the engine refuses
94    /// to "verify" the entry under the wrong algorithm rather than risk a
95    /// flipped tag passing the residence check.
96    MemtableKvChecksumCorruptAlgorithm {
97        /// Sequence number of the entry whose algorithm tag is invalid.
98        seqno: u64,
99
100        /// The invalid algorithm wire tag read from the node.
101        tag: u8,
102    },
103
104    /// A repair COMMITTED its rebuilt manifest, but a later step failed: the
105    /// repair's own post-commit cleanup, or the follow-up open inside
106    /// [`Config::open_or_repair`](crate::Config::open_or_repair).
107    ///
108    /// The report travels WITH the error because it exists only in that call:
109    /// the retry finds a healthy manifest (its open sweeps any cleanup
110    /// leftover itself), runs no repair, and answers `None` — an
111    /// external-WAL consumer would otherwise never learn its replay
112    /// obligation
113    /// ([`RepairReport::wal_replay_scope`](crate::RepairReport::wal_replay_scope))
114    /// and could keep stale or resurrected values. Consume the report exactly
115    /// as a successful repair's, then retry the open (`cause` is typically
116    /// transient).
117    #[cfg(feature = "std")]
118    RepairedButUnopened {
119        /// The completed repair's report — not rederivable on a retry.
120        report: alloc::boxed::Box<crate::RepairReport>,
121
122        /// The follow-up open's failure.
123        cause: alloc::boxed::Box<Self>,
124    },
125
126    /// The on-disk tree's type does not match the type the configuration
127    /// requests: a standard open (no `kv_separation_opts`) of a KV-separated
128    /// (blob) tree, or the reverse.
129    ///
130    /// This is a CONFIGURATION error — fix the options and reopen — not
131    /// damage, so [`Config::open_or_repair`](crate::Config::open_or_repair)
132    /// propagates it instead of repairing: a rebuild under the mismatched
133    /// type would commit a manifest of the wrong tree shape (a `Standard`
134    /// rebuild of a blob tree strands every blob file behind SSTs full of
135    /// indirections, and the orphan sweep then deletes them).
136    TreeTypeMismatch {
137        /// The type the configuration requested.
138        requested: crate::TreeType,
139
140        /// The type the on-disk tree actually has.
141        actual: crate::TreeType,
142    },
143
144    /// Blob frame header CRC mismatch (V4 format).
145    /// Distinct from `ChecksumMismatch` which covers data payload checksums.
146    HeaderCrcMismatch {
147        /// CRC recomputed from header fields
148        recomputed: u32,
149
150        /// CRC stored in the blob frame header
151        stored: u32,
152    },
153
154    /// Invalid enum tag
155    InvalidTag((&'static str, u8)),
156
157    /// Invalid block trailer
158    InvalidTrailer,
159
160    /// Invalid block header
161    InvalidHeader(&'static str),
162
163    /// Data size (decompressed, on-disk, or requested) is invalid or exceeds a safety limit
164    DecompressedSizeTooLarge {
165        /// Size associated with the data being processed. This may come from
166        /// on-disk/in-memory metadata (e.g., header, block/value handle) or be
167        /// derived from caller input (e.g., a requested key or value length),
168        /// and may be zero, invalid, or over the configured limit.
169        declared: u64,
170
171        /// Maximum allowed size for the data or request being processed
172        limit: u64,
173    },
174
175    /// UTF-8 error
176    Utf8(core::str::Utf8Error),
177
178    /// Merge operator failed.
179    ///
180    /// No context payload — consistent with other unit variants
181    /// (`Unrecoverable`, `InvalidTrailer`). Operators should log
182    /// details before returning this error.
183    MergeOperator,
184
185    /// Encryption failed
186    Encrypt(&'static str),
187
188    /// Decryption failed
189    Decrypt(&'static str),
190
191    /// Comparator mismatch on tree reopen.
192    ///
193    /// The tree was created with a comparator whose [`crate::UserComparator::name`]
194    /// differs from the one supplied at reopen time.
195    ComparatorMismatch {
196        /// Comparator name persisted in the tree metadata.
197        stored: String,
198
199        /// Comparator name supplied by the caller.
200        supplied: &'static str,
201    },
202
203    /// Zstd dictionary required but not provided, or `dict_id` mismatch
204    ZstdDictMismatch {
205        /// Dictionary ID stored in the block/table metadata
206        expected: u32,
207
208        /// Dictionary ID provided by the caller (`None` if no dictionary supplied)
209        got: Option<u32>,
210    },
211
212    /// Per-record XXH3-64 mismatch inside a framed manifest section
213    /// (`tables` / `blob_files`). Distinct from
214    /// [`Error::ChecksumMismatch`] — same XXH3 family but a
215    /// different output width (XXH3-64 here vs XXH3-128 for
216    /// block-level payloads) on a different layer of the on-disk
217    /// format, with different recovery semantics (manifest framing
218    /// surfaces routed through `ManifestRecoveryMode`; block
219    /// checksums surface via `Error::ChecksumMismatch` for the
220    /// block I/O paths). Strict manifest recovery modes surface
221    /// this so an operator can see the exact 64-bit digests that
222    /// disagreed; `SkipAnyCorruptedRecords` and
223    /// `PointInTimeRecovery` route around the corruption without
224    /// raising it.
225    ManifestFrameChecksumMismatch {
226        /// SFA section the corrupt record was found in (e.g.
227        /// `"tables"`, `"blob_files"`). Static so this can be
228        /// compared without parsing message strings.
229        section: &'static str,
230        /// XXH3-64 digest the framing header claimed for the
231        /// record's payload.
232        expected: u64,
233        /// XXH3-64 digest the reader recomputed over the bytes
234        /// actually on disk.
235        got: u64,
236    },
237
238    /// Range tombstone block decode failure.
239    RangeTombstoneDecode {
240        /// Which field or validation failed (e.g. `start_len`, `start`, `seqno`, `interval`)
241        field: &'static str,
242
243        /// Byte offset within the block to the start of the field whose decoding failed
244        /// (captured before reading bytes for that field).
245        offset: u64,
246    },
247
248    /// A [`WriteBatch`](crate::WriteBatch) contains mixed operation types
249    /// (e.g. insert + remove) for the same user key.
250    ///
251    /// Mixed ops at the same logical version are rejected because the
252    /// memtable/skiplist ordering ties on `(user_key, seqno)` and does not
253    /// include `value_type` as a tie-breaker. That would otherwise make
254    /// equal-key entries with different operation types ambiguous to later
255    /// reads and merges, yielding tie-break-dependent "last write wins"
256    /// semantics.
257    MixedOperationBatch,
258
259    /// Tree was opened with `Config::page_ecc(true)` but this build of
260    /// the crate does not have the `page_ecc` cargo feature enabled.
261    /// The reader has no way to verify or recover Reed-Solomon parity
262    /// without the codec, so opening such a tree would silently
263    /// downgrade integrity guarantees — return this error instead.
264    PageEccUnsupported,
265
266    /// Block payload failed the XXH3 integrity check and the
267    /// attached Reed-Solomon parity trailer could not reconstruct
268    /// it (more shards are corrupted than the (4, 2) RS scheme
269    /// can recover). Surfaced ONLY by ECC-protected blocks
270    /// (the `ECC_PARITY` header flag set); a block written without parity
271    /// (`Config::page_ecc(false)`) on a checksum mismatch returns
272    /// [`Self::ChecksumMismatch`] instead, because there's no
273    /// parity to even attempt recovery from.
274    PageEccUnrecoverable {
275        /// XXH3 checksum recomputed from the on-disk bytes.
276        got: Checksum,
277        /// XXH3 checksum stored in the block header.
278        expected: Checksum,
279    },
280
281    /// Route-compatibility mismatch on reopen.
282    ///
283    /// Recovery found fewer tables on disk than the manifest expects, and all
284    /// missing tables are on levels not covered by any current
285    /// [`level_routes`](crate::Config::level_routes).  This typically means a
286    /// previously configured route was removed, leaving its directory
287    /// unreachable.
288    ///
289    /// Re-adding the missing route(s) will usually resolve the error.  If
290    /// missing tables are on levels that *are* covered by a current route,
291    /// recovery returns [`Unrecoverable`](Self::Unrecoverable) instead
292    /// (the SST files were genuinely lost).
293    RouteMismatch {
294        /// Number of tables listed in the manifest.
295        expected: usize,
296
297        /// Number of tables actually found across all configured routes.
298        found: usize,
299    },
300
301    /// Valid configuration / on-disk layout that this build does not
302    /// yet know how to process, or constructor input that violates a
303    /// documented invariant (e.g. `CompressionType::None` passed to
304    /// [`crate::table::block::CompressionContext::new`]). Distinct
305    /// from [`Error::Unrecoverable`] (signals corruption) and from
306    /// [`Error::Io`] with `ErrorKind::Unsupported` (which can also
307    /// surface from platform / backend limits); the `&'static str`
308    /// payload names the specific marker that triggered the rejection
309    /// (e.g. `"filter_tli"` for a partitioned filter SFA section,
310    /// `"compression-context-none"` for the constructor invariant) so
311    /// the caller can route the diagnostic without parsing message
312    /// strings.
313    FeatureUnsupported(&'static str),
314
315    /// The tree directory is already locked by another live instance.
316    ///
317    /// Returned by [`Config::open`](crate::Config::open) and
318    /// [`Config::repair`](crate::Config::repair) when the cross-process
319    /// directory lock (a `LOCK` file under the tree directory, held via an
320    /// advisory OS file lock) could not be acquired because another process
321    /// owns it. Holds the directory path as a display string for diagnostics.
322    /// Two processes mutating the same manifest would corrupt it, so the second
323    /// acquirer fails fast here. Disable the lock with
324    /// [`Config::with_directory_lock`](crate::Config::with_directory_lock) only
325    /// when exclusivity is enforced at a higher layer.
326    Locked(String),
327
328    /// Manifest footer / TOC / file-level discovery failure.
329    ///
330    /// Scoped to errors detected at or before the TOC is parsed —
331    /// i.e. everything the reader needs *before* it can answer
332    /// "where is section X". Section-content failures (a specific
333    /// section's Block fails verification) go through
334    /// [`ManifestSectionInvalid`](Error::ManifestSectionInvalid)
335    /// instead so callers like `Tree::open` can distinguish a
336    /// totally unreadable manifest from a per-section problem.
337    ///
338    /// Typical causes:
339    ///
340    /// - **Footer-payload structural failure:** unknown layout
341    ///   version, oversized section count, empty/oversized section
342    ///   name, invalid UTF-8, duplicate section name, footer
343    ///   payload exceeds the 4 KiB reservation.
344    /// - **Tail / head-mirror double failure:** both the
345    ///   tail-footer Block read and the head-mirror fallback
346    ///   failed verification (XXH3 mismatch, AEAD decryption,
347    ///   parse error). Per-path causes are logged at `error`
348    ///   level and collapsed here.
349    /// - **TOC entry value corruption:** a TOC entry's
350    ///   `block_offset + block_size` overflows `u64` or extends
351    ///   past the end of the file. The TOC bytes are footer
352    ///   payload, so a malformed TOC entry is a footer-level
353    ///   issue even though it surfaces in
354    ///   `ManifestArchiveReader::read_section`.
355    /// - **Trailing size-hint corruption:** the tail's 4-byte
356    ///   footer-size hint is zero or exceeds
357    ///   `HEAD_FOOTER_RESERVED_SIZE` (4 KiB), or the implied
358    ///   `section_end` lands inside the head reservation. Caught
359    ///   in both the reader and `checkpoint::write_current_for_version`.
360    /// - **Writer-side invariant breach:** `write_cursor` would
361    ///   overflow `u64`, an in-memory section would exceed the
362    ///   on-disk Block-size cap, etc.
363    /// - **CURRENT pointer points at a missing manifest:** when
364    ///   `version::get_current_version` opens the referenced
365    ///   `v{N}` file and gets `NotFound`, the error is rewrapped
366    ///   here so `Tree::open`'s outer `Io(NotFound) => create_new`
367    ///   arm cannot mistake a half-applied recovery / corrupted
368    ///   state for a clean first-open.
369    ManifestFooterInvalid(&'static str),
370
371    /// Manifest section content failed verification or matched no
372    /// TOC entry.
373    ///
374    /// Surfaced by `ManifestArchiveReader::read_section` (and the
375    /// helper that validates the inner Block header before
376    /// delegating to `Block::from_reader`). Distinct from
377    /// [`ManifestFooterInvalid`](Error::ManifestFooterInvalid)
378    /// because the footer / TOC loaded fine — the bad bytes are
379    /// localised to one section Block and a caller MAY route
380    /// recovery differently (e.g. skip the section vs. refuse
381    /// the whole manifest).
382    ///
383    /// Causes:
384    ///
385    /// - **Requested section name not in TOC:** the caller asked
386    ///   for a section that the manifest doesn't declare.
387    /// - **Section Block header doesn't fit its outer buffer:**
388    ///   the inner block's derived on-disk size (header + payload +
389    ///   parity-if-flagged) exceeds the TOC-declared `block_size`.
390    ///   Defence-in-depth against a forged TOC pointing at a too-small
391    ///   slot.
392    /// - **Block decoded at the TOC offset has the wrong
393    ///   `block_type`:** TOC says "section here" but the bytes
394    ///   carry a non-`Manifest` Block. Defence-in-depth against
395    ///   TOC-redirect attacks; once AAD-binding lands in
396    ///   `encryption::block`, `Block::from_reader` will reject
397    ///   this internally and the check here becomes belt-and-
398    ///   braces.
399    ManifestSectionInvalid(&'static str),
400
401    /// The trailing record of the incremental manifest edit log is
402    /// incomplete or corrupt, and the active
403    /// [`ManifestRecoveryMode`](crate::config::ManifestRecoveryMode) does
404    /// not tolerate that defect, so the open aborts rather than silently
405    /// rolling the edit back.
406    ///
407    /// A clean end-of-log is never reported here: a crash exactly at a
408    /// record boundary is byte-identical to a pristine close, so that
409    /// case is always tolerated. This fires when bytes of a trailing
410    /// record are present but the record fails framing — a
411    /// power-loss-truncated append (only
412    /// [`AbsoluteConsistency`](crate::config::ManifestRecoveryMode::AbsoluteConsistency)
413    /// rejects it; other modes roll it back), or a fully-framed record
414    /// whose checksum doesn't match (bit-rot) / whose header is forged
415    /// (rejected by both `AbsoluteConsistency` and
416    /// [`TolerateCorruptedTailRecords`](crate::config::ManifestRecoveryMode::TolerateCorruptedTailRecords),
417    /// which salvages writer-incomplete tails only; rolled back under
418    /// `PointInTimeRecovery` / `SkipAnyCorruptedRecords`).
419    ///
420    /// Recover by truncating the torn tail: run
421    /// [`Config::repair`](crate::Config::repair), which rebuilds a clean
422    /// standalone snapshot (dropping the edit log), or re-open under a
423    /// [`ManifestRecoveryMode`](crate::config::ManifestRecoveryMode) that
424    /// tolerates the defect to roll the trailing edit back.
425    TornManifestEditLog {
426        /// The trailing defect detected: `"truncated"` (partial record
427        /// from a power-loss-interrupted append), `"checksum-mismatch"`
428        /// (fully-framed record whose payload bit-rotted),
429        /// `"bad-header"` (implausible framing length), or
430        /// `"len-mismatch"` (record length disagrees with the expected
431        /// fixed size). Static so callers can branch without parsing
432        /// the message string.
433        kind: &'static str,
434    },
435
436    /// A write was declined by the storage admission gate because accepting it
437    /// could push the tree's live footprint past its effective budget.
438    ///
439    /// Only produced when [`storage_admission_check`](crate::runtime_config::RuntimeConfig::storage_admission_check)
440    /// is enabled. The predicate is computed, not latched: raising
441    /// [`storage_limit_bytes`](crate::runtime_config::RuntimeConfig::storage_limit_bytes),
442    /// freeing disk, or a compaction reclaiming space clears the read-only
443    /// state on the next check with no restart. Internal flush / compaction are
444    /// never gated (reserved headroom), so the engine can always reclaim space.
445    StorageFull {
446        /// Live on-disk bytes at the time of the check.
447        used: u64,
448
449        /// Effective byte budget that `used` (plus reserved headroom) exceeded.
450        limit: u64,
451    },
452
453    /// A read asked for a snapshot whose version the history no longer
454    /// retains.
455    ///
456    /// The engine serves a snapshot at seqno `s` from the newest retained
457    /// version installed BELOW `s`. Compaction maintenance prunes the history
458    /// up to the newest version below the caller's GC watermark
459    /// ([`AbstractTree::major_compact`](crate::AbstractTree::major_compact)'s
460    /// `seqno_threshold`), and [`AbstractTree::clear`](crate::AbstractTree::clear)
461    /// drains it to the new empty version, so afterwards every snapshot at or
462    /// below the oldest retained version's seqno has nothing to be served
463    /// from. Serving it from the oldest retained version instead would
464    /// silently answer with data the snapshot never saw, so the read is
465    /// refused. Snapshot `0` is the exception: it sees no entry from any
466    /// version and is always served (empty). The boundary is persisted with
467    /// the manifest, so the refusal holds across a reopen as well.
468    ///
469    /// Point reads return it directly; iterators yield it as their first and
470    /// only item. [`AbstractTree::oldest_retained_seqno`](crate::AbstractTree::oldest_retained_seqno)
471    /// exposes the boundary so a caller can validate a snapshot before
472    /// reading: a snapshot is servable iff it is `0` or strictly above that
473    /// seqno.
474    SnapshotBelowRetention {
475        /// The snapshot seqno the read asked for.
476        requested: SeqNo,
477
478        /// Seqno of the oldest version the history still retains; reads at
479        /// `oldest_retained + 1` and above are servable.
480        oldest_retained: SeqNo,
481    },
482}
483
484impl core::fmt::Display for Error {
485    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
486        write!(f, "LsmTreeError: {self:?}")
487    }
488}
489
490impl core::error::Error for Error {
491    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
492        match self {
493            Self::Io(e) => Some(e),
494            // The variant is explicitly a causal wrapper: generic error-chain
495            // logging and transient-retry classifiers must be able to reach
496            // the post-commit step's own failure through the standard chain.
497            #[cfg(feature = "std")]
498            Self::RepairedButUnopened { cause, .. } => Some(&**cause),
499            _ => None,
500        }
501    }
502}
503
504impl From<crate::sfa::Error> for Error {
505    fn from(value: crate::sfa::Error) -> Self {
506        match value {
507            crate::sfa::Error::Io(e) => Self::from(e),
508            crate::sfa::Error::ChecksumMismatch { got, expected } => {
509                log::error!("Archive ToC checksum mismatch");
510                Self::ChecksumMismatch {
511                    got: got.into(),
512                    expected: expected.into(),
513                }
514            }
515            crate::sfa::Error::InvalidHeader => {
516                log::error!("Invalid archive header");
517                Self::Unrecoverable
518            }
519            crate::sfa::Error::InvalidVersion => {
520                log::error!("Invalid archive version");
521                Self::Unrecoverable
522            }
523            crate::sfa::Error::UnsupportedChecksumType => {
524                log::error!("Invalid archive checksum type");
525                Self::Unrecoverable
526            }
527        }
528    }
529}
530
531impl Error {
532    /// Whether this failure says something about the ENVIRONMENT or the
533    /// CALLER's configuration rather than about the bytes on disk — the class
534    /// every recovery path must PROPAGATE instead of recording as damage.
535    ///
536    /// Recording one of these as damage commits a manifest that omits the file
537    /// and then removes it, turning a fixable mistake into permanent loss;
538    /// propagating lets the operator fix the environment (or supply the right
539    /// key / dictionary) and re-run with everything still on disk.
540    ///
541    /// - [`Self::Io`] of an environmental kind: the interrupted-syscall
542    ///   retryables, plus access failures that do not implicate the bytes
543    ///   (`PermissionDenied`, `StorageFull`, `QuotaExceeded`,
544    ///   `ReadOnlyFilesystem`, `OutOfMemory`).
545    /// - [`Self::Decrypt`]: an AEAD failure is exactly what a missing or wrong
546    ///   key produces on perfectly healthy ciphertext.
547    /// - [`Self::ZstdDictMismatch`]: the persisted descriptor names a
548    ///   dictionary the caller did not supply, or supplied a different one.
549    ///
550    /// A failure that DOES implicate the bytes (a bad sector, a structural
551    /// decode failure) is not in this class: a retry cannot fix it, and the
552    /// recovery paths grade that file instead of aborting over it.
553    #[must_use]
554    pub(crate) fn is_environmental(&self) -> bool {
555        match self {
556            Self::Io(io) => io.kind().is_environmental(),
557            Self::Decrypt(_) => true,
558            #[cfg(zstd_any)]
559            Self::ZstdDictMismatch { .. } => true,
560            _ => false,
561        }
562    }
563}
564
565// The `Io` variant carries `crate::io::Error` (the no_std-capable I/O error),
566// so this bridge is a direct wrap. Std file-I/O paths surface `std::io::Error`;
567// the std-gated bridge below folds those through `crate::io::Error`.
568impl From<crate::io::Error> for Error {
569    fn from(value: crate::io::Error) -> Self {
570        Self::Io(value)
571    }
572}
573
574#[cfg(feature = "std")]
575impl From<std::io::Error> for Error {
576    fn from(value: std::io::Error) -> Self {
577        Self::Io(value.into())
578    }
579}
580
581/// Tree result
582pub type Result<T> = core::result::Result<T, Error>;