harn_sqlite/
diagnostic.rs1use std::error::Error;
3use std::fmt;
4use std::io::ErrorKind;
5
6#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct FailureDiagnostic {
9 pub stage: &'static str,
10 pub sqlite_primary_code: Option<i32>,
11 pub sqlite_extended_code: Option<i32>,
12 pub io_kind: Option<ErrorKind>,
13 pub os_code: Option<i32>,
14}
15
16#[must_use]
21pub fn failure_diagnostic(stage: &'static str, error: &(dyn Error + 'static)) -> FailureDiagnostic {
22 let mut result = FailureDiagnostic {
23 stage,
24 sqlite_primary_code: None,
25 sqlite_extended_code: None,
26 io_kind: None,
27 os_code: None,
28 };
29 let mut current = Some(error);
30 for _ in 0..16 {
32 let Some(error) = current else { break };
33 if let Some(rusqlite::Error::SqliteFailure(code, _)) =
34 error.downcast_ref::<rusqlite::Error>()
35 {
36 result
37 .sqlite_primary_code
38 .get_or_insert(code.extended_code & 0xff);
39 result
40 .sqlite_extended_code
41 .get_or_insert(code.extended_code);
42 }
43 if let Some(error) = error.downcast_ref::<std::io::Error>() {
44 result.io_kind.get_or_insert(error.kind());
45 if let Some(code) = error.raw_os_error() {
46 result.os_code.get_or_insert(code);
47 }
48 }
49 current = error.source();
50 }
51 result
52}
53
54impl fmt::Display for FailureDiagnostic {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "stage={}", self.stage)?;
57 if let Some(code) = self.sqlite_primary_code {
58 write!(f, " sqlite_primary_code={code}")?;
59 }
60 if let Some(code) = self.sqlite_extended_code {
61 write!(f, " sqlite_extended_code={code}")?;
62 }
63 if let Some(kind) = self.io_kind {
64 write!(f, " io_kind={kind:?}")?;
65 }
66 if let Some(code) = self.os_code {
67 write!(f, " os_code={code}")?;
68 }
69 Ok(())
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::InitializationError;
77
78 #[test]
79 fn sqlite_codes_remain_distinct_and_do_not_expose_messages() {
80 for code in [
81 rusqlite::ffi::SQLITE_CANTOPEN,
82 rusqlite::ffi::SQLITE_BUSY_RECOVERY,
83 rusqlite::ffi::SQLITE_READONLY_DIRECTORY,
84 ] {
85 let error = rusqlite::Error::SqliteFailure(
86 rusqlite::ffi::Error::new(code),
87 Some("fixture-secret-path-and-sql-value".into()),
88 );
89 let diagnostic = failure_diagnostic("open", &error);
90 assert_eq!(diagnostic.sqlite_primary_code, Some(code & 0xff));
91 assert_eq!(diagnostic.sqlite_extended_code, Some(code));
92 assert_eq!(
93 diagnostic.io_kind, None,
94 "SQLite codes alone do not prove an OS denial"
95 );
96 assert!(!diagnostic.to_string().contains("fixture-secret"));
97 }
98 }
99
100 #[test]
101 fn initialization_preserves_stage_and_underlying_io_kind_without_paths() {
102 let error: InitializationError<rusqlite::Error> =
103 InitializationError::InitializationLockOpen {
104 path: "/fixture-secret-directory/spend.sqlite.harn-init.lock".into(),
105 source: std::io::Error::new(ErrorKind::PermissionDenied, "fixture-secret"),
106 };
107 let diagnostic = failure_diagnostic(error.stage(), &error);
108 assert_eq!(diagnostic.stage, "initialization_lock_open");
109 assert_eq!(diagnostic.io_kind, Some(ErrorKind::PermissionDenied));
110 assert_eq!(diagnostic.sqlite_extended_code, None);
111 assert!(!diagnostic.to_string().contains("fixture-secret"));
112 }
113}