1use crate::{DbError, DbOptions, Result};
13use rusqlite::{Connection, ErrorCode};
14use std::path::{Path, PathBuf};
15use zeroize::Zeroizing;
16
17#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
18pub struct RecoveryPolicy {
19 pub quarantine_corrupt: bool,
21 pub quarantine_keyed: bool,
23}
24
25#[derive(Debug)]
26pub struct OpenOutcome {
27 pub conn: Connection,
28 pub key_index: usize,
30 pub quarantined: Option<PathBuf>,
32}
33
34impl DbOptions {
35 pub fn open_writer_with_key_candidates(
37 &self,
38 candidates: &[Zeroizing<String>],
39 ) -> Result<(Connection, usize)> {
40 let mut first_error = None;
41 for (index, candidate) in candidates.iter().enumerate() {
42 let mut attempt = self.clone();
43 attempt.key = Some(candidate.clone());
44 attempt.raw_key = None;
45 match attempt.open_writer() {
46 Ok(conn) => return Ok((conn, index)),
47 Err(error) => {
48 first_error.get_or_insert(error);
49 }
50 }
51 }
52 Err(first_error.unwrap_or(DbError::NoKeyOpened))
53 }
54
55 pub fn open_writer_recovering(&self, policy: RecoveryPolicy) -> Result<OpenOutcome> {
58 match self.open_writer() {
59 Ok(conn) => Ok(OpenOutcome {
60 conn,
61 key_index: 0,
62 quarantined: None,
63 }),
64 Err(error) if is_unreadable(&error) && allowed(self, policy) => {
65 let moved = quarantine_database(&self.path)?;
66 let conn = self.open_writer()?;
67 Ok(OpenOutcome {
68 conn,
69 key_index: 0,
70 quarantined: Some(moved),
71 })
72 }
73 Err(error) => Err(error),
74 }
75 }
76}
77
78fn allowed(options: &DbOptions, policy: RecoveryPolicy) -> bool {
79 if options.is_keyed() {
80 policy.quarantine_keyed
81 } else {
82 policy.quarantine_corrupt
83 }
84}
85
86fn is_unreadable(error: &DbError) -> bool {
87 matches!(
88 error,
89 DbError::Sqlite(rusqlite::Error::SqliteFailure(failure, _))
90 if matches!(failure.code, ErrorCode::NotADatabase | ErrorCode::DatabaseCorrupt)
91 )
92}
93
94pub fn quarantine_database(db: &Path) -> Result<PathBuf> {
97 let name = db
98 .file_name()
99 .ok_or(DbError::InvalidDatabasePath)?
100 .to_string_lossy()
101 .into_owned();
102 let stamp = std::time::SystemTime::now()
103 .duration_since(std::time::UNIX_EPOCH)
104 .map(|d| d.as_millis())
105 .unwrap_or(0);
106 let dest = db.with_file_name(format!("{name}.corrupt-{stamp}"));
107 std::fs::rename(db, &dest).map_err(|e| DbError::Fs(rightkit_fs::FsError::io(db, e)))?;
108 for suffix in ["-wal", "-shm", "-journal"] {
109 let mut from = db.as_os_str().to_os_string();
110 from.push(suffix);
111 let mut to = dest.as_os_str().to_os_string();
112 to.push(suffix);
113 let _ = std::fs::rename(PathBuf::from(from), PathBuf::from(to));
114 }
115 Ok(dest)
116}
117
118#[cfg(feature = "sqlcipher")]
123pub fn rekey(conn: &Connection, new_key: &str) -> Result<()> {
124 crate::validate_key(Some(new_key))?;
125 let statement = Zeroizing::new(format!("PRAGMA rekey = '{new_key}';"));
126 conn.execute_batch(statement.as_str())?;
127 crate::verify_key(conn)
128}