1pub mod dedup;
2pub mod eventlog;
3pub mod memory;
4pub mod metrics;
5pub mod privacy;
6pub mod relations;
7pub mod schema;
8pub mod scope;
9
10use rusqlite::Connection;
11
12use crate::config::Config;
13use crate::Error;
14
15pub struct Store {
16 conn: Connection,
17 config: Config,
18}
19
20impl Store {
21 pub fn open(path: &str, config: Config, passphrase: Option<&str>) -> crate::Result<Self> {
22 if config.storage.encryption_enabled && passphrase.is_none() {
23 return Err(Error::Encryption(
24 "encryption is enabled but no passphrase was provided".into(),
25 ));
26 }
27 let mut conn = Connection::open(path)?;
28 if let Some(key) = passphrase {
29 apply_passphrase(&conn, key)?;
30 verify_access(&conn)?;
31 }
32 configure_connection(&conn, &config)?;
33 schema::check_version(&conn)?;
34 run_migrations(&mut conn)?;
35 Ok(Self { conn, config })
36 }
37
38 pub fn open_in_memory() -> crate::Result<Self> {
39 Self::open_in_memory_with_config(Config::default())
40 }
41
42 pub fn open_in_memory_with_config(config: Config) -> crate::Result<Self> {
43 let mut conn = Connection::open_in_memory()?;
44 configure_connection(&conn, &config)?;
45 run_migrations(&mut conn)?;
46 Ok(Self { conn, config })
47 }
48
49 pub fn config(&self) -> &Config {
50 &self.config
51 }
52
53 pub(crate) fn conn(&self) -> &Connection {
54 &self.conn
55 }
56
57 pub fn get_metadata(&self, key: &str) -> crate::Result<Option<String>> {
58 match self.conn.query_row(
59 "SELECT value FROM _metadata WHERE key = ?1",
60 rusqlite::params![key],
61 |row| row.get::<_, String>(0),
62 ) {
63 Ok(val) => Ok(Some(val)),
64 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
65 Err(e) => Err(e.into()),
66 }
67 }
68
69 pub fn set_metadata(&self, key: &str, value: &str) -> crate::Result<()> {
70 self.conn.execute(
71 "INSERT INTO _metadata (key, value) VALUES (?1, ?2)
72 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
73 rusqlite::params![key, value],
74 )?;
75 Ok(())
76 }
77
78 pub fn schema_version(&self) -> crate::Result<i64> {
79 Ok(self
80 .conn
81 .pragma_query_value(None, "user_version", |r| r.get(0))?)
82 }
83
84 pub fn is_encrypted(path: &str) -> bool {
86 let conn = match Connection::open(path) {
88 Ok(c) => c,
89 Err(_) => return false,
90 };
91 conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(()))
92 .is_err()
93 }
94
95 #[cfg(feature = "encryption")]
98 pub fn encrypt(path: &str, passphrase: &str, config: Config) -> crate::Result<()> {
99 use std::fs;
100
101 if Self::is_encrypted(path) {
102 return Err(Error::Encryption(
103 "database is already encrypted — decrypt first or delete and reinitialise".into(),
104 ));
105 }
106
107 let tmp_path = format!("{}.encrypting", path);
108 let backup_path = format!("{}.backup", path);
109
110 fs::remove_file(&tmp_path).ok();
112
113 {
115 let conn = Connection::open(path)?;
116 conn.execute_batch(&format!(
117 "ATTACH DATABASE '{}' AS encrypted KEY '{}';",
118 escape_sql_string(&tmp_path),
119 escape_sql_string(passphrase)
120 ))?;
121 conn.execute_batch("SELECT sqlcipher_export('encrypted');")?;
122 let ver: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
123 conn.execute_batch(&format!("PRAGMA encrypted.user_version = {};", ver))?;
124 conn.execute_batch("DETACH DATABASE encrypted;")?;
125 }
126
127 let verify_result = Store::open(&tmp_path, config.clone(), Some(passphrase));
129 if let Err(e) = verify_result {
130 fs::remove_file(&tmp_path).ok();
131 return Err(Error::Encryption(format!(
132 "encrypted DB failed verification, original preserved: {}",
133 e
134 )));
135 }
136 drop(verify_result);
137
138 fs::rename(path, &backup_path)
140 .map_err(|e| Error::Encryption(format!("failed to backup original: {}", e)))?;
141 cleanup_wal_files(path);
142
143 if let Err(e) = fs::rename(&tmp_path, path) {
144 fs::rename(&backup_path, path).ok();
146 return Err(Error::Encryption(format!("failed to replace db: {}", e)));
147 }
148
149 fs::remove_file(&backup_path).ok();
151 Ok(())
152 }
153
154 #[cfg(feature = "encryption")]
157 pub fn decrypt(path: &str, passphrase: &str, config: Config) -> crate::Result<()> {
158 use std::fs;
159 let tmp_path = format!("{}.decrypting", path);
160 let backup_path = format!("{}.backup", path);
161
162 {
164 let conn = Connection::open(path)?;
165 apply_passphrase(&conn, passphrase)?;
166 verify_access(&conn)?;
167 conn.execute_batch(&format!(
168 "ATTACH DATABASE '{}' AS plaintext KEY '';",
169 escape_sql_string(&tmp_path)
170 ))?;
171 conn.execute_batch("SELECT sqlcipher_export('plaintext');")?;
172 let ver: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
173 conn.execute_batch(&format!("PRAGMA plaintext.user_version = {};", ver))?;
174 conn.execute_batch("DETACH DATABASE plaintext;")?;
175 }
176
177 let mut verify_config = config;
179 verify_config.storage.encryption_enabled = false;
180 let verify_result = Store::open(&tmp_path, verify_config.clone(), None);
181 if let Err(e) = verify_result {
182 fs::remove_file(&tmp_path).ok();
183 return Err(Error::Encryption(format!(
184 "decrypted DB failed verification, original preserved: {}",
185 e
186 )));
187 }
188 drop(verify_result);
189
190 fs::rename(path, &backup_path)
192 .map_err(|e| Error::Encryption(format!("failed to backup original: {}", e)))?;
193 cleanup_wal_files(path);
194
195 if let Err(e) = fs::rename(&tmp_path, path) {
196 fs::rename(&backup_path, path).ok();
197 return Err(Error::Encryption(format!("failed to replace db: {}", e)));
198 }
199
200 fs::remove_file(&backup_path).ok();
202 Ok(())
203 }
204}
205
206fn apply_passphrase(conn: &Connection, passphrase: &str) -> crate::Result<()> {
207 conn.pragma_update(None, "key", passphrase)
209 .map_err(|e| Error::Encryption(format!("failed to set encryption key: {}", e)))?;
210 Ok(())
211}
212
213fn verify_access(conn: &Connection) -> crate::Result<()> {
214 conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(()))
215 .map_err(|_| {
216 Error::Encryption("cannot access database — wrong passphrase or not encrypted".into())
217 })?;
218 Ok(())
219}
220
221fn configure_connection(conn: &Connection, config: &Config) -> rusqlite::Result<()> {
222 conn.execute_batch(&format!(
223 "PRAGMA journal_mode = WAL;
224 PRAGMA busy_timeout = {};
225 PRAGMA synchronous = NORMAL;
226 PRAGMA foreign_keys = ON;
227 PRAGMA cache_size = -{};",
228 config.storage.busy_timeout_ms, config.storage.cache_size_kb
229 ))?;
230 Ok(())
231}
232
233fn run_migrations(conn: &mut Connection) -> crate::Result<()> {
234 let migrations = schema::migrations();
235 migrations
236 .to_latest(conn)
237 .map_err(|e| Error::Migration(e.to_string()))?;
238 Ok(())
239}
240
241#[cfg(feature = "encryption")]
242fn escape_sql_string(s: &str) -> String {
243 s.replace('\'', "''")
244}
245
246#[cfg(feature = "encryption")]
250fn cleanup_wal_files(path: &str) {
251 use std::fs;
252 fs::remove_file(format!("{}-shm", path)).ok();
253 fs::remove_file(format!("{}-wal", path)).ok();
254}