1use crate::recover::{allowed, is_unreadable, quarantine_database_with};
24use crate::recover::{OpenOutcome, RecoveryPolicy, SidecarPolicy};
25use crate::{DbError, DbOptions, Result};
26use rusqlite::{Connection, ErrorCode};
27use std::fmt;
28use std::path::{Path, PathBuf};
29
30#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
32#[non_exhaustive]
33pub enum UnreadablePolicy {
34 #[default]
36 Fail,
37 QuarantineAfter { attempts: u32 },
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45#[non_exhaustive]
46pub enum UnreadableCause {
47 PermissionDenied,
49 IoReadFailure,
51 StorageUnavailable(StorageProblem),
53}
54
55impl UnreadableCause {
56 pub fn counts_toward_quarantine(self) -> bool {
58 matches!(self, Self::PermissionDenied | Self::IoReadFailure)
59 }
60}
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64#[non_exhaustive]
65pub enum StorageProblem {
66 DiskFull,
68 ReadOnlyFilesystem,
70 DeviceNotReady,
75 SharingViolation,
77 LockedOrBusy,
80 FileMissing,
82 Other,
86}
87
88impl fmt::Display for StorageProblem {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 f.write_str(match self {
91 Self::DiskFull => "the disk is full",
92 Self::ReadOnlyFilesystem => "the filesystem is read-only",
93 Self::DeviceNotReady => "the device or share is not ready",
94 Self::SharingViolation => "a sharing or lock violation",
95 Self::LockedOrBusy => "the database is busy or locked",
96 Self::FileMissing => "the file is missing",
97 Self::Other => "the storage reported an unclassified error",
98 })
99 }
100}
101
102impl fmt::Display for UnreadableCause {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 Self::PermissionDenied => f.write_str("permission denied reading the file"),
106 Self::IoReadFailure => f.write_str("I/O error reading the file"),
107 Self::StorageUnavailable(p) => write!(f, "storage unavailable: {p}"),
108 }
109 }
110}
111
112#[derive(Clone, Debug, Eq, PartialEq)]
114#[non_exhaustive]
115pub enum RecoveryAction {
116 OpenedNormally,
118 QuarantinedCorrupt,
120 QuarantinedUnreadable {
123 cause: UnreadableCause,
124 consecutive_failures: u32,
125 },
126}
127
128#[derive(Debug)]
130pub struct UnreadableRecovery {
131 pub conn: Connection,
132 pub key_index: usize,
134 pub quarantined: Option<PathBuf>,
136 pub action: RecoveryAction,
137}
138
139impl From<UnreadableRecovery> for OpenOutcome {
140 fn from(r: UnreadableRecovery) -> Self {
141 OpenOutcome {
142 conn: r.conn,
143 key_index: r.key_index,
144 quarantined: r.quarantined,
145 }
146 }
147}
148
149pub fn classify_open_failure(error: &DbError, path: &Path) -> Option<UnreadableCause> {
154 match error {
155 DbError::Sqlite(rusqlite::Error::SqliteFailure(failure, _)) => {
156 classify_sqlite(failure.code, failure.extended_code, path)
157 }
158 other => {
159 let mut source: Option<&(dyn std::error::Error + 'static)> =
160 std::error::Error::source(other);
161 while let Some(e) = source {
162 if let Some(io) = e.downcast_ref::<std::io::Error>() {
163 return Some(classify_io(io));
164 }
165 source = e.source();
166 }
167 None
168 }
169 }
170}
171
172const IOERR_READ: i32 = 266;
175const IOERR_SHORT_READ: i32 = 522;
176const IOERR_UNLOCK: i32 = 2058;
177const IOERR_RDLOCK: i32 = 2314;
178const IOERR_BLOCKED: i32 = 2826;
179const IOERR_CHECKRESERVEDLOCK: i32 = 3594;
180const IOERR_LOCK: i32 = 3850;
181const IOERR_SHMLOCK: i32 = 5130;
182const IOERR_DATA: i32 = 8202;
183const IOERR_CORRUPTFS: i32 = 8458;
184const CANTOPEN_PLAIN: i32 = 14;
185
186fn storage(p: StorageProblem) -> Option<UnreadableCause> {
187 Some(UnreadableCause::StorageUnavailable(p))
188}
189
190fn classify_sqlite(code: ErrorCode, extended: i32, path: &Path) -> Option<UnreadableCause> {
191 match code {
192 ErrorCode::PermissionDenied => Some(UnreadableCause::PermissionDenied),
193 ErrorCode::DiskFull => storage(StorageProblem::DiskFull),
194 ErrorCode::ReadOnly => storage(StorageProblem::ReadOnlyFilesystem),
195 ErrorCode::DatabaseBusy
196 | ErrorCode::DatabaseLocked
197 | ErrorCode::FileLockingProtocolFailed => storage(StorageProblem::LockedOrBusy),
198 ErrorCode::OutOfMemory => storage(StorageProblem::Other),
199 ErrorCode::SystemIoFailure => match extended {
200 IOERR_READ | IOERR_SHORT_READ => Some(UnreadableCause::IoReadFailure),
201 IOERR_UNLOCK
202 | IOERR_RDLOCK
203 | IOERR_BLOCKED
204 | IOERR_CHECKRESERVEDLOCK
205 | IOERR_LOCK
206 | IOERR_SHMLOCK => storage(StorageProblem::LockedOrBusy),
207 IOERR_DATA | IOERR_CORRUPTFS => None,
210 _ => storage(StorageProblem::Other),
211 },
212 ErrorCode::CannotOpen if extended == CANTOPEN_PLAIN => match std::fs::File::open(path) {
213 Ok(_) => storage(StorageProblem::Other),
216 Err(e) => Some(classify_io(&e)),
217 },
218 ErrorCode::CannotOpen => storage(StorageProblem::Other),
219 _ => None,
220 }
221}
222
223fn classify_io(e: &std::io::Error) -> UnreadableCause {
224 use std::io::ErrorKind as K;
225 match e.kind() {
226 K::PermissionDenied => return UnreadableCause::PermissionDenied,
227 K::NotFound => return UnreadableCause::StorageUnavailable(StorageProblem::FileMissing),
228 K::StorageFull | K::QuotaExceeded => {
229 return UnreadableCause::StorageUnavailable(StorageProblem::DiskFull)
230 }
231 K::ReadOnlyFilesystem => {
232 return UnreadableCause::StorageUnavailable(StorageProblem::ReadOnlyFilesystem)
233 }
234 K::ResourceBusy | K::Deadlock | K::WouldBlock => {
235 return UnreadableCause::StorageUnavailable(StorageProblem::LockedOrBusy)
236 }
237 K::StaleNetworkFileHandle
238 | K::NotConnected
239 | K::NetworkDown
240 | K::NetworkUnreachable
241 | K::HostUnreachable
242 | K::TimedOut => {
243 return UnreadableCause::StorageUnavailable(StorageProblem::DeviceNotReady)
244 }
245 _ => {}
246 }
247 match e.raw_os_error() {
248 Some(code) => classify_os_error(code),
249 None => UnreadableCause::StorageUnavailable(StorageProblem::Other),
250 }
251}
252
253#[cfg(unix)]
254fn classify_os_error(code: i32) -> UnreadableCause {
255 use StorageProblem as P;
256 let problem = match code {
257 libc::EIO => return UnreadableCause::IoReadFailure,
258 libc::ENXIO | libc::ENODEV => P::DeviceNotReady,
259 libc::EBUSY | libc::ETXTBSY | libc::EAGAIN | libc::EDEADLK => P::LockedOrBusy,
260 libc::ENOSPC | libc::EDQUOT => P::DiskFull,
261 libc::EROFS => P::ReadOnlyFilesystem,
262 libc::ESTALE
263 | libc::ENOTCONN
264 | libc::ENETDOWN
265 | libc::ENETUNREACH
266 | libc::EHOSTUNREACH
267 | libc::ETIMEDOUT => P::DeviceNotReady,
268 #[cfg(target_os = "linux")]
269 libc::ENOMEDIUM => P::DeviceNotReady,
270 _ => P::Other,
271 };
272 UnreadableCause::StorageUnavailable(problem)
273}
274
275#[cfg(windows)]
276fn classify_os_error(code: i32) -> UnreadableCause {
277 use StorageProblem as P;
278 let problem = match code {
279 1117 | 23 | 27 | 30 => return UnreadableCause::IoReadFailure,
281 32 | 33 => P::SharingViolation,
283 21 | 55 | 1167 | 53 | 64 | 59 | 1231 => P::DeviceNotReady,
286 112 | 39 => P::DiskFull,
288 19 => P::ReadOnlyFilesystem,
290 _ => P::Other,
291 };
292 UnreadableCause::StorageUnavailable(problem)
293}
294
295#[cfg(not(any(unix, windows)))]
296fn classify_os_error(_code: i32) -> UnreadableCause {
297 UnreadableCause::StorageUnavailable(StorageProblem::Other)
298}
299
300fn with_suffix(path: &Path, suffix: &str) -> PathBuf {
301 let mut name = path.as_os_str().to_os_string();
302 name.push(suffix);
303 PathBuf::from(name)
304}
305
306pub fn unreadable_counter_path(db: &Path) -> PathBuf {
308 with_suffix(db, ".unreadable-opens")
309}
310
311fn read_counter(path: &Path) -> u32 {
312 std::fs::read_to_string(path)
313 .ok()
314 .and_then(|s| s.lines().nth(1).and_then(|l| l.trim().parse().ok()))
315 .unwrap_or(0)
316}
317
318fn bump_counter(path: &Path) -> Result<u32> {
319 let next = read_counter(path).saturating_add(1);
320 rightkit_fs::atomic_write_private(
321 path,
322 format!("rightkit-db-unreadable.v1\n{next}\n").as_bytes(),
323 )?;
324 Ok(next)
325}
326
327fn reset_counter(path: &Path) {
328 if std::fs::symlink_metadata(path).is_ok() {
330 let _ = std::fs::remove_file(path);
331 }
332}
333
334fn is_regular_file(path: &Path) -> bool {
335 std::fs::symlink_metadata(path).is_ok_and(|m| m.file_type().is_file())
336}
337
338impl DbOptions {
339 pub fn open_writer_recovering_unreadable(
352 &self,
353 policy: RecoveryPolicy,
354 unreadable: UnreadablePolicy,
355 ) -> Result<UnreadableRecovery> {
356 self.open_writer_recovering_via(policy, unreadable, || self.open_writer())
357 }
358
359 pub fn open_writer_recovering_via(
363 &self,
364 policy: RecoveryPolicy,
365 unreadable: UnreadablePolicy,
366 mut open: impl FnMut() -> Result<Connection>,
367 ) -> Result<UnreadableRecovery> {
368 let counter = unreadable_counter_path(&self.path);
369 let error = match open() {
370 Ok(conn) => {
371 reset_counter(&counter);
372 return Ok(UnreadableRecovery {
373 conn,
374 key_index: 0,
375 quarantined: None,
376 action: RecoveryAction::OpenedNormally,
377 });
378 }
379 Err(error) => error,
380 };
381 if is_unreadable(&error) && allowed(self, policy) {
382 let moved =
383 quarantine_database_with(&self.path, policy.quarantine_stamp, policy.sidecars)?;
384 let conn = open()?;
385 reset_counter(&counter);
386 return Ok(UnreadableRecovery {
387 conn,
388 key_index: 0,
389 quarantined: Some(moved),
390 action: RecoveryAction::QuarantinedCorrupt,
391 });
392 }
393 let Some(cause) = classify_open_failure(&error, &self.path) else {
394 return Err(error);
395 };
396 let threshold = match unreadable {
397 UnreadablePolicy::QuarantineAfter { attempts } if cause.counts_toward_quarantine() => {
398 Some(attempts.max(1))
399 }
400 _ => None,
401 };
402 let Some(threshold) = threshold else {
403 return Err(self.unreadable_error(cause, read_counter(&counter), None, error));
405 };
406 let failures = match bump_counter(&counter) {
407 Ok(n) => n,
408 Err(_) => {
410 return Err(self.unreadable_error(
411 cause,
412 read_counter(&counter),
413 Some(threshold),
414 error,
415 ))
416 }
417 };
418 if failures < threshold || !is_regular_file(&self.path) {
419 return Err(self.unreadable_error(cause, failures, Some(threshold), error));
420 }
421 let moved = quarantine_database_with(
423 &self.path,
424 policy.quarantine_stamp,
425 SidecarPolicy::RenameSidecars,
426 )?;
427 reset_counter(&counter);
428 let conn = open()?;
429 Ok(UnreadableRecovery {
430 conn,
431 key_index: 0,
432 quarantined: Some(moved),
433 action: RecoveryAction::QuarantinedUnreadable {
434 cause,
435 consecutive_failures: failures,
436 },
437 })
438 }
439
440 fn unreadable_error(
441 &self,
442 cause: UnreadableCause,
443 consecutive_failures: u32,
444 quarantine_after: Option<u32>,
445 source: DbError,
446 ) -> DbError {
447 DbError::Unreadable {
448 path: self.path.clone(),
449 cause,
450 consecutive_failures,
451 quarantine_after,
452 source: Box::new(source),
453 }
454 }
455}