use core::fmt;
use std::path::PathBuf;
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
#[cfg(feature = "nested")]
InvalidPath,
#[cfg(feature = "ttl")]
TtlOverflow,
Io(std::io::Error),
MagicMismatch,
VersionMismatch {
found: u32,
expected: u32,
},
FeatureMismatch {
file_flags: u32,
build_flags: u32,
},
Corrupted {
offset: u64,
reason: &'static str,
},
InvalidConfig(&'static str),
LockBusy {
path: PathBuf,
},
LockfileError(std::io::Error),
#[cfg(feature = "encrypt")]
Encryption(&'static str),
#[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)
}
}
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");
}
}