1use std::fmt;
2use std::path::PathBuf;
3use std::sync::atomic::AtomicBool;
4use std::sync::Arc;
5use std::time::Duration;
6
7use cdk_sql_common::pool::{self, DatabasePool};
8use cdk_sql_common::value::Value;
9use rusqlite::Connection;
10
11use crate::async_sqlite;
12
13#[derive(Clone)]
15pub struct Config {
16 path: Option<String>,
17 password: Option<String>,
18}
19
20impl fmt::Debug for Config {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 f.debug_struct("Config")
23 .field("path", &self.path)
24 .field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
25 .finish()
26 }
27}
28
29impl pool::DatabaseConfig for Config {
30 fn default_timeout(&self) -> Duration {
31 Duration::from_secs(5)
32 }
33
34 fn max_size(&self) -> usize {
35 if self.path.is_none() {
36 1
37 } else {
38 20
39 }
40 }
41}
42
43#[derive(Debug)]
45pub struct SqliteConnectionManager;
46
47impl DatabasePool for SqliteConnectionManager {
48 type Config = Config;
49
50 type Connection = async_sqlite::AsyncSqlite;
51
52 type Error = rusqlite::Error;
53
54 fn new_resource(
55 config: &Self::Config,
56 _stale: Arc<AtomicBool>,
57 _timeout: Duration,
58 ) -> Result<Self::Connection, pool::Error<Self::Error>> {
59 let conn = if let Some(path) = config.path.as_ref() {
60 let path_buf = PathBuf::from(path);
62 if let Some(parent) = path_buf.parent() {
63 if !parent.to_str().unwrap_or_default().is_empty() && !parent.exists() {
64 return Err(pool::Error::Resource(rusqlite::Error::InvalidPath(
65 path_buf.clone(),
66 )));
67 }
68 }
69 Connection::open(path)?
70 } else {
71 Connection::open_in_memory()?
72 };
73
74 if let Some(password) = config.password.as_ref() {
75 conn.pragma_update(None, "key", password)?;
76 }
77
78 conn.execute_batch(
79 r#"
80 pragma busy_timeout = 10000;
81 pragma journal_mode = WAL;
82 pragma synchronous = FULL;
83 pragma temp_store = memory;
84 pragma mmap_size = 5242880;
85 pragma cache = shared;
86 "#,
87 )?;
88
89 conn.busy_timeout(Duration::from_secs(10))?;
90
91 Ok(async_sqlite::AsyncSqlite::new(conn))
92 }
93}
94
95impl From<PathBuf> for Config {
96 fn from(path: PathBuf) -> Self {
97 path.to_str().unwrap_or_default().into()
98 }
99}
100
101impl From<(PathBuf, String)> for Config {
102 fn from((path, password): (PathBuf, String)) -> Self {
103 (path.to_str().unwrap_or_default(), password.as_str()).into()
104 }
105}
106
107impl From<&PathBuf> for Config {
108 fn from(path: &PathBuf) -> Self {
109 path.to_str().unwrap_or_default().into()
110 }
111}
112
113impl From<&str> for Config {
114 fn from(path: &str) -> Self {
115 if path.contains(":memory:") {
116 Config {
117 path: None,
118 password: None,
119 }
120 } else {
121 Config {
122 path: Some(path.to_owned()),
123 password: None,
124 }
125 }
126 }
127}
128
129impl From<(&str, &str)> for Config {
130 fn from((path, pass): (&str, &str)) -> Self {
131 if path.contains(":memory:") {
132 Config {
133 path: None,
134 password: Some(pass.to_owned()),
135 }
136 } else {
137 Config {
138 path: Some(path.to_owned()),
139 password: Some(pass.to_owned()),
140 }
141 }
142 }
143}
144
145#[inline(always)]
147pub fn to_sqlite(v: Value) -> rusqlite::types::Value {
148 match v {
149 Value::Blob(blob) => rusqlite::types::Value::Blob(blob),
150 Value::Integer(i) => rusqlite::types::Value::Integer(i),
151 Value::Null => rusqlite::types::Value::Null,
152 Value::Text(t) => rusqlite::types::Value::Text(t),
153 Value::Real(r) => rusqlite::types::Value::Real(r),
154 }
155}
156
157#[inline(always)]
159pub fn from_sqlite(v: rusqlite::types::Value) -> Value {
160 match v {
161 rusqlite::types::Value::Blob(blob) => Value::Blob(blob),
162 rusqlite::types::Value::Integer(i) => Value::Integer(i),
163 rusqlite::types::Value::Null => Value::Null,
164 rusqlite::types::Value::Text(t) => Value::Text(t),
165 rusqlite::types::Value::Real(r) => Value::Real(r),
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::Config;
172
173 #[test]
174 fn config_debug_redacts_sqlcipher_password() {
175 let secret = "sqlcipher-password-secret";
176 let config = Config::from(("wallet.sqlite", secret));
177
178 let debug = format!("{config:?}");
179
180 assert!(debug.contains("wallet.sqlite"));
181 assert!(debug.contains("[REDACTED]"));
182 assert!(!debug.contains(secret));
183 }
184}