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