use crate::{DbError, DbOptions, Result};
use rusqlite::{Connection, ErrorCode};
use std::path::{Path, PathBuf};
use zeroize::Zeroizing;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct RecoveryPolicy {
pub quarantine_corrupt: bool,
pub quarantine_keyed: bool,
}
#[derive(Debug)]
pub struct OpenOutcome {
pub conn: Connection,
pub key_index: usize,
pub quarantined: Option<PathBuf>,
}
impl DbOptions {
pub fn open_writer_with_key_candidates(
&self,
candidates: &[Zeroizing<String>],
) -> Result<(Connection, usize)> {
let mut first_error = None;
for (index, candidate) in candidates.iter().enumerate() {
let mut attempt = self.clone();
attempt.key = Some(candidate.clone());
attempt.raw_key = None;
match attempt.open_writer() {
Ok(conn) => return Ok((conn, index)),
Err(error) => {
first_error.get_or_insert(error);
}
}
}
Err(first_error.unwrap_or(DbError::NoKeyOpened))
}
pub fn open_writer_recovering(&self, policy: RecoveryPolicy) -> Result<OpenOutcome> {
match self.open_writer() {
Ok(conn) => Ok(OpenOutcome {
conn,
key_index: 0,
quarantined: None,
}),
Err(error) if is_unreadable(&error) && allowed(self, policy) => {
let moved = quarantine_database(&self.path)?;
let conn = self.open_writer()?;
Ok(OpenOutcome {
conn,
key_index: 0,
quarantined: Some(moved),
})
}
Err(error) => Err(error),
}
}
}
fn allowed(options: &DbOptions, policy: RecoveryPolicy) -> bool {
if options.is_keyed() {
policy.quarantine_keyed
} else {
policy.quarantine_corrupt
}
}
fn is_unreadable(error: &DbError) -> bool {
matches!(
error,
DbError::Sqlite(rusqlite::Error::SqliteFailure(failure, _))
if matches!(failure.code, ErrorCode::NotADatabase | ErrorCode::DatabaseCorrupt)
)
}
pub fn quarantine_database(db: &Path) -> Result<PathBuf> {
let name = db
.file_name()
.ok_or(DbError::InvalidDatabasePath)?
.to_string_lossy()
.into_owned();
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let dest = db.with_file_name(format!("{name}.corrupt-{stamp}"));
std::fs::rename(db, &dest).map_err(|e| DbError::Fs(rightkit_fs::FsError::io(db, e)))?;
for suffix in ["-wal", "-shm", "-journal"] {
let mut from = db.as_os_str().to_os_string();
from.push(suffix);
let mut to = dest.as_os_str().to_os_string();
to.push(suffix);
let _ = std::fs::rename(PathBuf::from(from), PathBuf::from(to));
}
Ok(dest)
}
#[cfg(feature = "cipher")]
pub fn rekey(conn: &Connection, new_key: &str) -> Result<()> {
crate::validate_key(Some(new_key))?;
let statement = Zeroizing::new(format!("PRAGMA rekey = '{new_key}';"));
conn.execute_batch(statement.as_str())?;
crate::verify_key(conn)
}