emdb 1.0.3

Lightweight, high-performance embedded key-value database. Bitcask-style append-only journal, lock-free sharded hash index, at-rest encryption, sync + async APIs with streaming iterators.
Documentation
// Copyright 2026 James Gober. Licensed under Apache-2.0.

//! Error types for the `emdb` crate.
//!
//! All fallible operations return [`Result<T>`] — an alias for
//! `core::result::Result<T, Error>`. The [`Error`] type enumerates every
//! failure mode the crate can produce. Error codes are reserved under the
//! `EM-XXXXX` prefix in the wider Hive error registry.

use core::fmt;
use std::path::PathBuf;

/// Convenient `Result` alias where the error type is fixed to [`Error`].
pub type Result<T> = core::result::Result<T, Error>;

/// The top-level error type returned by every fallible operation in `emdb`.
///
/// The enum is `#[non_exhaustive]`; new variants may be added in minor
/// releases as new failure modes emerge. Callers must never write
/// exhaustive `match` arms over `Error` — always include a `_` arm.
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
    /// An invalid path was provided to a nested operation.
    ///
    /// This is returned when a nested API receives an empty prefix.
    /// Callers should provide a non-empty prefix and retry.
    #[cfg(feature = "nested")]
    InvalidPath,

    /// A TTL computation overflowed the representable `SystemTime` range.
    ///
    /// This occurs when adding a duration to the current wall clock exceeds
    /// the maximum representable timestamp. Callers should use a smaller TTL.
    #[cfg(feature = "ttl")]
    TtlOverflow,

    /// A lower-level I/O failure occurred.
    ///
    /// Callers should inspect the wrapped `std::io::ErrorKind` and decide
    /// whether retry, fallback, or surface-to-user behavior is appropriate.
    /// The kind and OS error code of failures reported by the storage
    /// substrate are preserved. A record larger than the 256 MiB journal
    /// frame cap is reported here with `ErrorKind::InvalidInput`. A write
    /// or sync failure poisons the journal: every later write, `flush`
    /// and `checkpoint` on the same handle fails, and the database must
    /// be reopened.
    Io(std::io::Error),

    /// The file exists but does not contain the emdb magic header.
    ///
    /// This usually means a non-emdb file was opened by mistake. The
    /// file is left untouched. Also returned for a database written by
    /// emdb before 0.9, whose single-file format this version does not
    /// read.
    MagicMismatch,

    /// The on-disk format version does not match this build.
    ///
    /// The file was likely written by a newer or incompatible emdb version.
    VersionMismatch {
        /// Version found in the file header.
        found: u32,
        /// Version expected by this build.
        expected: u32,
    },

    /// The file requires features not enabled in this build.
    ///
    /// Rebuild with the required features or open a compatible database file.
    FeatureMismatch {
        /// Feature bitmask stored in file header.
        file_flags: u32,
        /// Feature bitmask compiled into this build.
        build_flags: u32,
    },

    /// Corrupted or truncated data was detected while parsing storage records.
    ///
    /// When returned by an open, the journal holds valid records after a
    /// damaged region, so the damage is not a torn tail left by a crash.
    /// emdb refuses to open such a file instead of discarding the records
    /// that follow the damage; the file is left untouched. Restore from a
    /// backup, or keep a copy and truncate the file at `offset` to accept
    /// the loss of everything from that point on (see the "Recovery"
    /// section of `docs/ARCHITECTURE.md`).
    Corrupted {
        /// Byte offset where corruption was detected.
        offset: u64,
        /// Short reason string for diagnostics.
        reason: &'static str,
    },

    /// Invalid runtime configuration.
    ///
    /// This indicates programmer error when constructing the database.
    InvalidConfig(&'static str),

    /// Another process currently holds the advisory lock for this database.
    LockBusy {
        /// Path to the lockfile that is currently held.
        path: PathBuf,
    },

    /// Lockfile acquisition or lockfile I/O failed.
    LockfileError(std::io::Error),

    /// At-rest encryption configuration is invalid or AEAD failed
    /// internally.
    ///
    /// Distinct from [`Self::EncryptionKeyMismatch`]: this variant
    /// signals a problem the user cannot fix by supplying a
    /// different key (truncated buffer, malformed verification
    /// block, AEAD machinery failure). The database should be
    /// considered corrupt or the build mis-configured.
    #[cfg(feature = "encrypt")]
    Encryption(&'static str),

    /// The encryption key supplied to
    /// [`crate::EmdbBuilder::encryption_key`] does not match the key
    /// the database was created with.
    ///
    /// AEAD authentication failed on the verification block (or on a
    /// subsequent record). The database is fine; the caller supplied
    /// the wrong key.
    #[cfg(feature = "encrypt")]
    EncryptionKeyMismatch,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            #[cfg(feature = "nested")]
            Self::InvalidPath => f.write_str("emdb: invalid nested path"),
            #[cfg(feature = "ttl")]
            Self::TtlOverflow => f.write_str("emdb: ttl overflow"),
            Self::Io(err) => write!(f, "emdb: io error ({}): {err}", err.kind()),
            Self::MagicMismatch => f.write_str("emdb: file magic mismatch"),
            Self::VersionMismatch { found, expected } => {
                write!(f, "emdb: format version mismatch (found {}, expected {})", found, expected)
            }
            Self::FeatureMismatch {
                file_flags,
                build_flags,
            } => write!(
                f,
                "emdb: feature mismatch (file flags 0x{file_flags:08x}, build flags 0x{build_flags:08x})"
            ),
            Self::Corrupted { offset, reason } => {
                write!(f, "emdb: corrupted data at offset {} ({})", offset, reason)
            }
            Self::InvalidConfig(msg) => write!(f, "emdb: invalid configuration ({msg})"),
            Self::LockBusy { path } => {
                write!(f, "emdb: lock busy ({})", path.display())
            }
            Self::LockfileError(err) => {
                write!(f, "emdb: lockfile error ({}): {err}", err.kind())
            }
            #[cfg(feature = "encrypt")]
            Self::Encryption(msg) => write!(f, "emdb: encryption error ({msg})"),
            #[cfg(feature = "encrypt")]
            Self::EncryptionKeyMismatch => f.write_str(
                "emdb: encryption key mismatch (file was created with a different key)",
            ),
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Io(err) | Self::LockfileError(err) => Some(err),
            _ => None,
        }
    }
}

