Skip to main content

cdk_sqlite/
common.rs

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/// The config need to create a new SQLite connection
14#[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/// Sqlite connection manager
44#[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            // Check if parent directory exists before attempting to open database
61            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/// Convert cdk_sql_common::value::Value to rusqlite Value
146#[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/// Convert from rusqlite Valute to cdk_sql_common::value::Value
158#[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}