use crate::recover::{allowed, is_unreadable, quarantine_database_with};
use crate::recover::{OpenOutcome, RecoveryPolicy, SidecarPolicy};
use crate::{DbError, DbOptions, Result};
use rusqlite::{Connection, ErrorCode};
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum UnreadablePolicy {
#[default]
Fail,
QuarantineAfter { attempts: u32 },
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum UnreadableCause {
PermissionDenied,
IoReadFailure,
StorageUnavailable(StorageProblem),
}
impl UnreadableCause {
pub fn counts_toward_quarantine(self) -> bool {
matches!(self, Self::PermissionDenied | Self::IoReadFailure)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum StorageProblem {
DiskFull,
ReadOnlyFilesystem,
DeviceNotReady,
SharingViolation,
LockedOrBusy,
FileMissing,
Other,
}
impl fmt::Display for StorageProblem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::DiskFull => "the disk is full",
Self::ReadOnlyFilesystem => "the filesystem is read-only",
Self::DeviceNotReady => "the device or share is not ready",
Self::SharingViolation => "a sharing or lock violation",
Self::LockedOrBusy => "the database is busy or locked",
Self::FileMissing => "the file is missing",
Self::Other => "the storage reported an unclassified error",
})
}
}
impl fmt::Display for UnreadableCause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PermissionDenied => f.write_str("permission denied reading the file"),
Self::IoReadFailure => f.write_str("I/O error reading the file"),
Self::StorageUnavailable(p) => write!(f, "storage unavailable: {p}"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RecoveryAction {
OpenedNormally,
QuarantinedCorrupt,
QuarantinedUnreadable {
cause: UnreadableCause,
consecutive_failures: u32,
},
}
#[derive(Debug)]
pub struct UnreadableRecovery {
pub conn: Connection,
pub key_index: usize,
pub quarantined: Option<PathBuf>,
pub action: RecoveryAction,
}
impl From<UnreadableRecovery> for OpenOutcome {
fn from(r: UnreadableRecovery) -> Self {
OpenOutcome {
conn: r.conn,
key_index: r.key_index,
quarantined: r.quarantined,
}
}
}
pub fn classify_open_failure(error: &DbError, path: &Path) -> Option<UnreadableCause> {
match error {
DbError::Sqlite(rusqlite::Error::SqliteFailure(failure, _)) => {
classify_sqlite(failure.code, failure.extended_code, path)
}
other => {
let mut source: Option<&(dyn std::error::Error + 'static)> =
std::error::Error::source(other);
while let Some(e) = source {
if let Some(io) = e.downcast_ref::<std::io::Error>() {
return Some(classify_io(io));
}
source = e.source();
}
None
}
}
}
const IOERR_READ: i32 = 266;
const IOERR_SHORT_READ: i32 = 522;
const IOERR_UNLOCK: i32 = 2058;
const IOERR_RDLOCK: i32 = 2314;
const IOERR_BLOCKED: i32 = 2826;
const IOERR_CHECKRESERVEDLOCK: i32 = 3594;
const IOERR_LOCK: i32 = 3850;
const IOERR_SHMLOCK: i32 = 5130;
const IOERR_DATA: i32 = 8202;
const IOERR_CORRUPTFS: i32 = 8458;
const CANTOPEN_PLAIN: i32 = 14;
fn storage(p: StorageProblem) -> Option<UnreadableCause> {
Some(UnreadableCause::StorageUnavailable(p))
}
fn classify_sqlite(code: ErrorCode, extended: i32, path: &Path) -> Option<UnreadableCause> {
match code {
ErrorCode::PermissionDenied => Some(UnreadableCause::PermissionDenied),
ErrorCode::DiskFull => storage(StorageProblem::DiskFull),
ErrorCode::ReadOnly => storage(StorageProblem::ReadOnlyFilesystem),
ErrorCode::DatabaseBusy
| ErrorCode::DatabaseLocked
| ErrorCode::FileLockingProtocolFailed => storage(StorageProblem::LockedOrBusy),
ErrorCode::OutOfMemory => storage(StorageProblem::Other),
ErrorCode::SystemIoFailure => match extended {
IOERR_READ | IOERR_SHORT_READ => Some(UnreadableCause::IoReadFailure),
IOERR_UNLOCK
| IOERR_RDLOCK
| IOERR_BLOCKED
| IOERR_CHECKRESERVEDLOCK
| IOERR_LOCK
| IOERR_SHMLOCK => storage(StorageProblem::LockedOrBusy),
IOERR_DATA | IOERR_CORRUPTFS => None,
_ => storage(StorageProblem::Other),
},
ErrorCode::CannotOpen if extended == CANTOPEN_PLAIN => match std::fs::File::open(path) {
Ok(_) => storage(StorageProblem::Other),
Err(e) => Some(classify_io(&e)),
},
ErrorCode::CannotOpen => storage(StorageProblem::Other),
_ => None,
}
}
fn classify_io(e: &std::io::Error) -> UnreadableCause {
use std::io::ErrorKind as K;
match e.kind() {
K::PermissionDenied => return UnreadableCause::PermissionDenied,
K::NotFound => return UnreadableCause::StorageUnavailable(StorageProblem::FileMissing),
K::StorageFull | K::QuotaExceeded => {
return UnreadableCause::StorageUnavailable(StorageProblem::DiskFull)
}
K::ReadOnlyFilesystem => {
return UnreadableCause::StorageUnavailable(StorageProblem::ReadOnlyFilesystem)
}
K::ResourceBusy | K::Deadlock | K::WouldBlock => {
return UnreadableCause::StorageUnavailable(StorageProblem::LockedOrBusy)
}
K::StaleNetworkFileHandle
| K::NotConnected
| K::NetworkDown
| K::NetworkUnreachable
| K::HostUnreachable
| K::TimedOut => {
return UnreadableCause::StorageUnavailable(StorageProblem::DeviceNotReady)
}
_ => {}
}
match e.raw_os_error() {
Some(code) => classify_os_error(code),
None => UnreadableCause::StorageUnavailable(StorageProblem::Other),
}
}
#[cfg(unix)]
fn classify_os_error(code: i32) -> UnreadableCause {
use StorageProblem as P;
let problem = match code {
libc::EIO => return UnreadableCause::IoReadFailure,
libc::ENXIO | libc::ENODEV => P::DeviceNotReady,
libc::EBUSY | libc::ETXTBSY | libc::EAGAIN | libc::EDEADLK => P::LockedOrBusy,
libc::ENOSPC | libc::EDQUOT => P::DiskFull,
libc::EROFS => P::ReadOnlyFilesystem,
libc::ESTALE
| libc::ENOTCONN
| libc::ENETDOWN
| libc::ENETUNREACH
| libc::EHOSTUNREACH
| libc::ETIMEDOUT => P::DeviceNotReady,
#[cfg(target_os = "linux")]
libc::ENOMEDIUM => P::DeviceNotReady,
_ => P::Other,
};
UnreadableCause::StorageUnavailable(problem)
}
#[cfg(windows)]
fn classify_os_error(code: i32) -> UnreadableCause {
use StorageProblem as P;
let problem = match code {
1117 | 23 | 27 | 30 => return UnreadableCause::IoReadFailure,
32 | 33 => P::SharingViolation,
21 | 55 | 1167 | 53 | 64 | 59 | 1231 => P::DeviceNotReady,
112 | 39 => P::DiskFull,
19 => P::ReadOnlyFilesystem,
_ => P::Other,
};
UnreadableCause::StorageUnavailable(problem)
}
#[cfg(not(any(unix, windows)))]
fn classify_os_error(_code: i32) -> UnreadableCause {
UnreadableCause::StorageUnavailable(StorageProblem::Other)
}
fn with_suffix(path: &Path, suffix: &str) -> PathBuf {
let mut name = path.as_os_str().to_os_string();
name.push(suffix);
PathBuf::from(name)
}
pub fn unreadable_counter_path(db: &Path) -> PathBuf {
with_suffix(db, ".unreadable-opens")
}
fn read_counter(path: &Path) -> u32 {
std::fs::read_to_string(path)
.ok()
.and_then(|s| s.lines().nth(1).and_then(|l| l.trim().parse().ok()))
.unwrap_or(0)
}
fn bump_counter(path: &Path) -> Result<u32> {
let next = read_counter(path).saturating_add(1);
rightkit_fs::atomic_write_private(
path,
format!("rightkit-db-unreadable.v1\n{next}\n").as_bytes(),
)?;
Ok(next)
}
fn reset_counter(path: &Path) {
if std::fs::symlink_metadata(path).is_ok() {
let _ = std::fs::remove_file(path);
}
}
fn is_regular_file(path: &Path) -> bool {
std::fs::symlink_metadata(path).is_ok_and(|m| m.file_type().is_file())
}
impl DbOptions {
pub fn open_writer_recovering_unreadable(
&self,
policy: RecoveryPolicy,
unreadable: UnreadablePolicy,
) -> Result<UnreadableRecovery> {
self.open_writer_recovering_via(policy, unreadable, || self.open_writer())
}
pub fn open_writer_recovering_via(
&self,
policy: RecoveryPolicy,
unreadable: UnreadablePolicy,
mut open: impl FnMut() -> Result<Connection>,
) -> Result<UnreadableRecovery> {
let counter = unreadable_counter_path(&self.path);
let error = match open() {
Ok(conn) => {
reset_counter(&counter);
return Ok(UnreadableRecovery {
conn,
key_index: 0,
quarantined: None,
action: RecoveryAction::OpenedNormally,
});
}
Err(error) => error,
};
if is_unreadable(&error) && allowed(self, policy) {
let moved =
quarantine_database_with(&self.path, policy.quarantine_stamp, policy.sidecars)?;
let conn = open()?;
reset_counter(&counter);
return Ok(UnreadableRecovery {
conn,
key_index: 0,
quarantined: Some(moved),
action: RecoveryAction::QuarantinedCorrupt,
});
}
let Some(cause) = classify_open_failure(&error, &self.path) else {
return Err(error);
};
let threshold = match unreadable {
UnreadablePolicy::QuarantineAfter { attempts } if cause.counts_toward_quarantine() => {
Some(attempts.max(1))
}
_ => None,
};
let Some(threshold) = threshold else {
return Err(self.unreadable_error(cause, read_counter(&counter), None, error));
};
let failures = match bump_counter(&counter) {
Ok(n) => n,
Err(_) => {
return Err(self.unreadable_error(
cause,
read_counter(&counter),
Some(threshold),
error,
))
}
};
if failures < threshold || !is_regular_file(&self.path) {
return Err(self.unreadable_error(cause, failures, Some(threshold), error));
}
let moved = quarantine_database_with(
&self.path,
policy.quarantine_stamp,
SidecarPolicy::RenameSidecars,
)?;
reset_counter(&counter);
let conn = open()?;
Ok(UnreadableRecovery {
conn,
key_index: 0,
quarantined: Some(moved),
action: RecoveryAction::QuarantinedUnreadable {
cause,
consecutive_failures: failures,
},
})
}
fn unreadable_error(
&self,
cause: UnreadableCause,
consecutive_failures: u32,
quarantine_after: Option<u32>,
source: DbError,
) -> DbError {
DbError::Unreadable {
path: self.path.clone(),
cause,
consecutive_failures,
quarantine_after,
source: Box::new(source),
}
}
}