impl From<std::io::Error> for Error {
    fn from(value: std::io::Error) -> Self {
        Self::Io(value)
    }
}

/// Map an `fsys` error into [`Error::Io`].
///
/// `fsys::Error::Io` is passed through unchanged so the caller keeps
/// the `ErrorKind` and the OS error code (`ENOSPC`, `EFBIG`, ...). Any
/// other `fsys` error is wrapped in an `ErrorKind::Other` I/O error
/// (reachable through `std::io::Error::get_ref`), whose `Display` is the
/// fsys message.
pub(crate) fn from_fsys(err: fsys::Error) -> Error {
    match err {
        fsys::Error::Io(io) => Error::Io(io),
        other => Error::Io(std::io::Error::other(other)),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_error_implements_std_error() {
        fn assert_error<E: std::error::Error>() {}
        assert_error::<Error>();
    }

    #[test]
    fn test_io_error_display_includes_kind_and_message() {
        let err = Error::Io(std::io::Error::new(
            std::io::ErrorKind::PermissionDenied,
            "open /data/db.emdb",
        ));
        let msg = format!("{}", err);
        assert!(msg.contains("permission denied") || msg.contains("PermissionDenied"));
        assert!(msg.contains("open /data/db.emdb"));
    }

    #[test]
    fn test_source_exposes_wrapped_io_error() {
        use std::error::Error as _;
        let err = Error::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "gone"));
        let source = err.source().expect("io source");
        assert_eq!(source.to_string(), "gone");
        let lock = Error::LockfileError(std::io::Error::other("held"));
        assert!(lock.source().is_some());
        assert!(Error::MagicMismatch.source().is_none());
    }

    #[test]
    fn test_from_fsys_io_preserves_kind_and_os_code() {
        let raw = std::io::Error::from_raw_os_error(2);
        let kind = raw.kind();
        match from_fsys(fsys::Error::Io(raw)) {
            Error::Io(io) => {
                assert_eq!(io.kind(), kind);
                assert_eq!(io.raw_os_error(), Some(2));
            }
            other => panic!("expected Io, got {other:?}"),
        }
    }

    #[test]
    fn test_from_fsys_other_keeps_inner_error() {
        match from_fsys(fsys::Error::QueueFull) {
            Error::Io(io) => {
                assert_eq!(io.kind(), std::io::ErrorKind::Other);
                let inner = io.get_ref().expect("wrapped fsys error");
                assert!(inner.downcast_ref::<fsys::Error>().is_some());
            }
            other => panic!("expected Io, got {other:?}"),
        }
    }

    #[test]
    fn test_version_mismatch_display_is_stable() {
        let msg = format!(
            "{}",
            Error::VersionMismatch {
                found: 2,
                expected: 1,
            }
        );
        assert!(msg.contains("found 2"));
        assert!(msg.contains("expected 1"));
    }

    #[test]
    fn test_corrupted_display_includes_offset_and_reason() {
        let msg = format!(
            "{}",
            Error::Corrupted {
                offset: 42,
                reason: "crc mismatch",
            }
        );
        assert!(msg.contains("42"));
        assert!(msg.contains("crc mismatch"));
    }

    #[test]
    fn test_from_io_maps_to_io_variant() {
        let err: Error = std::io::Error::new(std::io::ErrorKind::NotFound, "missing").into();
        assert!(matches!(err, Error::Io(_)));
    }

    #[test]
    fn test_lock_errors_display_are_stable() {
        let busy = format!(
            "{}",
            Error::LockBusy {
                path: std::path::PathBuf::from("/tmp/demo.lock"),
            }
        );
        assert!(busy.contains("lock busy"));

        let io_msg = format!("{}", Error::LockfileError(std::io::Error::other("x")));
        assert!(io_msg.contains("lockfile error"));
    }

    #[cfg(feature = "nested")]
    #[test]
    fn test_invalid_path_display_is_stable() {
        let msg = format!("{}", Error::InvalidPath);
        assert_eq!(msg, "emdb: invalid nested path");
    }

    #[cfg(feature = "ttl")]
    #[test]
    fn test_ttl_overflow_display_is_stable() {
        let msg = format!("{}", Error::TtlOverflow);
        assert_eq!(msg, "emdb: ttl overflow");
    }
}