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 rekey(&self, new_passphrase: &str) -> crate::Result<()> {
158 self.conn
159 .pragma_update(None, "rekey", new_passphrase)
160 .map_err(|e| Error::Encryption(format!("rekey failed: {}", e)))?;
161 self.conn
163 .query_row("PRAGMA user_version", [], |r| r.get::<_, i32>(0))
164 .map_err(|_| {
165 Error::Encryption("rekey verification failed — database may be in an inconsistent state".into())
166 })?;
167 Ok(())
168 }
169
170 #[cfg(feature = "encryption")]
173 pub fn decrypt(path: &str, passphrase: &str, config: Config) -> crate::Result<()> {
174 use std::fs;
175 let tmp_path = format!("{}.decrypting", path);
176 let backup_path = format!("{}.backup", path);
177
178 {
180 let conn = Connection::open(path)?;
181 apply_passphrase(&conn, passphrase)?;
182 verify_access(&conn)?;
183 conn.execute_batch(&format!(
184 "ATTACH DATABASE '{}' AS plaintext KEY '';",
185 escape_sql_string(&tmp_path)
186 ))?;
187 conn.execute_batch("SELECT sqlcipher_export('plaintext');")?;
188 let ver: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
189 conn.execute_batch(&format!("PRAGMA plaintext.user_version = {};", ver))?;
190 conn.execute_batch("DETACH DATABASE plaintext;")?;
191 }
192
193 let mut verify_config = config;
195 verify_config.storage.encryption_enabled = false;
196 let verify_result = Store::open(&tmp_path, verify_config.clone(), None);
197 if let Err(e) = verify_result {
198 fs::remove_file(&tmp_path).ok();
199 return Err(Error::Encryption(format!(
200 "decrypted DB failed verification, original preserved: {}",
201 e
202 )));
203 }
204 drop(verify_result);
205
206 fs::rename(path, &backup_path)
208 .map_err(|e| Error::Encryption(format!("failed to backup original: {}", e)))?;
209 cleanup_wal_files(path);
210
211 if let Err(e) = fs::rename(&tmp_path, path) {
212 fs::rename(&backup_path, path).ok();
213 return Err(Error::Encryption(format!("failed to replace db: {}", e)));
214 }
215
216 fs::remove_file(&backup_path).ok();
218 Ok(())
219 }
220}
221
222fn apply_passphrase(conn: &Connection, passphrase: &str) -> crate::Result<()> {
223 conn.pragma_update(None, "key", passphrase)
225 .map_err(|e| Error::Encryption(format!("failed to set encryption key: {}", e)))?;
226 Ok(())
227}
228
229fn verify_access(conn: &Connection) -> crate::Result<()> {
230 conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(()))
231 .map_err(|_| {
232 Error::Encryption("cannot access database — wrong passphrase or not encrypted".into())
233 })?;
234 Ok(())
235}
236
237fn configure_connection(conn: &Connection, config: &Config) -> rusqlite::Result<()> {
238 conn.execute_batch(&format!(
239 "PRAGMA journal_mode = WAL;
240 PRAGMA busy_timeout = {};
241 PRAGMA synchronous = NORMAL;
242 PRAGMA foreign_keys = ON;
243 PRAGMA cache_size = -{};",
244 config.storage.busy_timeout_ms, config.storage.cache_size_kb
245 ))?;
246 Ok(())
247}
248
249fn run_migrations(conn: &mut Connection) -> crate::Result<()> {
250 let migrations = schema::migrations();
251 migrations
252 .to_latest(conn)
253 .map_err(|e| Error::Migration(e.to_string()))?;
254 Ok(())
255}
256
257#[cfg(feature = "encryption")]
258fn escape_sql_string(s: &str) -> String {
259 s.replace('\'', "''")
260}
261
262#[cfg(feature = "encryption")]
266fn cleanup_wal_files(path: &str) {
267 use std::fs;
268 fs::remove_file(format!("{}-shm", path)).ok();
269 fs::remove_file(format!("{}-wal", path)).ok();
270}