1use anyhow::{Context, Result, bail};
4use rusqlite::{Connection, OptionalExtension, TransactionBehavior};
5use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
6use std::path::Path;
7use std::sync::Arc;
8
9#[derive(Debug)]
12pub struct OwnedConnection {
13 conn: Connection,
14 coordination: Arc<crate::coordination::Coordination>,
15}
16
17impl OwnedConnection {
18 pub fn coordination(&self) -> Arc<crate::coordination::Coordination> {
19 self.coordination.clone()
20 }
21}
22impl std::ops::Deref for OwnedConnection {
23 type Target = Connection;
24 fn deref(&self) -> &Connection {
25 &self.conn
26 }
27}
28impl std::ops::DerefMut for OwnedConnection {
29 fn deref_mut(&mut self) -> &mut Connection {
30 &mut self.conn
31 }
32}
33
34pub struct CoordinatedConnection {
36 inner: std::sync::Mutex<OwnedConnection>,
37 coordination: Arc<crate::coordination::Coordination>,
38}
39pub struct ConnectionGuard<'a> {
40 inner: std::sync::MutexGuard<'a, OwnedConnection>,
41 _gate: crate::coordination::GateGuard,
42}
43impl CoordinatedConnection {
44 pub fn new(conn: OwnedConnection) -> Self {
45 let coordination = conn.coordination();
46 Self {
47 inner: std::sync::Mutex::new(conn),
48 coordination,
49 }
50 }
51 pub fn coordination(&self) -> Arc<crate::coordination::Coordination> {
52 self.coordination.clone()
53 }
54 pub fn lock(&self) -> Result<ConnectionGuard<'_>, &'static str> {
55 let gate = self.coordination.gate.lock();
56 let inner = self.inner.lock().map_err(|_| "database connection poisoned")?;
57 Ok(ConnectionGuard { inner, _gate: gate })
58 }
59}
60impl std::ops::Deref for ConnectionGuard<'_> {
61 type Target = Connection;
62 fn deref(&self) -> &Connection {
63 &self.inner
64 }
65}
66impl std::ops::DerefMut for ConnectionGuard<'_> {
67 fn deref_mut(&mut self) -> &mut Connection {
68 &mut self.inner
69 }
70}
71
72pub fn open(path: &Path) -> Result<OwnedConnection> {
84 let file = std::fs::OpenOptions::new()
85 .read(true)
86 .write(true)
87 .create(true)
88 .truncate(false)
89 .mode(0o600)
90 .custom_flags(libc::O_NOFOLLOW)
91 .open(path)
92 .with_context(|| format!("open {} (a symlink is refused)", path.display()))?;
93 make_private(&file, path)?;
94 let coordination = crate::coordination::Coordination::acquire(file, path)?;
95 let conn = configure(Connection::open(path)?)?;
96 for suffix in ["-wal", "-shm"] {
97 let mut name = path.as_os_str().to_os_string();
98 name.push(suffix);
99 let sidecar = Path::new(&name);
100 match std::fs::OpenOptions::new()
101 .read(true)
102 .custom_flags(libc::O_NOFOLLOW)
103 .open(sidecar)
104 {
105 Ok(file) => make_private(&file, sidecar)?,
106 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
108 Err(err) => {
109 return Err(err).with_context(|| format!("open {} (a symlink is refused)", sidecar.display()));
110 }
111 }
112 }
113 Ok(OwnedConnection { conn, coordination })
114}
115
116fn make_private(file: &std::fs::File, path: &Path) -> Result<()> {
118 file.set_permissions(std::fs::Permissions::from_mode(0o600))
119 .with_context(|| format!("make {} private", path.display()))
120}
121
122pub fn open_in_memory() -> Result<OwnedConnection> {
123 Ok(OwnedConnection {
124 conn: configure(Connection::open_in_memory()?)?,
125 coordination: Arc::default(),
126 })
127}
128
129fn configure(mut conn: Connection) -> Result<Connection> {
138 conn.pragma_update(None, "journal_mode", "WAL")?;
139 conn.pragma_update(None, "foreign_keys", "ON")?;
140 conn.pragma_update(None, "secure_delete", "ON")?;
145 conn.busy_timeout(std::time::Duration::from_secs(5))?;
146 conn.set_transaction_behavior(rusqlite::TransactionBehavior::Immediate);
147 Ok(conn)
148}
149
150pub fn migrate(conn: &mut Connection, migrations: &[&str]) -> Result<()> {
161 loop {
162 let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
163 let current: usize = tx.pragma_query_value(None, "user_version", |r| r.get::<_, i64>(0))? as usize;
164 if current > migrations.len() {
165 bail!(
166 "database schema version {current} is newer than this binary supports ({})",
167 migrations.len()
168 );
169 }
170 let Some(sql) = migrations.get(current) else {
171 return Ok(());
172 };
173 tx.execute_batch(sql)?;
174 tx.pragma_update(None, "user_version", (current + 1) as i64)?;
175 tx.commit()?;
176 }
177}
178
179pub fn migrate_component(conn: &mut Connection, component: &str, migrations: &[&str]) -> Result<()> {
186 loop {
187 let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
188 tx.execute_batch(
189 "CREATE TABLE IF NOT EXISTS schema_versions (component TEXT PRIMARY KEY, version INTEGER NOT NULL);",
190 )?;
191 let current: usize = tx
192 .query_row(
193 "SELECT version FROM schema_versions WHERE component = ?1",
194 [component],
195 |r| r.get::<_, i64>(0),
196 )
197 .optional()?
198 .unwrap_or(0) as usize;
199 if current > migrations.len() {
200 bail!(
201 "{component} schema version {current} is newer than this binary supports ({})",
202 migrations.len()
203 );
204 }
205 let Some(sql) = migrations.get(current) else {
206 tx.commit()?;
208 return Ok(());
209 };
210 tx.execute_batch(sql)?;
211 tx.execute(
212 "INSERT INTO schema_versions(component, version) VALUES (?1, ?2)
213 ON CONFLICT(component) DO UPDATE SET version = excluded.version",
214 rusqlite::params![component, (current + 1) as i64],
215 )?;
216 tx.commit()?;
217 }
218}
219
220pub fn kernel_owner(conn: &mut Connection) -> Result<String> {
227 migrate_component(conn, crate::schema::COMPONENT, crate::schema::MIGRATIONS)?;
228 conn.query_row("SELECT id FROM owners ORDER BY created_at, id LIMIT 1", [], |r| {
229 r.get(0)
230 })
231 .optional()?
232 .context("the database has no owner")
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use std::os::unix::fs::PermissionsExt;
239
240 fn mode(path: &Path) -> u32 {
241 std::fs::metadata(path).unwrap().permissions().mode() & 0o777
242 }
243
244 #[test]
247 fn opening_makes_an_existing_database_and_its_wal_files_private() {
248 let dir = tempfile::tempdir().unwrap();
249 let path = dir.path().join("hennery.db");
250 let first = open(&path).unwrap();
252 first
253 .execute_batch("CREATE TABLE t (x INTEGER); INSERT INTO t VALUES (1);")
254 .unwrap();
255 let files = [
256 path.clone(),
257 dir.path().join("hennery.db-wal"),
258 dir.path().join("hennery.db-shm"),
259 ];
260 for file in &files {
261 std::fs::set_permissions(file, std::fs::Permissions::from_mode(0o644)).unwrap();
262 }
263 let _second = open(&path).unwrap();
264 for file in &files {
265 assert_eq!(mode(file), 0o600, "{}", file.display());
266 }
267 }
268
269 #[test]
273 fn a_symlinked_database_is_refused_and_its_target_left_alone() {
274 let dir = tempfile::tempdir().unwrap();
275 let target = dir.path().join("elsewhere");
276 std::fs::write(&target, b"not a database").unwrap();
277 std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o644)).unwrap();
278 let db = dir.path().join("hennery.db");
279 std::os::unix::fs::symlink(&target, &db).unwrap();
280 let err = open(&db).expect_err("a symlinked database was opened");
281 assert!(format!("{err:#}").contains("a symlink is refused"), "{err:#}");
282 assert_eq!(mode(&target), 0o644);
283 assert_eq!(std::fs::read(&target).unwrap(), b"not a database");
284 }
285
286 #[test]
291 fn a_transaction_holds_the_write_lock_from_its_start() {
292 let dir = tempfile::tempdir().unwrap();
293 let path = dir.path().join("hennery.db");
294 let mut first = open(&path).unwrap();
295 first.execute_batch("CREATE TABLE t (x INTEGER);").unwrap();
296 let second = open(&path).unwrap();
297 second.busy_timeout(std::time::Duration::ZERO).unwrap();
298
299 let tx = first.transaction().unwrap();
300 let count: i64 = tx.query_row("SELECT count(*) FROM t", [], |r| r.get(0)).unwrap();
301 assert_eq!(count, 0);
302 let err = second.execute("INSERT INTO t VALUES (1)", []).unwrap_err();
303 assert_eq!(
304 err.sqlite_error_code(),
305 Some(rusqlite::ErrorCode::DatabaseBusy),
306 "{err}"
307 );
308 tx.execute("INSERT INTO t VALUES (2)", []).unwrap();
309 tx.commit().unwrap();
310 second.execute("INSERT INTO t VALUES (3)", []).unwrap();
311 }
312
313 #[test]
314 fn migrations_apply_once_and_record_the_version() {
315 let mut conn = open_in_memory().unwrap();
316 let steps = ["CREATE TABLE a (x INTEGER);", "CREATE TABLE b (y INTEGER);"];
317 migrate(&mut conn, &steps).unwrap();
318 migrate(&mut conn, &steps).unwrap();
319 let v: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0)).unwrap();
320 assert_eq!(v, 2);
321 }
322
323 #[test]
324 fn a_newer_database_is_refused() {
325 let mut conn = open_in_memory().unwrap();
326 migrate(
327 &mut conn,
328 &["CREATE TABLE a (x INTEGER);", "CREATE TABLE b (y INTEGER);"],
329 )
330 .unwrap();
331 let err = migrate(&mut conn, &["CREATE TABLE a (x INTEGER);"]).unwrap_err();
332 assert!(err.to_string().contains("newer"), "{err}");
333 }
334
335 #[test]
336 fn components_keep_their_own_versions_beside_user_version() {
337 let mut conn = open_in_memory().unwrap();
338 migrate(&mut conn, &["CREATE TABLE a (x INTEGER);"]).unwrap();
339 migrate_component(
340 &mut conn,
341 "kernel",
342 &["CREATE TABLE k1 (x INTEGER);", "CREATE TABLE k2 (x INTEGER);"],
343 )
344 .unwrap();
345 migrate_component(
346 &mut conn,
347 "kernel",
348 &["CREATE TABLE k1 (x INTEGER);", "CREATE TABLE k2 (x INTEGER);"],
349 )
350 .unwrap();
351 migrate_component(&mut conn, "other", &["CREATE TABLE o1 (x INTEGER);"]).unwrap();
352 let user_version: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0)).unwrap();
353 let kernel: i64 = conn
354 .query_row(
355 "SELECT version FROM schema_versions WHERE component = 'kernel'",
356 [],
357 |r| r.get(0),
358 )
359 .unwrap();
360 assert_eq!((user_version, kernel), (1, 2));
361 migrate(&mut conn, &["CREATE TABLE a (x INTEGER);"]).unwrap();
363 }
364
365 fn unconfigured(path: &Path) -> Connection {
369 let conn = Connection::open(path).unwrap();
370 conn.busy_timeout(std::time::Duration::from_secs(5)).unwrap();
371 conn
372 }
373
374 #[test]
384 fn a_migration_racing_another_connection_is_applied_once() {
385 let dir = tempfile::tempdir().unwrap();
386 let path = dir.path().join("hennery.db");
387 let steps = ["CREATE TABLE a (x INTEGER);"];
388 let mut first = open(&path).unwrap();
389 let tx = first.transaction().unwrap();
390 let racing = {
391 let path = path.clone();
392 std::thread::spawn(move || migrate(&mut unconfigured(&path), &steps))
393 };
394 std::thread::sleep(std::time::Duration::from_millis(200));
395 tx.execute_batch(steps[0]).unwrap();
396 tx.pragma_update(None, "user_version", 1).unwrap();
397 tx.commit().unwrap();
398 racing.join().unwrap().expect("the racing migration failed");
399 let v: i64 = first.pragma_query_value(None, "user_version", |r| r.get(0)).unwrap();
400 assert_eq!(v, 1);
401 }
402
403 #[test]
405 fn a_component_migration_racing_another_connection_is_applied_once() {
406 let dir = tempfile::tempdir().unwrap();
407 let path = dir.path().join("hennery.db");
408 let steps = ["CREATE TABLE k1 (x INTEGER);"];
409 let mut first = open(&path).unwrap();
410 migrate_component(&mut first, "kernel", &[]).unwrap();
411 let tx = first.transaction().unwrap();
412 let racing = {
413 let path = path.clone();
414 std::thread::spawn(move || migrate_component(&mut unconfigured(&path), "kernel", &steps))
415 };
416 std::thread::sleep(std::time::Duration::from_millis(200));
417 tx.execute_batch(steps[0]).unwrap();
418 tx.execute(
419 "INSERT INTO schema_versions(component, version) VALUES ('kernel', 1)",
420 [],
421 )
422 .unwrap();
423 tx.commit().unwrap();
424 racing.join().unwrap().expect("the racing migration failed");
425 let v: i64 = first
426 .query_row(
427 "SELECT version FROM schema_versions WHERE component = 'kernel'",
428 [],
429 |r| r.get(0),
430 )
431 .unwrap();
432 assert_eq!(v, 1);
433 }
434
435 #[test]
436 fn a_newer_component_is_refused() {
437 let mut conn = open_in_memory().unwrap();
438 migrate_component(
439 &mut conn,
440 "kernel",
441 &["CREATE TABLE k1 (x INTEGER);", "CREATE TABLE k2 (x INTEGER);"],
442 )
443 .unwrap();
444 let err = migrate_component(&mut conn, "kernel", &["CREATE TABLE k1 (x INTEGER);"]).unwrap_err();
445 assert!(err.to_string().contains("kernel schema version 2 is newer"), "{err}");
446 }
447}