use crate::superblock::SlotDefect;
use std::fmt;
use std::io;
#[non_exhaustive]
#[derive(Debug)]
pub enum ChiselError {
InvalidHandle(u64),
NoActiveTransaction,
TransactionAlreadyActive,
SavepointNotFound(String),
DuplicateSavepoint(String),
ReadOnlyMode,
FileNotFound,
InvalidRootName,
RootNameTableFull,
InvalidSuperblockCount {
value: u32,
},
CacheFull {
limit: usize,
},
SpillwayFull {
limit_bytes: u64,
},
TransactionInProgress,
TagMismatch {
handle: u64,
expected: u32,
actual: u32,
},
IoError(io::Error),
ChecksumMismatch {
page_id: u64,
},
CorruptSuperblock {
defects: Vec<SlotDefect>,
},
FileSizeMismatch {
expected: u64,
actual: u64,
},
LockFailed,
UnsupportedFormatVersion {
found: u32,
expected: u32,
},
Poisoned,
CorruptPage {
page_id: u64,
},
InvalidPageId {
page_id: u64,
},
UnsupportedPageSize {
stored: u32,
compiled: u32,
},
NoEncryptionKey,
InvalidEncryptionKey,
EncryptionNotSupported,
NoFreeKeySlot,
LastKeySlot,
DecryptionFailed {
page_id: u64,
},
}
impl ChiselError {
pub fn is_fatal(&self) -> bool {
matches!(
self,
ChiselError::IoError(_)
| ChiselError::ChecksumMismatch { .. }
| ChiselError::CorruptSuperblock { .. }
| ChiselError::FileSizeMismatch { .. }
| ChiselError::LockFailed
| ChiselError::UnsupportedFormatVersion { .. }
| ChiselError::CorruptPage { .. }
| ChiselError::InvalidPageId { .. }
| ChiselError::UnsupportedPageSize { .. }
| ChiselError::DecryptionFailed { .. }
)
}
}
impl fmt::Display for ChiselError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ChiselError::InvalidHandle(h) => write!(f, "invalid handle: {h}"),
ChiselError::TagMismatch { handle, expected, actual } => {
write!(f, "handle {handle} has tag {actual}, not the expected {expected}")
}
ChiselError::NoActiveTransaction => write!(f, "no active transaction"),
ChiselError::TransactionAlreadyActive => write!(f, "transaction already active"),
ChiselError::SavepointNotFound(name) => write!(f, "savepoint not found: {name}"),
ChiselError::DuplicateSavepoint(name) => write!(f, "duplicate savepoint: {name}"),
ChiselError::ReadOnlyMode => write!(f, "database is read-only"),
ChiselError::FileNotFound => write!(f, "database file not found"),
ChiselError::InvalidRootName => write!(
f,
"invalid named-root name (empty, too long, non-UTF-8, or contains NUL)"
),
ChiselError::RootNameTableFull => {
write!(f, "named-root table is full (all slots are in use)")
}
ChiselError::InvalidSuperblockCount { value } => {
write!(f, "invalid superblock_count {value}; must be in 2..=16")
}
ChiselError::CacheFull { limit } => write!(
f,
"page cache full: {limit} dirty pages held; commit or roll back to free cache"
),
ChiselError::SpillwayFull { limit_bytes } => {
if *limit_bytes == 0 {
write!(
f,
"spillway is disabled (spillway_max_bytes=0); commit or roll back to free cache"
)
} else {
write!(
f,
"spillway full: {limit_bytes}-byte limit reached; commit or roll back to free cache and spillway"
)
}
},
ChiselError::TransactionInProgress => write!(
f,
"configuration changes are only allowed between transactions; commit or roll back first"
),
ChiselError::IoError(e) => write!(f, "I/O error: {e}"),
ChiselError::ChecksumMismatch { page_id } => {
write!(f, "checksum mismatch on page {page_id}")
}
ChiselError::CorruptSuperblock { defects } => {
write!(f, "no valid superblock found")?;
for (i, sd) in defects.iter().enumerate() {
let sep = if i == 0 { ":" } else { ";" };
write!(f, "{sep} slot {}: {}", sd.slot, sd.defect)?;
}
Ok(())
}
ChiselError::FileSizeMismatch { expected, actual } => {
write!(
f,
"file size mismatch: expected {expected} bytes, got {actual}"
)
}
ChiselError::LockFailed => write!(f, "failed to acquire exclusive file lock"),
ChiselError::UnsupportedFormatVersion { found, expected } => write!(
f,
"unsupported on-disk format version: found {found}, this build supports {expected}"
),
ChiselError::Poisoned => write!(
f,
"database handle is poisoned after a previous fatal error; drop and reopen"
),
ChiselError::CorruptPage { page_id } => {
write!(f, "corrupt page structure at page {page_id}")
}
ChiselError::InvalidPageId { page_id } => {
write!(f, "invalid page id {page_id} (out of range for file)")
}
ChiselError::UnsupportedPageSize { stored, compiled } => write!(
f,
"page size mismatch: file was written with {stored}-byte pages, this build uses {compiled}-byte pages"
),
ChiselError::NoEncryptionKey => write!(
f,
"database is encrypted but no encryption_key was supplied"
),
ChiselError::InvalidEncryptionKey => write!(
f,
"encryption key does not match any key slot (wrong passphrase or raw key)"
),
ChiselError::EncryptionNotSupported => write!(
f,
"encryption not supported for this open (key supplied for a plaintext database, or unknown crypto algorithm)"
),
ChiselError::NoFreeKeySlot => write!(
f,
"no free key slot: all 8 key slots are occupied (remove an unused key first)"
),
ChiselError::LastKeySlot => write!(
f,
"refusing to remove the last active key slot (the database would become permanently unopenable)"
),
ChiselError::DecryptionFailed { page_id } => {
write!(f, "decryption/authentication failed for page {page_id}")
}
}
}
}
impl std::error::Error for ChiselError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ChiselError::IoError(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for ChiselError {
fn from(e: io::Error) -> Self {
match e.kind() {
io::ErrorKind::NotFound => ChiselError::FileNotFound,
_ => ChiselError::IoError(e),
}
}
}
impl From<crate::crypto::CryptoError> for ChiselError {
fn from(_: crate::crypto::CryptoError) -> Self {
ChiselError::InvalidEncryptionKey
}
}
pub type Result<T> = std::result::Result<T, ChiselError>;
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn ioerror_source_returns_inner_io_error() {
let inner = io::Error::new(io::ErrorKind::PermissionDenied, "denied");
let e: ChiselError = inner.into();
let src = e
.source()
.expect("IoError must expose its inner error via source()");
let downcast: &io::Error = src
.downcast_ref()
.expect("source() should downcast back to io::Error");
assert_eq!(downcast.kind(), io::ErrorKind::PermissionDenied);
}
#[test]
fn non_io_variants_have_no_source() {
let cases: [ChiselError; 5] = [
ChiselError::InvalidHandle(0),
ChiselError::NoActiveTransaction,
ChiselError::ChecksumMismatch { page_id: 0 },
ChiselError::CorruptSuperblock { defects: vec![] },
ChiselError::Poisoned,
];
for e in &cases {
assert!(
e.source().is_none(),
"expected source() == None for {e:?}, got Some(...)"
);
}
}
#[test]
fn corrupt_page_variant_contract() {
let e = ChiselError::CorruptPage { page_id: 42 };
assert!(
e.is_fatal(),
"CorruptPage must be fatal so the manager poisons"
);
assert!(e.source().is_none(), "CorruptPage has no inner source");
let msg = format!("{e}");
assert!(
msg.contains("42"),
"Display message {msg:?} should mention page id 42"
);
}
#[test]
fn corrupt_superblock_display_format() {
use crate::superblock::{SlotDefect, SuperblockDefect};
let empty = ChiselError::CorruptSuperblock { defects: vec![] };
assert_eq!(format!("{empty}"), "no valid superblock found");
let one = ChiselError::CorruptSuperblock {
defects: vec![SlotDefect {
slot: 0,
defect: SuperblockDefect::BadChecksum,
}],
};
assert_eq!(
format!("{one}"),
"no valid superblock found: slot 0: bad checksum"
);
let many = ChiselError::CorruptSuperblock {
defects: vec![
SlotDefect {
slot: 0,
defect: SuperblockDefect::BadChecksum,
},
SlotDefect {
slot: 1,
defect: SuperblockDefect::BadMagic,
},
SlotDefect {
slot: 2,
defect: SuperblockDefect::BadCount(99),
},
],
};
assert_eq!(
format!("{many}"),
"no valid superblock found: slot 0: bad checksum; slot 1: bad magic; slot 2: bad superblock_count 99"
);
}
#[test]
fn is_fatal_matches_documented_classification_for_every_variant() {
fn documented_is_fatal(e: &ChiselError) -> bool {
match e {
ChiselError::InvalidHandle(_)
| ChiselError::NoActiveTransaction
| ChiselError::TransactionAlreadyActive
| ChiselError::SavepointNotFound(_)
| ChiselError::DuplicateSavepoint(_)
| ChiselError::ReadOnlyMode
| ChiselError::FileNotFound
| ChiselError::InvalidRootName
| ChiselError::RootNameTableFull
| ChiselError::InvalidSuperblockCount { .. }
| ChiselError::CacheFull { .. }
| ChiselError::SpillwayFull { .. }
| ChiselError::TransactionInProgress
| ChiselError::TagMismatch { .. }
| ChiselError::Poisoned
| ChiselError::NoEncryptionKey
| ChiselError::InvalidEncryptionKey
| ChiselError::EncryptionNotSupported
| ChiselError::NoFreeKeySlot
| ChiselError::LastKeySlot => false,
ChiselError::IoError(_)
| ChiselError::ChecksumMismatch { .. }
| ChiselError::CorruptSuperblock { .. }
| ChiselError::FileSizeMismatch { .. }
| ChiselError::LockFailed
| ChiselError::UnsupportedFormatVersion { .. }
| ChiselError::CorruptPage { .. }
| ChiselError::InvalidPageId { .. }
| ChiselError::UnsupportedPageSize { .. }
| ChiselError::DecryptionFailed { .. } => true,
}
}
let all = [
ChiselError::InvalidHandle(0),
ChiselError::NoActiveTransaction,
ChiselError::TransactionAlreadyActive,
ChiselError::SavepointNotFound("s".to_string()),
ChiselError::DuplicateSavepoint("s".to_string()),
ChiselError::ReadOnlyMode,
ChiselError::FileNotFound,
ChiselError::InvalidRootName,
ChiselError::RootNameTableFull,
ChiselError::InvalidSuperblockCount { value: 0 },
ChiselError::CacheFull { limit: 0 },
ChiselError::SpillwayFull { limit_bytes: 0 },
ChiselError::TransactionInProgress,
ChiselError::TagMismatch {
handle: 0,
expected: 0,
actual: 0,
},
ChiselError::Poisoned,
ChiselError::IoError(io::Error::other("io")),
ChiselError::ChecksumMismatch { page_id: 0 },
ChiselError::CorruptSuperblock { defects: vec![] },
ChiselError::FileSizeMismatch {
expected: 0,
actual: 0,
},
ChiselError::LockFailed,
ChiselError::UnsupportedFormatVersion {
found: 0,
expected: 0,
},
ChiselError::CorruptPage { page_id: 0 },
ChiselError::InvalidPageId { page_id: 0 },
ChiselError::UnsupportedPageSize {
stored: 0,
compiled: 0,
},
ChiselError::NoEncryptionKey,
ChiselError::InvalidEncryptionKey,
ChiselError::EncryptionNotSupported,
ChiselError::NoFreeKeySlot,
ChiselError::LastKeySlot,
ChiselError::DecryptionFailed { page_id: 0 },
];
for e in &all {
assert_eq!(
e.is_fatal(),
documented_is_fatal(e),
"is_fatal() disagrees with the documented Fatal/Operational block for {e:?}"
);
}
assert_eq!(all.iter().filter(|e| e.is_fatal()).count(), 10);
}
#[test]
fn encryption_error_classification() {
assert!(!ChiselError::NoEncryptionKey.is_fatal());
assert!(!ChiselError::InvalidEncryptionKey.is_fatal());
assert!(!ChiselError::EncryptionNotSupported.is_fatal());
assert!(!ChiselError::NoFreeKeySlot.is_fatal());
assert!(!ChiselError::LastKeySlot.is_fatal());
assert!(ChiselError::DecryptionFailed { page_id: 7 }.is_fatal());
let msg = format!("{}", ChiselError::DecryptionFailed { page_id: 7 });
assert!(
msg.contains('7'),
"Display {msg:?} should mention page id 7"
);
use std::error::Error;
for e in [
ChiselError::NoEncryptionKey,
ChiselError::InvalidEncryptionKey,
ChiselError::EncryptionNotSupported,
ChiselError::NoFreeKeySlot,
ChiselError::LastKeySlot,
ChiselError::DecryptionFailed { page_id: 0 },
] {
assert!(e.source().is_none());
}
}
}