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