1use std::path::{Path, PathBuf};
6use std::sync::{Arc, Mutex, MutexGuard};
7
8use rusqlite::{Connection, OptionalExtension, Transaction, TransactionBehavior, params};
9use uuid::Uuid;
10use vcon::read::Inspection;
11
12use crate::audit::{self, Action, Actor, Entry};
13use crate::catalog;
14use crate::clock::{Clock, nanos};
15use crate::envelope::{self, Context, Key, WrapLimit};
16use crate::fs::Fs;
17use crate::ids::{KeyId, TenantId, VersionId};
18use crate::retention::Retention;
19
20mod backup;
21mod examine;
22mod legal;
23mod list;
24mod purge;
25mod rekey;
26
27pub use backup::{BackupError, Copied, TRIES};
28pub use examine::Examination;
29pub use legal::{Legal, VersionInfo};
30pub use list::{Cursor, ListQuery, Listed, Listing};
31pub use purge::{Kept, PurgeReport, due};
32
33const TABLES: &str = "
37CREATE TABLE vcon (
38 uuid BLOB PRIMARY KEY NOT NULL,
39 current BLOB,
40 deleted_at INTEGER,
41 hold INTEGER NOT NULL DEFAULT 0,
42 stored_at INTEGER NOT NULL DEFAULT 0
43);
44CREATE INDEX vcon_listed ON vcon (stored_at, uuid);
45CREATE TABLE version (
46 version BLOB PRIMARY KEY NOT NULL,
47 uuid BLOB NOT NULL,
48 form TEXT NOT NULL,
49 gzip INTEGER NOT NULL,
50 size INTEGER NOT NULL,
51 stored_at INTEGER NOT NULL,
52 hidden_at INTEGER,
53 tenant_key_id BLOB NOT NULL,
54 wrapped_key BLOB NOT NULL
55);
56CREATE INDEX version_uuid ON version (uuid);
57CREATE TABLE tombstone (
58 uuid BLOB PRIMARY KEY NOT NULL,
59 purged_at INTEGER NOT NULL
60);
61CREATE TABLE key_use (
62 tenant_key_id BLOB PRIMARY KEY NOT NULL,
63 wraps INTEGER NOT NULL
64);
65";
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct Caller {
70 pub key: KeyId,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum Created {
76 New(VersionId),
78 Same(VersionId),
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct Stored {
85 pub version: VersionId,
86 pub form: vcon::Form,
87 pub gzip: bool,
89 pub body: Vec<u8>,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct Counts {
96 pub versions: u64,
98 pub held: u64,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum StoreError {
105 Conflict,
106 Gone,
107 NotFound,
108 NotDeleted,
109 PreconditionFailed,
110 Immutable,
111 LimitReached,
113 Damaged,
115 AuditFailed,
117 Io,
118}
119
120#[derive(Debug)]
123pub enum OpenError {
124 WrongKey,
126 Access {
129 path: PathBuf,
130 error: std::io::Error,
131 },
132 Store(StoreError),
134}
135
136impl From<OpenError> for StoreError {
137 fn from(_: OpenError) -> StoreError {
139 StoreError::Io
140 }
141}
142
143impl From<StoreError> for OpenError {
144 fn from(e: StoreError) -> OpenError {
145 OpenError::Store(e)
146 }
147}
148
149impl From<catalog::CatalogError> for OpenError {
150 fn from(e: catalog::CatalogError) -> OpenError {
151 match e {
152 catalog::CatalogError::WrongKey => OpenError::WrongKey,
153 catalog::CatalogError::Access { path, error } => OpenError::Access { path, error },
154 _ => OpenError::Store(StoreError::Io),
155 }
156 }
157}
158
159pub struct Store {
161 tenant: TenantId,
162 tenant_key: Key,
163 objects: PathBuf,
164 conn: Mutex<Connection>,
165 fs: Arc<dyn Fs>,
166 clock: Arc<dyn Clock>,
167 #[cfg(test)]
168 hooks: tests::Hooks,
169}
170
171impl Store {
172 pub fn open(
175 dir: &Path,
176 tenant: TenantId,
177 tenant_key: Key,
178 fs: Arc<dyn Fs>,
179 clock: Arc<dyn Clock>,
180 ) -> Result<Store, OpenError> {
181 let store = Store::open_beside(dir, tenant, tenant_key, fs, clock)?;
182 store.recover()?;
183 Ok(store)
184 }
185
186 pub fn open_beside(
190 dir: &Path,
191 tenant: TenantId,
192 tenant_key: Key,
193 fs: Arc<dyn Fs>,
194 clock: Arc<dyn Clock>,
195 ) -> Result<Store, OpenError> {
196 let catalog_key = envelope::catalog_key(&tenant_key).map_err(|_| StoreError::Io)?;
197 let schema = format!("{TABLES}{}", audit::SCHEMA);
198 let conn = catalog::open(&dir.join("catalog.sqlite"), &catalog_key, &schema)?;
199 let objects = dir.join("objects");
200 fs.create_dir_all(&objects).map_err(|_| StoreError::Io)?;
201 catalog::may_use(&objects, libc::W_OK | libc::X_OK)?;
203 fs.sync_dir(dir).map_err(|_| StoreError::Io)?;
207 Ok(Store {
208 tenant,
209 tenant_key,
210 objects,
211 conn: Mutex::new(conn),
212 fs,
213 clock,
214 #[cfg(test)]
215 hooks: tests::Hooks::default(),
216 })
217 }
218
219 fn recover(&self) -> Result<(), StoreError> {
224 let conn = self.conn();
225 let busy: i64 = conn
226 .query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |r| r.get(0))
227 .map_err(io)?;
228 if busy != 0 {
229 return Err(StoreError::Io);
230 }
231 let mut removed = false;
232 for name in self.fs.list(&self.objects).map_err(io)? {
233 let Some(name) = name.to_str() else { continue };
234 let (id, temporary) = match name.strip_suffix(".tmp") {
235 Some(id) => (id, true),
236 None => (name, false),
237 };
238 let Some(version) = VersionId::parse(id) else {
239 continue;
240 };
241 let has_row = conn
242 .query_row(
243 "SELECT 1 FROM version WHERE version = ?1",
244 [version.0],
245 |_| Ok(()),
246 )
247 .optional()
248 .map_err(io)?
249 .is_some();
250 if temporary || !has_row {
251 self.fs.remove(&self.objects.join(name)).map_err(io)?;
252 removed = true;
253 }
254 }
255 if removed {
256 self.fs.sync_dir(&self.objects).map_err(io)?;
257 }
258 Ok(())
259 }
260
261 pub fn create(
266 &self,
267 who: &Caller,
268 found: &Inspection,
269 body: &[u8],
270 ) -> Result<Created, StoreError> {
271 let uuid = found.uuid;
272 let entry = |status, version| Entry {
273 actor: Actor::Key(who.key),
274 action: Action::Create,
275 target: Some(uuid.to_string()),
276 version,
277 status,
278 };
279 let key_id = self.tenant_key.id();
280 {
281 let mut conn = self.conn();
282 let tx = immediate(&mut conn)?;
283 let refused = match state(&tx, uuid)? {
284 State::Gone => Some((410, None, StoreError::Gone)),
285 State::Live(current) => match self.read_version(&tx, uuid, current) {
286 Ok(stored) if stored == body => {
287 self.record(&tx, &entry(200, Some(current)))?;
288 commit(tx)?;
289 return Ok(Created::Same(current));
290 }
291 Ok(_) => Some((409, None, StoreError::Conflict)),
292 Err(StoreError::Damaged) => Some((500, Some(current), StoreError::Damaged)),
293 Err(e) => return Err(e),
294 },
295 State::Absent if envelope::wrap_limit(wraps(&tx, key_id)?) == WrapLimit::Refuse => {
296 Some((503, None, StoreError::LimitReached))
297 }
298 State::Absent => {
301 add_wrap(&tx, key_id)?;
302 None
303 }
304 };
305 if let Some((status, version, error)) = refused {
306 self.record(&tx, &entry(status, version))?;
307 commit(tx)?;
308 return Err(error);
309 }
310 commit(tx)?;
311 }
312
313 let (version, wrapped, file) = self.write_version(uuid, body)?;
314 self.pause();
315
316 let committed = (|| {
317 let mut conn = self.conn();
318 let tx = immediate(&mut conn)?;
319 match state(&tx, uuid)? {
320 State::Absent => {}
321 lost => {
322 let (status, error) = match lost {
323 State::Gone => (410, StoreError::Gone),
324 _ => (409, StoreError::Conflict),
325 };
326 self.record(&tx, &entry(status, None))?;
327 commit(tx)?;
328 return Err(error);
329 }
330 }
331 let stored_at = nanos(self.clock.now());
334 tx.execute(
335 "INSERT INTO version
336 (version, uuid, form, gzip, size, stored_at, hidden_at, tenant_key_id, wrapped_key)
337 VALUES (?1, ?2, ?3, ?4, ?5, ?6, NULL, ?7, ?8)",
338 params![
339 version.0,
340 uuid.as_bytes(),
341 form_name(found.form),
342 found.gzip,
343 i64::try_from(body.len()).map_err(io)?,
344 stored_at,
345 key_id.bytes(),
346 wrapped,
347 ],
348 )
349 .map_err(io)?;
350 tx.execute(
351 "INSERT INTO vcon (uuid, current, stored_at) VALUES (?1, ?2, ?3)",
352 params![uuid.as_bytes(), version.0, stored_at],
353 )
354 .map_err(io)?;
355 self.record(&tx, &entry(201, Some(version)))?;
356 commit(tx)?;
357 Ok(Created::New(version))
358 })();
359 if committed.is_err() {
360 let _ = self.fs.remove(&file);
362 }
363 committed
364 }
365
366 pub fn get(&self, who: &Caller, uuid: Uuid) -> Result<Stored, StoreError> {
371 let entry = |status, version| Entry {
372 actor: Actor::Key(who.key),
373 action: Action::Read,
374 target: Some(uuid.to_string()),
375 version,
376 status,
377 };
378 let refuse = |tx: Transaction, state: State| {
379 let (status, error) = match state {
380 State::Gone => (410, StoreError::Gone),
381 _ => (404, StoreError::NotFound),
382 };
383 self.record(&tx, &entry(status, None))?;
384 commit(tx)?;
385 Err(error)
386 };
387 loop {
388 let (version, row) = {
389 let mut conn = self.conn();
390 let tx = immediate(&mut conn)?;
391 match state(&tx, uuid)? {
392 State::Live(version) => (version, version_row(&tx, version)),
393 other => return refuse(tx, other),
394 }
395 };
396 let opened = row.and_then(|row| {
397 let body = self.open_version(uuid, version, &row.wrapped)?;
398 Ok(Stored {
399 version,
400 form: row.form,
401 gzip: row.gzip,
402 body,
403 })
404 });
405 self.pause();
406 let mut conn = self.conn();
407 let tx = immediate(&mut conn)?;
408 match state(&tx, uuid)? {
409 State::Live(now) if now == version => {
410 let status = if opened.is_ok() { 200 } else { 500 };
411 self.record(&tx, &entry(status, Some(version)))?;
412 commit(tx)?;
413 return opened;
414 }
415 State::Live(_) => continue,
416 other => return refuse(tx, other),
417 }
418 }
419 }
420
421 pub fn replace(
428 &self,
429 who: &Caller,
430 if_match: VersionId,
431 found: &Inspection,
432 body: &[u8],
433 ) -> Result<VersionId, StoreError> {
434 let uuid = found.uuid;
435 let entry = |status, version| Entry {
436 actor: Actor::Key(who.key),
437 action: Action::Replace,
438 target: Some(uuid.to_string()),
439 version,
440 status,
441 };
442 let key_id = self.tenant_key.id();
443 let refusal = |tx: &Transaction| -> Result<Option<(u16, StoreError)>, StoreError> {
445 Ok(match state(tx, uuid)? {
446 State::Absent => Some((404, StoreError::NotFound)),
447 State::Gone => Some((410, StoreError::Gone)),
448 State::Live(current) if version_row(tx, current)?.form != vcon::Form::Unsigned => {
449 Some((409, StoreError::Immutable))
450 }
451 State::Live(current) if current != if_match => {
452 Some((412, StoreError::PreconditionFailed))
453 }
454 State::Live(_) => None,
455 })
456 };
457 {
458 let mut conn = self.conn();
459 let tx = immediate(&mut conn)?;
460 let refused = match refusal(&tx)? {
461 None if envelope::wrap_limit(wraps(&tx, key_id)?) == WrapLimit::Refuse => {
462 Some((503, StoreError::LimitReached))
463 }
464 other => other,
465 };
466 if let Some((status, error)) = refused {
467 self.record(&tx, &entry(status, None))?;
468 commit(tx)?;
469 return Err(error);
470 }
471 add_wrap(&tx, key_id)?;
472 commit(tx)?;
473 }
474
475 let (version, wrapped, file) = self.write_version(uuid, body)?;
476 self.pause();
477
478 let committed = (|| {
479 let mut conn = self.conn();
480 let tx = immediate(&mut conn)?;
481 if let Some((status, error)) = refusal(&tx)? {
482 self.record(&tx, &entry(status, None))?;
483 commit(tx)?;
484 return Err(error);
485 }
486 let now = nanos(self.clock.now());
487 tx.execute(
488 "INSERT INTO version
489 (version, uuid, form, gzip, size, stored_at, hidden_at, tenant_key_id, wrapped_key)
490 VALUES (?1, ?2, ?3, ?4, ?5, ?6, NULL, ?7, ?8)",
491 params![
492 version.0,
493 uuid.as_bytes(),
494 form_name(found.form),
495 found.gzip,
496 i64::try_from(body.len()).map_err(io)?,
497 now,
498 key_id.bytes(),
499 wrapped,
500 ],
501 )
502 .map_err(io)?;
503 tx.execute(
504 "UPDATE version SET hidden_at = ?1 WHERE version = ?2",
505 params![now, if_match.0],
506 )
507 .map_err(io)?;
508 tx.execute(
509 "UPDATE vcon SET current = ?1 WHERE uuid = ?2",
510 params![version.0, uuid.as_bytes()],
511 )
512 .map_err(io)?;
513 self.record(&tx, &entry(200, Some(version)))?;
514 commit(tx)?;
515 Ok(version)
516 })();
517 if committed.is_err() {
518 let _ = self.fs.remove(&file);
520 }
521 committed
522 }
523
524 pub fn delete(&self, who: &Caller, uuid: Uuid, retention: Retention) -> Result<(), StoreError> {
532 let entry = |status| Entry {
533 actor: Actor::Key(who.key),
534 action: Action::Delete,
535 target: Some(uuid.to_string()),
536 version: None,
537 status,
538 };
539 let mut conn = self.conn();
540 let tx = immediate(&mut conn)?;
541 let outcome = match state(&tx, uuid)? {
542 State::Absent => Err(StoreError::NotFound),
543 State::Gone => Ok(()),
544 State::Live(_) => {
545 let now = nanos(self.clock.now());
546 tx.execute(
547 "UPDATE vcon SET current = NULL, deleted_at = ?1 WHERE uuid = ?2",
548 params![now, uuid.as_bytes()],
549 )
550 .map_err(io)?;
551 tx.execute(
552 "UPDATE version SET hidden_at = ?1 WHERE uuid = ?2 AND hidden_at IS NULL",
553 params![now, uuid.as_bytes()],
554 )
555 .map_err(io)?;
556 Ok(())
557 }
558 };
559 let status = if outcome.is_ok() { 204 } else { 404 };
560 self.record(&tx, &entry(status))?;
561 commit(tx)?;
562 outcome?;
563 drop(conn);
564 self.purge_hidden(Actor::Key(who.key), retention, Some(uuid))?;
567 Ok(())
568 }
569
570 pub fn check(&self) -> Result<Vec<VersionId>, StoreError> {
573 let conn = self.conn();
574 let mut rows = conn
575 .prepare("SELECT version FROM version ORDER BY version")
576 .map_err(io)?;
577 let versions = rows
578 .query_map([], |r| r.get::<_, [u8; 16]>(0))
579 .map_err(io)?
580 .collect::<Result<Vec<_>, _>>()
581 .map_err(io)?;
582 let present: std::collections::BTreeSet<_> = self
583 .fs
584 .list(&self.objects)
585 .map_err(io)?
586 .into_iter()
587 .collect();
588 Ok(versions
589 .into_iter()
590 .map(VersionId)
591 .filter(|v| !present.contains(std::ffi::OsStr::new(&v.to_string())))
592 .collect())
593 }
594
595 fn write_version(
599 &self,
600 uuid: Uuid,
601 body: &[u8],
602 ) -> Result<(VersionId, Vec<u8>, PathBuf), StoreError> {
603 let version = VersionId::generate();
604 let ctx = Context::Version {
605 tenant: self.tenant,
606 uuid,
607 version,
608 };
609 let data_key = Key::generate().map_err(|_| StoreError::Io)?;
610 let blob = envelope::seal(&data_key, &ctx, body);
611 let wrapped = envelope::wrap(&self.tenant_key, &ctx, &data_key);
612 let file = self.write_object(version, &blob)?;
613 Ok((version, wrapped, file))
614 }
615
616 fn pause(&self) {
619 #[cfg(test)]
620 {
621 let hook = self.hooks.between.lock().expect("hook").take();
622 if let Some(hook) = hook {
623 hook();
624 }
625 }
626 }
627
628 fn write_object(&self, version: VersionId, blob: &[u8]) -> Result<PathBuf, StoreError> {
632 let tmp = self.objects.join(format!("{version}.tmp"));
633 let file = self.objects.join(version.to_string());
634 let written = self
635 .fs
636 .write_new(&tmp, blob)
637 .and_then(|()| self.fs.sync_file(&tmp))
638 .and_then(|()| self.fs.rename(&tmp, &file))
639 .and_then(|()| self.fs.sync_dir(&self.objects));
640 if written.is_err() {
641 let _ = self.fs.remove(&tmp);
643 let _ = self.fs.remove(&file);
644 return Err(StoreError::Io);
645 }
646 Ok(file)
647 }
648
649 fn read_version(
652 &self,
653 tx: &Transaction,
654 uuid: Uuid,
655 version: VersionId,
656 ) -> Result<Vec<u8>, StoreError> {
657 let row = version_row(tx, version)?;
658 self.open_version(uuid, version, &row.wrapped)
659 }
660
661 fn open_version(
663 &self,
664 uuid: Uuid,
665 version: VersionId,
666 wrapped: &[u8],
667 ) -> Result<Vec<u8>, StoreError> {
668 let ctx = Context::Version {
669 tenant: self.tenant,
670 uuid,
671 version,
672 };
673 let data_key =
674 envelope::unwrap(&self.tenant_key, &ctx, wrapped).map_err(|_| StoreError::Damaged)?;
675 let blob = self
676 .fs
677 .read(&self.objects.join(version.to_string()))
678 .map_err(|_| StoreError::Damaged)?;
679 envelope::open(&data_key, &ctx, &blob).map_err(|_| StoreError::Damaged)
680 }
681
682 pub fn counts(&self) -> Result<Counts, StoreError> {
684 let conn = self.conn();
685 let count = |sql| -> Result<u64, StoreError> {
686 let n: i64 = conn.query_row(sql, [], |r| r.get(0)).map_err(io)?;
687 u64::try_from(n).map_err(io)
688 };
689 Ok(Counts {
690 versions: count("SELECT count(*) FROM version")?,
691 held: count("SELECT count(*) FROM vcon WHERE hold != 0")?,
692 })
693 }
694
695 pub fn export(
698 &self,
699 since: Option<i64>,
700 out: &mut dyn std::io::Write,
701 ) -> Result<(), audit::AuditError> {
702 audit::export(&self.conn(), since, out)
703 }
704
705 pub fn audit_page(
710 &self,
711 who: &Caller,
712 query: &audit::AuditQuery,
713 ) -> Result<Vec<u8>, StoreError> {
714 let mut conn = self.conn();
715 let tx = immediate(&mut conn)?;
716 let mut out = Vec::new();
717 let read = audit::page(&tx, query, &mut out);
718 self.record(
719 &tx,
720 &Entry {
721 actor: Actor::Key(who.key),
722 action: Action::AuditRead,
723 target: None,
724 version: None,
725 status: if read.is_ok() { 200 } else { 500 },
726 },
727 )?;
728 commit(tx)?;
729 read.map(|()| out).map_err(io)
730 }
731
732 pub fn log(&self, entry: &Entry) -> Result<(), StoreError> {
735 let mut conn = self.conn();
736 let tx = immediate(&mut conn)?;
737 self.record(&tx, entry)?;
738 commit(tx)
739 }
740
741 #[cfg(test)]
743 pub(crate) fn break_audit(&self) {
744 self.conn()
745 .execute_batch(
746 "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
747 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
748 )
749 .expect("trigger");
750 }
751
752 #[cfg(test)]
755 pub(crate) fn on_backup_copied(&self, hook: tests::BackupHook) {
756 *self.hooks.copied.lock().expect("hook") = Some(hook);
757 }
758
759 #[cfg(test)]
761 pub(crate) fn conn_for_test(&self) -> MutexGuard<'_, Connection> {
762 self.conn()
763 }
764
765 #[cfg(test)]
767 pub(crate) fn records(&self) -> Vec<serde_json::Value> {
768 crate::audit::testing::exported(&self.conn(), None)
769 }
770
771 pub fn count(&self, entry: &Entry) -> Result<(), StoreError> {
775 let mut conn = self.conn();
776 let tx = immediate(&mut conn)?;
777 audit::count(&tx, entry, self.clock.now()).map_err(|_| StoreError::AuditFailed)?;
778 commit(tx)
779 }
780
781 fn record(&self, tx: &Transaction, entry: &Entry) -> Result<(), StoreError> {
783 audit::record(tx, entry, self.clock.now()).map_err(|_| StoreError::AuditFailed)
784 }
785
786 fn conn(&self) -> MutexGuard<'_, Connection> {
787 self.conn
788 .lock()
789 .expect("no thread panicked holding the catalog")
790 }
791}
792
793enum State {
795 Absent,
797 Live(VersionId),
799 Gone,
801}
802
803fn state(tx: &Transaction, uuid: Uuid) -> Result<State, StoreError> {
804 let purged = tx
805 .query_row(
806 "SELECT 1 FROM tombstone WHERE uuid = ?1",
807 [uuid.as_bytes()],
808 |_| Ok(()),
809 )
810 .optional()
811 .map_err(io)?;
812 if purged.is_some() {
813 return Ok(State::Gone);
814 }
815 let row: Option<(Option<[u8; 16]>, Option<i64>)> = tx
816 .query_row(
817 "SELECT current, deleted_at FROM vcon WHERE uuid = ?1",
818 [uuid.as_bytes()],
819 |r| Ok((r.get(0)?, r.get(1)?)),
820 )
821 .optional()
822 .map_err(io)?;
823 match row {
824 None => Ok(State::Absent),
825 Some((_, Some(_))) => Ok(State::Gone),
826 Some((Some(current), None)) => Ok(State::Live(VersionId(current))),
827 Some((None, None)) => Err(StoreError::Damaged),
829 }
830}
831
832struct VersionRow {
834 form: vcon::Form,
835 gzip: bool,
836 wrapped: Vec<u8>,
837}
838
839fn version_row(tx: &Transaction, version: VersionId) -> Result<VersionRow, StoreError> {
841 let row: Option<(String, bool, Vec<u8>)> = tx
842 .query_row(
843 "SELECT form, gzip, wrapped_key FROM version WHERE version = ?1",
844 [version.0],
845 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
846 )
847 .optional()
848 .map_err(io)?;
849 let (form, gzip, wrapped) = row.ok_or(StoreError::Damaged)?;
850 let form = match form.as_str() {
851 "unsigned" => vcon::Form::Unsigned,
852 "signed" => vcon::Form::Signed,
853 "encrypted" => vcon::Form::Encrypted,
854 _ => return Err(StoreError::Damaged),
855 };
856 Ok(VersionRow {
857 form,
858 gzip,
859 wrapped,
860 })
861}
862
863fn wraps(tx: &Transaction, key: KeyId) -> Result<u64, StoreError> {
865 let wraps: Option<i64> = tx
866 .query_row(
867 "SELECT wraps FROM key_use WHERE tenant_key_id = ?1",
868 [key.bytes()],
869 |r| r.get(0),
870 )
871 .optional()
872 .map_err(io)?;
873 u64::try_from(wraps.unwrap_or(0)).map_err(io)
874}
875
876fn add_wrap(tx: &Transaction, key: KeyId) -> Result<(), StoreError> {
877 tx.execute(
878 "INSERT INTO key_use (tenant_key_id, wraps) VALUES (?1, 1)
879 ON CONFLICT (tenant_key_id) DO UPDATE SET wraps = wraps + 1",
880 [key.bytes()],
881 )
882 .map_err(io)?;
883 Ok(())
884}
885
886fn immediate(conn: &mut Connection) -> Result<Transaction<'_>, StoreError> {
887 conn.transaction_with_behavior(TransactionBehavior::Immediate)
888 .map_err(io)
889}
890
891fn commit(tx: Transaction) -> Result<(), StoreError> {
892 tx.commit().map_err(io)
893}
894
895fn io<E>(_: E) -> StoreError {
896 StoreError::Io
897}
898
899fn form_name(form: vcon::Form) -> &'static str {
900 match form {
901 vcon::Form::Unsigned => "unsigned",
902 vcon::Form::Signed => "signed",
903 vcon::Form::Encrypted => "encrypted",
904 }
905}
906
907#[cfg(test)]
909pub(crate) mod testing {
910 use std::collections::BTreeSet;
911
912 use vcon::Form;
913 use vcon::read::{Inspection, Limits, inspect};
914
915 pub(crate) fn vcon(uuid: &str, subject: &str) -> (Inspection, Vec<u8>) {
917 let body = format!(
918 r#"{{"uuid": "{uuid}", "created_at": "2026-10-23T12:00:00Z", "subject": "{subject}", "parties": []}}"#
919 )
920 .into_bytes();
921 let found =
922 inspect(&body, None, &Limits::new(1 << 20), &BTreeSet::new()).expect("a valid vCon");
923 assert_eq!(found.form, Form::Unsigned);
924 (found, body)
925 }
926}
927
928#[cfg(test)]
929mod tests {
930 use std::collections::BTreeSet;
931
932 use serde_json::Value;
933 use time::macros::datetime;
934 use vcon::read::{Limits, inspect};
935
936 use super::testing::vcon;
937 use super::*;
938 use crate::audit::testing::exported;
939
940 #[test]
944 fn an_objects_directory_the_process_may_not_write_is_named() {
945 use std::os::unix::fs::PermissionsExt;
946 let f = fixture();
947 let objects = f.dir.path().join("objects");
948 let mode = |m| {
949 std::fs::set_permissions(&objects, std::fs::Permissions::from_mode(m)).expect("chmod")
950 };
951 mode(0o500);
952 let refused = f.open_or_why();
953 mode(0o700);
954 match refused {
955 Err(OpenError::Access { path, error }) => {
956 assert_eq!(path, objects);
957 assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
958 }
959 other => panic!("{:?}", other.map(|_| "opened")),
960 }
961 assert!(f.open_or_why().is_ok(), "opens once allowed");
962 }
963 use crate::catalog::testing::columns_taking_null;
964 use crate::clock::ManualClock;
965 use crate::fs::{CrashFs, Op};
966
967 const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
968
969 pub(crate) type BackupHook = Box<dyn FnMut(&Store) + Send>;
971
972 #[derive(Default)]
974 pub(super) struct Hooks {
975 pub(super) between: Mutex<Option<Box<dyn FnOnce() + Send>>>,
979 pub(super) copied: Mutex<Option<BackupHook>>,
982 }
983
984 pub(super) struct Fixture {
985 pub(super) dir: tempfile::TempDir,
986 pub(super) fs: Arc<CrashFs>,
987 pub(super) clock: Arc<ManualClock>,
988 master: Key,
991 wrapped: Vec<u8>,
992 pub(super) store: Store,
993 pub(super) caller: Caller,
994 }
995
996 pub(super) const TENANT: TenantId = TenantId([0x11; 16]);
997
998 impl Fixture {
999 pub(super) fn open(&self) -> Result<Store, StoreError> {
1000 self.open_or_why().map_err(StoreError::from)
1001 }
1002
1003 pub(super) fn open_or_why(&self) -> Result<Store, OpenError> {
1005 let ctx = Context::Tenant { tenant: TENANT };
1006 let tenant_key = envelope::unwrap(&self.master, &ctx, &self.wrapped).expect("unwraps");
1007 Store::open(
1008 self.dir.path(),
1009 TENANT,
1010 tenant_key,
1011 self.fs.clone(),
1012 self.clock.clone(),
1013 )
1014 }
1015
1016 pub(super) fn tenant_key(&self) -> Key {
1018 let ctx = Context::Tenant { tenant: TENANT };
1019 envelope::unwrap(&self.master, &ctx, &self.wrapped).expect("unwraps")
1020 }
1021
1022 pub(super) fn tenant(&self) -> TenantId {
1023 TENANT
1024 }
1025
1026 fn open_beside(&self) -> Result<Store, StoreError> {
1028 let ctx = Context::Tenant { tenant: TENANT };
1029 let tenant_key = envelope::unwrap(&self.master, &ctx, &self.wrapped).expect("unwraps");
1030 Store::open_beside(
1031 self.dir.path(),
1032 TENANT,
1033 tenant_key,
1034 self.fs.clone(),
1035 self.clock.clone(),
1036 )
1037 .map_err(StoreError::from)
1038 }
1039
1040 pub(super) fn reopen(&mut self) {
1042 let placeholder = self.open().expect("opens");
1043 drop(std::mem::replace(&mut self.store, placeholder));
1044 self.store = self.open().expect("reopens");
1045 }
1046 }
1047
1048 pub(super) fn fixture() -> Fixture {
1049 let dir = tempfile::tempdir().expect("temp dir");
1050 let fs = Arc::new(CrashFs::new(dir.path()));
1051 let clock = Arc::new(ManualClock::at(datetime!(2026-10-23 12:00 UTC)));
1052 let master = Key::generate().expect("master key");
1053 let tenant_key = Key::generate().expect("tenant key");
1054 let wrapped = envelope::wrap(&master, &Context::Tenant { tenant: TENANT }, &tenant_key);
1055 let store =
1056 Store::open(dir.path(), TENANT, tenant_key, fs.clone(), clock.clone()).expect("opens");
1057 let caller = Caller {
1058 key: Key::generate().expect("caller key").id(),
1059 };
1060 Fixture {
1061 dir,
1062 fs,
1063 clock,
1064 master,
1065 wrapped,
1066 store,
1067 caller,
1068 }
1069 }
1070
1071 fn audit_log(store: &Store) -> Vec<Value> {
1072 exported(&store.conn(), None)
1073 }
1074
1075 fn count(store: &Store, sql: &str) -> i64 {
1076 store.conn().query_row(sql, [], |r| r.get(0)).expect(sql)
1077 }
1078
1079 fn wraps(store: &Store) -> i64 {
1080 store
1081 .conn()
1082 .query_row("SELECT coalesce(sum(wraps), 0) FROM key_use", [], |r| {
1083 r.get(0)
1084 })
1085 .expect("wraps")
1086 }
1087
1088 fn stored_body(f: &Fixture, uuid: Uuid, version: VersionId) -> Vec<u8> {
1090 let wrapped: Vec<u8> = f
1091 .store
1092 .conn()
1093 .query_row(
1094 "SELECT wrapped_key FROM version WHERE version = ?1",
1095 [version.0],
1096 |r| r.get(0),
1097 )
1098 .expect("row");
1099 let ctx = Context::Version {
1100 tenant: f.store.tenant,
1101 uuid,
1102 version,
1103 };
1104 let data_key = envelope::unwrap(&f.store.tenant_key, &ctx, &wrapped).expect("unwraps");
1105 let blob =
1106 f.fs.read(&f.store.objects.join(version.to_string()))
1107 .expect("file");
1108 envelope::open(&data_key, &ctx, &blob).expect("opens")
1109 }
1110
1111 fn audit_page(f: &Fixture, after: i64, limit: u32) -> Result<Vec<u8>, StoreError> {
1112 f.store
1113 .audit_page(&f.caller, &audit::AuditQuery { after, limit })
1114 }
1115
1116 fn export_text(store: &Store, since: Option<i64>) -> Vec<u8> {
1117 let mut out = Vec::new();
1118 store.export(since, &mut out).expect("export");
1119 out
1120 }
1121
1122 #[test]
1123 fn an_audit_page_is_export_s_lines_and_records_its_read_after_them() {
1124 let f = fixture();
1125 for n in 0..3 {
1126 let (found, body) = vcon(&format!("01a07da8-c2bb-83e5-b9a2-27900000000{n}"), "x");
1127 f.store.create(&f.caller, &found, &body).expect("stored");
1128 }
1129 let before = export_text(&f.store, None);
1130 assert_eq!(audit_page(&f, 0, 1000), Ok(before.clone()));
1131 let read = audit_log(&f.store);
1132 assert_eq!(read.len(), 4, "three creates and the read");
1133 assert_eq!(read[3]["action"], "audit_read");
1134 assert_eq!(read[3]["actor"], f.caller.key.to_string());
1135 assert_eq!(read[3]["status"], 200);
1136 assert_eq!(read[3]["seq"], 4);
1137 assert_eq!(read[3]["target"], Value::Null);
1138 let lines: Vec<&[u8]> = before.split_inclusive(|b| *b == b'\n').collect();
1140 assert_eq!(audit_page(&f, 1, 2), Ok(lines[1..3].concat()));
1141 let tail = audit_page(&f, 4, 1000).expect("a page");
1143 assert_eq!(tail, export_text(&f.store, Some(4))[..tail.len()]);
1144 assert_eq!(String::from_utf8(tail).expect("UTF-8").lines().count(), 1);
1145 }
1146
1147 #[test]
1150 fn an_audit_page_that_cannot_be_read_is_a_failure_and_recorded() {
1151 let f = fixture();
1152 f.store
1153 .conn_for_test()
1154 .execute(
1155 "INSERT INTO audit (at, actor, action, status, count)
1156 VALUES ('2026-10-23T12:00:00Z', 'cli', 'read', 70000, 1)",
1157 [],
1158 )
1159 .expect("damaged");
1160 assert_eq!(audit_page(&f, 0, 10), Err(StoreError::Io));
1161 let mut recorded = Vec::new();
1162 f.store.export(Some(1), &mut recorded).expect("export");
1163 let recorded: Value = serde_json::from_slice(&recorded).expect("one record");
1164 assert_eq!(recorded["action"], "audit_read");
1165 assert_eq!(recorded["status"], 500);
1166 }
1167
1168 #[test]
1169 fn an_audit_page_whose_record_fails_is_refused() {
1170 let f = fixture();
1171 f.store.break_audit();
1172 assert_eq!(audit_page(&f, 0, 10), Err(StoreError::AuditFailed));
1173 }
1174
1175 #[test]
1176 fn a_new_vcon_is_written_in_order_and_recorded() {
1177 let f = fixture();
1178 let (found, body) = vcon(UUID, "first");
1179 let ops = f.fs.log().len();
1180 let Ok(Created::New(version)) = f.store.create(&f.caller, &found, &body) else {
1181 panic!("a new vCon")
1182 };
1183 let (tmp, done) = (
1184 f.store.objects.join(format!("{version}.tmp")),
1185 f.store.objects.join(version.to_string()),
1186 );
1187 assert_eq!(
1188 f.fs.log()[ops..],
1189 [
1190 Op::Write(tmp.clone()),
1191 Op::SyncFile(tmp.clone()),
1192 Op::Rename(tmp, done),
1193 Op::SyncDir(f.store.objects.clone()),
1194 ]
1195 );
1196 assert_eq!(stored_body(&f, found.uuid, version), body);
1197 assert_eq!(
1198 count(
1199 &f.store,
1200 "SELECT count(*) FROM version WHERE hidden_at IS NULL"
1201 ),
1202 1
1203 );
1204 let current: Vec<u8> = f
1205 .store
1206 .conn()
1207 .query_row(
1208 "SELECT current FROM vcon WHERE uuid = ?1",
1209 [found.uuid.as_bytes()],
1210 |r| r.get(0),
1211 )
1212 .expect("vcon row");
1213 assert_eq!(current, version.0);
1214 assert_eq!(wraps(&f.store), 1, "one data key wrapped");
1215 let log = audit_log(&f.store);
1216 assert_eq!(log.len(), 1);
1217 assert_eq!(log[0]["action"], "create");
1218 assert_eq!(log[0]["actor"], f.caller.key.to_string());
1219 assert_eq!(log[0]["target"], UUID);
1220 assert_eq!(log[0]["version"], version.to_string());
1221 assert_eq!(log[0]["status"], 201);
1222 }
1223
1224 #[test]
1225 fn the_same_body_again_is_same_and_writes_nothing() {
1226 let f = fixture();
1227 let (found, body) = vcon(UUID, "first");
1228 let Ok(Created::New(version)) = f.store.create(&f.caller, &found, &body) else {
1229 panic!("a new vCon")
1230 };
1231 let ops = f.fs.log().len();
1232 assert_eq!(
1233 f.store.create(&f.caller, &found, &body),
1234 Ok(Created::Same(version))
1235 );
1236 assert_eq!(f.fs.log().len(), ops, "no file written");
1237 assert_eq!(count(&f.store, "SELECT count(*) FROM version"), 1);
1238 assert_eq!(wraps(&f.store), 1, "nothing wrapped");
1239 let log = audit_log(&f.store);
1240 assert_eq!(
1241 (log[1]["status"].clone(), log[1]["version"].clone()),
1242 (200.into(), version.to_string().into())
1243 );
1244 }
1245
1246 #[test]
1247 fn another_body_for_a_stored_uuid_is_a_conflict_and_writes_nothing() {
1248 let f = fixture();
1249 let (found, body) = vcon(UUID, "first");
1250 f.store.create(&f.caller, &found, &body).expect("stored");
1251 let ops = f.fs.log().len();
1252 let (found, other) = vcon(UUID, "other");
1254 assert_eq!(
1255 f.store.create(&f.caller, &found, &other),
1256 Err(StoreError::Conflict)
1257 );
1258 assert_eq!(f.fs.log().len(), ops, "no file written");
1259 assert_eq!(wraps(&f.store), 1, "nothing wrapped");
1260 assert_eq!(audit_log(&f.store)[1]["status"], 409);
1261 }
1262
1263 #[test]
1264 fn a_deleted_or_purged_uuid_is_gone_and_writes_nothing() {
1265 for sql in [
1266 "INSERT INTO vcon (uuid, current, deleted_at) VALUES (?1, NULL, 1)",
1267 "INSERT INTO tombstone (uuid, purged_at) VALUES (?1, 1)",
1268 ] {
1269 let f = fixture();
1270 let (found, body) = vcon(UUID, "first");
1271 f.store
1272 .conn()
1273 .execute(sql, [found.uuid.as_bytes()])
1274 .expect(sql);
1275 let ops = f.fs.log().len();
1276 assert_eq!(
1277 f.store.create(&f.caller, &found, &body),
1278 Err(StoreError::Gone),
1279 "{sql}"
1280 );
1281 assert_eq!(f.fs.log().len(), ops, "no file written");
1282 assert_eq!(wraps(&f.store), 0, "nothing wrapped");
1283 assert_eq!(audit_log(&f.store)[0]["status"], 410);
1284 }
1285 }
1286
1287 #[test]
1288 fn counts_include_hidden_versions_and_held_uuids() {
1289 let f = fixture();
1290 let none = Counts {
1291 versions: 0,
1292 held: 0,
1293 };
1294 assert_eq!(f.store.counts(), Ok(none));
1295 let (found, old) = stored(&f, "first");
1296 let (_, newer) = vcon(UUID, "second");
1297 f.store
1298 .replace(&f.caller, old, &found, &newer)
1299 .expect("replaced");
1300 f.store
1301 .delete(&f.caller, found.uuid, Retention::Years(7))
1302 .expect("deleted");
1303 let (other, body) = vcon("01a07da8-c2bb-83e5-b9a2-279e0d16bc47", "other");
1304 f.store.create(&f.caller, &other, &body).expect("stored");
1305 assert_eq!(
1306 f.store.counts(),
1307 Ok(Counts {
1308 versions: 3,
1309 held: 0
1310 }),
1311 "two hidden versions and a live one"
1312 );
1313 f.store
1314 .conn()
1315 .execute(
1316 "UPDATE vcon SET hold = 1 WHERE uuid = ?1",
1317 [found.uuid.as_bytes()],
1318 )
1319 .expect("a hold");
1320 assert_eq!(f.store.counts().map(|c| c.held), Ok(1));
1321 }
1322
1323 #[test]
1324 fn a_negative_wrap_count_is_damage_and_stores_nothing() {
1325 let f = fixture();
1326 f.store
1327 .conn()
1328 .execute(
1329 "INSERT INTO key_use (tenant_key_id, wraps) VALUES (?1, -1)",
1330 [f.store.tenant_key.id().bytes()],
1331 )
1332 .expect("a damaged count");
1333 let (found, body) = vcon(UUID, "first");
1334 let ops = f.fs.log().len();
1335 assert_eq!(
1336 f.store.create(&f.caller, &found, &body),
1337 Err(StoreError::Io)
1338 );
1339 assert_eq!(f.fs.log().len(), ops, "no file written");
1340 }
1341
1342 #[test]
1343 fn a_tenant_key_at_its_wrap_limit_refuses_new_vcons() {
1344 let f = fixture();
1345 f.store
1346 .conn()
1347 .execute(
1348 "INSERT INTO key_use (tenant_key_id, wraps) VALUES (?1, ?2)",
1349 params![f.store.tenant_key.id().bytes(), 1_i64 << 32],
1350 )
1351 .expect("at the limit");
1352 let (found, body) = vcon(UUID, "first");
1353 let ops = f.fs.log().len();
1354 assert_eq!(
1355 f.store.create(&f.caller, &found, &body),
1356 Err(StoreError::LimitReached)
1357 );
1358 assert_eq!(f.fs.log().len(), ops, "no file written");
1359 assert_eq!(audit_log(&f.store)[0]["status"], 503);
1360 }
1361
1362 #[test]
1363 fn a_missing_current_file_is_damage_not_a_conflict() {
1364 let f = fixture();
1365 let (found, body) = vcon(UUID, "first");
1366 let Ok(Created::New(version)) = f.store.create(&f.caller, &found, &body) else {
1367 panic!("a new vCon")
1368 };
1369 f.fs.remove(&f.store.objects.join(version.to_string()))
1370 .expect("remove");
1371 assert_eq!(
1372 f.store.create(&f.caller, &found, &body),
1373 Err(StoreError::Damaged)
1374 );
1375 assert_eq!(audit_log(&f.store)[1]["status"], 500);
1376 }
1377
1378 #[test]
1379 fn a_racing_create_that_loses_gets_a_conflict_and_leaves_no_file() {
1380 let f = Arc::new(fixture());
1381 let (found, body) = vcon(UUID, "first");
1382 let winner = f.clone();
1383 *f.store.hooks.between.lock().expect("hook") = Some(Box::new(move || {
1384 let (found, other) = vcon(UUID, "second");
1385 assert!(matches!(
1386 winner.store.create(&winner.caller, &found, &other),
1387 Ok(Created::New(_))
1388 ));
1389 }));
1390 assert_eq!(
1391 f.store.create(&f.caller, &found, &body),
1392 Err(StoreError::Conflict)
1393 );
1394 assert_eq!(
1395 count(&f.store, "SELECT count(*) FROM version"),
1396 1,
1397 "only the winner"
1398 );
1399 assert_eq!(
1400 f.fs.list(&f.store.objects).expect("list").len(),
1401 1,
1402 "the loser's file is gone"
1403 );
1404 assert_eq!(wraps(&f.store), 2, "both wraps are counted");
1405 let statuses: Vec<_> = audit_log(&f.store)
1406 .iter()
1407 .map(|r| r["status"].clone())
1408 .collect();
1409 assert_eq!(statuses, [201, 409]);
1410 }
1411
1412 #[test]
1413 fn a_create_whose_audit_record_fails_leaves_no_row_and_no_file() {
1414 let f = fixture();
1415 f.store
1417 .conn()
1418 .execute_batch(
1419 "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
1420 BEGIN SELECT RAISE(ABORT, 'audit fails'); END;",
1421 )
1422 .expect("trigger");
1423 let (found, body) = vcon(UUID, "first");
1424 assert_eq!(
1425 f.store.create(&f.caller, &found, &body),
1426 Err(StoreError::AuditFailed)
1427 );
1428 assert_eq!(count(&f.store, "SELECT count(*) FROM version"), 0);
1429 assert_eq!(count(&f.store, "SELECT count(*) FROM vcon"), 0);
1430 assert_eq!(
1431 f.fs.list(&f.store.objects).expect("list"),
1432 Vec::<std::ffi::OsString>::new()
1433 );
1434 }
1435
1436 fn get_entry_status(f: &Fixture) -> Value {
1437 audit_log(&f.store).last().expect("a record")["status"].clone()
1438 }
1439
1440 #[test]
1441 fn a_stored_vcon_reads_back_byte_for_byte_and_is_recorded() {
1442 let f = fixture();
1443 let (found, body) = vcon(UUID, "first");
1444 let Ok(Created::New(version)) = f.store.create(&f.caller, &found, &body) else {
1445 panic!("a new vCon")
1446 };
1447 let stored = f.store.get(&f.caller, found.uuid).expect("reads");
1448 assert_eq!(
1449 stored,
1450 Stored {
1451 version,
1452 form: vcon::Form::Unsigned,
1453 gzip: false,
1454 body
1455 }
1456 );
1457 let log = audit_log(&f.store);
1458 let read = log.last().expect("a record");
1459 assert_eq!(read["action"], "read");
1460 assert_eq!(read["status"], 200);
1461 assert_eq!(read["version"], version.to_string());
1462 assert_eq!(read["target"], UUID);
1463 }
1464
1465 #[test]
1466 fn a_uuid_never_stored_is_not_found() {
1467 let f = fixture();
1468 let (found, _) = vcon(UUID, "first");
1469 assert_eq!(
1470 f.store.get(&f.caller, found.uuid),
1471 Err(StoreError::NotFound)
1472 );
1473 assert_eq!(get_entry_status(&f), 404);
1474 }
1475
1476 #[test]
1477 fn a_deleted_or_purged_uuid_is_gone() {
1478 for sql in [
1479 "UPDATE vcon SET current = NULL, deleted_at = 1 WHERE uuid = ?1",
1480 "DELETE FROM vcon WHERE uuid = ?1; INSERT INTO tombstone (uuid, purged_at) VALUES (?1, 1)",
1481 ] {
1482 let f = fixture();
1483 let (found, body) = vcon(UUID, "first");
1484 f.store.create(&f.caller, &found, &body).expect("stored");
1485 for statement in sql.split("; ") {
1486 f.store
1487 .conn()
1488 .execute(statement, [found.uuid.as_bytes()])
1489 .expect(statement);
1490 }
1491 assert_eq!(
1492 f.store.get(&f.caller, found.uuid),
1493 Err(StoreError::Gone),
1494 "{sql}"
1495 );
1496 assert_eq!(get_entry_status(&f), 410);
1497 }
1498 }
1499
1500 #[test]
1501 fn a_missing_or_damaged_file_is_a_500_never_an_empty_body() {
1502 let f = fixture();
1503 let (found, body) = vcon(UUID, "first");
1504 let Ok(Created::New(version)) = f.store.create(&f.caller, &found, &body) else {
1505 panic!("a new vCon")
1506 };
1507 let file = f.store.objects.join(version.to_string());
1508 let blob = f.fs.read(&file).expect("file");
1509 f.fs.remove(&file).expect("remove");
1510 assert_eq!(
1511 f.store.get(&f.caller, found.uuid),
1512 Err(StoreError::Damaged),
1513 "missing"
1514 );
1515 assert_eq!(get_entry_status(&f), 500);
1516 let mut bad = blob.clone();
1517 let last = bad.len() - 1;
1518 bad[last] ^= 1;
1519 f.fs.write_new(&file, &bad).expect("write");
1520 assert_eq!(
1521 f.store.get(&f.caller, found.uuid),
1522 Err(StoreError::Damaged),
1523 "tampered"
1524 );
1525 }
1526
1527 #[test]
1528 fn a_read_racing_a_delete_never_returns_the_body() {
1529 let f = Arc::new(fixture());
1530 let (found, body) = vcon(UUID, "first");
1531 f.store.create(&f.caller, &found, &body).expect("stored");
1532 let deleter = f.clone();
1533 *f.store.hooks.between.lock().expect("hook") = Some(Box::new(move || {
1534 deleter
1535 .store
1536 .conn()
1537 .execute(
1538 "UPDATE vcon SET current = NULL, deleted_at = 1 WHERE uuid = ?1",
1539 [found.uuid.as_bytes()],
1540 )
1541 .expect("delete");
1542 }));
1543 assert_eq!(f.store.get(&f.caller, found.uuid), Err(StoreError::Gone));
1544 assert_eq!(get_entry_status(&f), 410);
1545 }
1546
1547 #[test]
1548 fn a_read_racing_a_new_version_starts_again_and_returns_the_new_one() {
1549 let f = Arc::new(fixture());
1550 let (found, body) = vcon(UUID, "first");
1551 f.store.create(&f.caller, &found, &body).expect("stored");
1552 let (_, newer) = vcon(UUID, "newer");
1553 let (version, wrapped, _) = f.store.write_version(found.uuid, &newer).expect("written");
1554 let writer = f.clone();
1555 *f.store.hooks.between.lock().expect("hook") = Some(Box::new(move || {
1556 let conn = writer.store.conn();
1557 conn.execute(
1558 "INSERT INTO version
1559 (version, uuid, form, gzip, size, stored_at, tenant_key_id, wrapped_key)
1560 VALUES (?1, ?2, 'unsigned', 0, 0, 0, ?3, ?4)",
1561 params![
1562 version.0,
1563 found.uuid.as_bytes(),
1564 writer.store.tenant_key.id().bytes(),
1565 wrapped
1566 ],
1567 )
1568 .expect("new version");
1569 conn.execute(
1570 "UPDATE vcon SET current = ?1 WHERE uuid = ?2",
1571 params![version.0, found.uuid.as_bytes()],
1572 )
1573 .expect("made current");
1574 }));
1575 let stored = f.store.get(&f.caller, found.uuid).expect("reads");
1576 assert_eq!((stored.version, stored.body), (version, newer));
1577 let reads = audit_log(&f.store)
1578 .iter()
1579 .filter(|r| r["action"] == "read")
1580 .count();
1581 assert_eq!(reads, 1, "one record for the one request");
1582 }
1583
1584 #[test]
1585 fn every_form_and_the_gzip_flag_read_back_as_stored() {
1586 use base64::Engine;
1587 use std::io::Write;
1588 let f = fixture();
1589 let uuids = [
1590 "01a07da8-c2bb-83e5-b9a2-279e0d16bc01",
1591 "01a07da8-c2bb-83e5-b9a2-279e0d16bc02",
1592 "01a07da8-c2bb-83e5-b9a2-279e0d16bc03",
1593 ];
1594 let (_, unsigned) = vcon(uuids[0], "plain");
1595 let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
1596 gz.write_all(&unsigned).expect("gzip");
1597 let gzipped = gz.finish().expect("gzip");
1598 let (_, inner) = vcon(uuids[1], "signed");
1599 let signed = format!(
1600 r#"{{"payload": "{}", "signatures": [{{"header": {{}}}}]}}"#,
1601 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(inner)
1602 )
1603 .into_bytes();
1604 let encrypted = format!(
1605 r#"{{"unprotected": {{"uuid": "{}"}}, "ciphertext": "x", "recipients": []}}"#,
1606 uuids[2]
1607 )
1608 .into_bytes();
1609 let cases = [
1610 (gzipped, vcon::Form::Unsigned, true),
1611 (signed, vcon::Form::Signed, false),
1612 (encrypted, vcon::Form::Encrypted, false),
1613 ];
1614 for (body, form, gzip) in cases {
1615 let found = inspect(&body, None, &Limits::new(1 << 20), &BTreeSet::new())
1616 .expect("a valid vCon");
1617 assert_eq!((found.form, found.gzip), (form, gzip));
1618 f.store.create(&f.caller, &found, &body).expect("stored");
1619 let stored = f.store.get(&f.caller, found.uuid).expect("reads");
1620 assert_eq!((stored.form, stored.gzip, stored.body), (form, gzip, body));
1621 }
1622 }
1623
1624 fn stored(f: &Fixture, subject: &str) -> (Inspection, VersionId) {
1626 let (found, body) = vcon(UUID, subject);
1627 let Ok(Created::New(version)) = f.store.create(&f.caller, &found, &body) else {
1628 panic!("a new vCon")
1629 };
1630 (found, version)
1631 }
1632
1633 #[test]
1634 fn a_replace_makes_a_new_current_version_and_hides_the_old_one() {
1635 let f = fixture();
1636 let (found, old) = stored(&f, "first");
1637 let (_, newer) = vcon(UUID, "second");
1638 let version = f
1639 .store
1640 .replace(&f.caller, old, &found, &newer)
1641 .expect("replaced");
1642 assert_ne!(version, old);
1643 let stored = f.store.get(&f.caller, found.uuid).expect("reads");
1644 assert_eq!((stored.version, stored.body), (version, newer));
1645 let hidden: Option<i64> = f
1646 .store
1647 .conn()
1648 .query_row(
1649 "SELECT hidden_at FROM version WHERE version = ?1",
1650 [old.0],
1651 |r| r.get(0),
1652 )
1653 .expect("the old row is kept");
1654 assert!(hidden.is_some(), "the old version is hidden");
1655 assert!(
1656 f.fs.read(&f.store.objects.join(old.to_string())).is_ok(),
1657 "its file is kept"
1658 );
1659 let record = audit_log(&f.store)
1660 .into_iter()
1661 .find(|r| r["action"] == "replace")
1662 .expect("a replace record");
1663 assert_eq!(
1664 (record["status"].clone(), record["version"].clone()),
1665 (200.into(), version.to_string().into())
1666 );
1667 }
1668
1669 #[test]
1670 fn a_replace_of_a_missing_or_gone_uuid_is_refused() {
1671 let f = fixture();
1672 let (found, body) = vcon(UUID, "first");
1673 let any = VersionId([7; 16]);
1674 assert_eq!(
1675 f.store.replace(&f.caller, any, &found, &body),
1676 Err(StoreError::NotFound)
1677 );
1678 assert_eq!(get_entry_status(&f), 404);
1679 let (found, old) = stored(&f, "first");
1680 f.store
1681 .conn()
1682 .execute(
1683 "UPDATE vcon SET current = NULL, deleted_at = 1 WHERE uuid = ?1",
1684 [found.uuid.as_bytes()],
1685 )
1686 .expect("delete");
1687 assert_eq!(
1688 f.store.replace(&f.caller, old, &found, &body),
1689 Err(StoreError::Gone)
1690 );
1691 assert_eq!(get_entry_status(&f), 410);
1692 }
1693
1694 #[test]
1695 fn a_stale_if_match_is_refused_and_writes_nothing() {
1696 let f = fixture();
1697 let (found, _) = stored(&f, "first");
1698 let ops = f.fs.log().len();
1699 let (_, newer) = vcon(UUID, "second");
1700 assert_eq!(
1701 f.store
1702 .replace(&f.caller, VersionId([7; 16]), &found, &newer),
1703 Err(StoreError::PreconditionFailed)
1704 );
1705 assert_eq!(f.fs.log().len(), ops, "no file written");
1706 assert_eq!(wraps(&f.store), 1, "nothing wrapped");
1707 assert_eq!(get_entry_status(&f), 412);
1708 }
1709
1710 #[test]
1711 fn a_signed_or_encrypted_current_version_is_immutable() {
1712 use base64::Engine;
1713 let f = fixture();
1714 let (found, old) = stored(&f, "first");
1715 let (_, inner) = vcon(UUID, "signed");
1716 let signed = format!(
1717 r#"{{"payload": "{}", "signatures": [{{"header": {{}}}}]}}"#,
1718 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(inner)
1719 )
1720 .into_bytes();
1721 let signed_found =
1722 inspect(&signed, None, &Limits::new(1 << 20), &BTreeSet::new()).expect("a signed vCon");
1723 let version = f
1724 .store
1725 .replace(&f.caller, old, &signed_found, &signed)
1726 .expect("an unsigned vCon may be replaced by a signed one");
1727 let (_, newer) = vcon(UUID, "second");
1728 let ops = f.fs.log().len();
1729 assert_eq!(
1730 f.store.replace(&f.caller, version, &found, &newer),
1731 Err(StoreError::Immutable)
1732 );
1733 assert_eq!(f.fs.log().len(), ops, "no file written");
1734 assert_eq!(get_entry_status(&f), 409);
1735
1736 let g = fixture();
1737 let encrypted = format!(
1738 r#"{{"unprotected": {{"uuid": "{UUID}"}}, "ciphertext": "x", "recipients": []}}"#
1739 )
1740 .into_bytes();
1741 let enc_found = inspect(&encrypted, None, &Limits::new(1 << 20), &BTreeSet::new())
1742 .expect("an encrypted vCon");
1743 let Ok(Created::New(enc_version)) = g.store.create(&g.caller, &enc_found, &encrypted)
1744 else {
1745 panic!("stored")
1746 };
1747 assert_eq!(
1748 g.store.replace(&g.caller, enc_version, &found, &newer),
1749 Err(StoreError::Immutable)
1750 );
1751 }
1752
1753 #[test]
1754 fn a_replace_at_the_wrap_limit_is_refused() {
1755 let f = fixture();
1756 let (found, old) = stored(&f, "first");
1757 f.store
1758 .conn()
1759 .execute("UPDATE key_use SET wraps = ?1", [1_i64 << 32])
1760 .expect("at the limit");
1761 let (_, newer) = vcon(UUID, "second");
1762 assert_eq!(
1763 f.store.replace(&f.caller, old, &found, &newer),
1764 Err(StoreError::LimitReached)
1765 );
1766 assert_eq!(get_entry_status(&f), 503);
1767 }
1768
1769 #[test]
1770 fn of_two_replaces_racing_on_one_version_the_second_to_commit_loses() {
1771 let f = Arc::new(fixture());
1772 let (found, old) = stored(&f, "first");
1773 let winner = f.clone();
1774 let racer = found;
1775 *f.store.hooks.between.lock().expect("hook") = Some(Box::new(move || {
1776 let (_, body) = vcon(UUID, "winner");
1777 winner
1778 .store
1779 .replace(&winner.caller, old, &racer, &body)
1780 .expect("wins");
1781 }));
1782 let (_, body) = vcon(UUID, "loser");
1783 assert_eq!(
1784 f.store.replace(&f.caller, old, &found, &body),
1785 Err(StoreError::PreconditionFailed)
1786 );
1787 assert_eq!(
1788 f.fs.list(&f.store.objects).expect("list").len(),
1789 2,
1790 "first and winner only"
1791 );
1792 assert_eq!(wraps(&f.store), 3, "every wrap counted");
1793 assert_eq!(get_entry_status(&f), 412);
1794 }
1795
1796 #[test]
1797 fn two_threads_replacing_with_the_same_if_match_one_wins() {
1798 let f = Arc::new(fixture());
1799 let (_, old) = stored(&f, "first");
1800 let threads: Vec<_> = ["one", "two"]
1801 .into_iter()
1802 .map(|subject| {
1803 let f = f.clone();
1804 std::thread::spawn(move || {
1805 let (found, body) = vcon(UUID, subject);
1806 f.store.replace(&f.caller, old, &found, &body)
1807 })
1808 })
1809 .collect();
1810 let results: Vec<_> = threads
1811 .into_iter()
1812 .map(|t| t.join().expect("thread"))
1813 .collect();
1814 assert_eq!(
1815 results.iter().filter(|r| r.is_ok()).count(),
1816 1,
1817 "{results:?}"
1818 );
1819 assert!(
1820 results.contains(&Err(StoreError::PreconditionFailed)),
1821 "{results:?}"
1822 );
1823 }
1824
1825 #[test]
1826 fn a_replace_whose_audit_record_fails_changes_nothing() {
1827 let f = fixture();
1828 let (found, old) = stored(&f, "first");
1829 f.store
1830 .conn()
1831 .execute_batch(
1832 "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
1833 BEGIN SELECT RAISE(ABORT, 'audit fails'); END;",
1834 )
1835 .expect("trigger");
1836 let (_, newer) = vcon(UUID, "second");
1837 assert_eq!(
1838 f.store.replace(&f.caller, old, &found, &newer),
1839 Err(StoreError::AuditFailed)
1840 );
1841 f.store
1842 .conn()
1843 .execute_batch("DROP TRIGGER no_audit")
1844 .expect("drop trigger");
1845 assert_eq!(
1846 f.store.get(&f.caller, found.uuid).expect("reads").version,
1847 old
1848 );
1849 assert_eq!(
1850 f.fs.list(&f.store.objects).expect("list").len(),
1851 1,
1852 "only the first file"
1853 );
1854 }
1855
1856 fn hidden_versions(f: &Fixture) -> i64 {
1857 count(
1858 &f.store,
1859 "SELECT count(*) FROM version WHERE hidden_at IS NOT NULL",
1860 )
1861 }
1862
1863 #[test]
1864 fn a_deleted_uuid_answers_gone_and_keeps_its_versions_hidden() {
1865 let f = fixture();
1866 let (found, old) = stored(&f, "first");
1867 let (_, newer) = vcon(UUID, "second");
1868 f.clock.set(datetime!(2026-10-23 13:00 UTC));
1869 let current = f
1870 .store
1871 .replace(&f.caller, old, &found, &newer)
1872 .expect("replaced");
1873 f.clock.set(datetime!(2026-10-23 14:00 UTC));
1874 assert_eq!(
1875 f.store.delete(&f.caller, found.uuid, Retention::Years(7)),
1876 Ok(())
1877 );
1878 let record = audit_log(&f.store).pop().expect("a record");
1879 assert_eq!(
1880 (record["action"].clone(), record["status"].clone()),
1881 ("delete".into(), 204.into())
1882 );
1883 let hidden_at = |version: VersionId| -> i64 {
1884 f.store
1885 .conn()
1886 .query_row(
1887 "SELECT hidden_at FROM version WHERE version = ?1",
1888 [version.0],
1889 |r| r.get(0),
1890 )
1891 .expect("hidden")
1892 };
1893 assert_eq!(
1894 hidden_at(old),
1895 nanos(datetime!(2026-10-23 13:00 UTC)),
1896 "hidden by the replace"
1897 );
1898 assert_eq!(
1899 hidden_at(current),
1900 nanos(datetime!(2026-10-23 14:00 UTC)),
1901 "hidden by the delete"
1902 );
1903 assert_eq!(f.store.get(&f.caller, found.uuid), Err(StoreError::Gone));
1904 let (_, body) = vcon(UUID, "third");
1905 assert_eq!(
1906 f.store.create(&f.caller, &found, &body),
1907 Err(StoreError::Gone)
1908 );
1909 assert_eq!(hidden_versions(&f), 2, "both versions hidden");
1910 assert_eq!(
1911 f.fs.list(&f.store.objects).expect("list").len(),
1912 2,
1913 "files kept for legal reads"
1914 );
1915 let (current, deleted_at): (Option<Vec<u8>>, Option<i64>) = f
1916 .store
1917 .conn()
1918 .query_row(
1919 "SELECT current, deleted_at FROM vcon WHERE uuid = ?1",
1920 [found.uuid.as_bytes()],
1921 |r| Ok((r.get(0)?, r.get(1)?)),
1922 )
1923 .expect("row");
1924 assert_eq!(current, None);
1925 assert_eq!(deleted_at, Some(nanos(datetime!(2026-10-23 14:00 UTC))));
1926 }
1927
1928 #[test]
1929 fn deleting_a_deleted_or_purged_uuid_succeeds_again() {
1930 let f = fixture();
1931 let (found, _) = stored(&f, "first");
1932 f.store
1933 .delete(&f.caller, found.uuid, Retention::Years(7))
1934 .expect("deleted");
1935 assert_eq!(
1936 f.store.delete(&f.caller, found.uuid, Retention::Years(7)),
1937 Ok(())
1938 );
1939 assert_eq!(get_entry_status(&f), 204);
1940 let g = fixture();
1941 g.store
1942 .conn()
1943 .execute(
1944 "INSERT INTO tombstone (uuid, purged_at) VALUES (?1, 1)",
1945 [found.uuid.as_bytes()],
1946 )
1947 .expect("purged");
1948 assert_eq!(
1949 g.store.delete(&g.caller, found.uuid, Retention::Years(7)),
1950 Ok(())
1951 );
1952 assert_eq!(get_entry_status(&g), 204);
1953 }
1954
1955 #[test]
1956 fn deleting_a_uuid_never_stored_is_not_found() {
1957 let f = fixture();
1958 let (found, _) = vcon(UUID, "first");
1959 assert_eq!(
1960 f.store.delete(&f.caller, found.uuid, Retention::Years(7)),
1961 Err(StoreError::NotFound)
1962 );
1963 assert_eq!(get_entry_status(&f), 404);
1964 }
1965
1966 #[test]
1967 fn a_hold_does_not_stop_a_delete() {
1968 let f = fixture();
1969 let (found, _) = stored(&f, "first");
1970 f.store
1971 .conn()
1972 .execute(
1973 "UPDATE vcon SET hold = 1 WHERE uuid = ?1",
1974 [found.uuid.as_bytes()],
1975 )
1976 .expect("hold");
1977 assert_eq!(
1978 f.store.delete(&f.caller, found.uuid, Retention::Years(7)),
1979 Ok(())
1980 );
1981 assert_eq!(f.store.get(&f.caller, found.uuid), Err(StoreError::Gone));
1982 }
1983
1984 #[test]
1985 fn a_delete_whose_audit_record_fails_changes_nothing() {
1986 let f = fixture();
1987 let (found, version) = stored(&f, "first");
1988 f.store
1989 .conn()
1990 .execute_batch(
1991 "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
1992 BEGIN SELECT RAISE(ABORT, 'audit fails'); END;",
1993 )
1994 .expect("trigger");
1995 assert_eq!(
1996 f.store.delete(&f.caller, found.uuid, Retention::Years(7)),
1997 Err(StoreError::AuditFailed)
1998 );
1999 f.store
2000 .conn()
2001 .execute_batch("DROP TRIGGER no_audit")
2002 .expect("drop trigger");
2003 assert_eq!(
2004 f.store
2005 .get(&f.caller, found.uuid)
2006 .expect("still live")
2007 .version,
2008 version
2009 );
2010 assert_eq!(hidden_versions(&f), 0);
2011 }
2012
2013 #[test]
2014 fn only_the_optional_catalog_columns_may_be_empty() {
2015 let f = fixture();
2016 let conn = f.store.conn();
2017 let tables: [(&str, &[(&str, &str)]); 5] = [
2018 (
2019 "vcon",
2020 &[
2021 ("uuid", "x'01'"),
2022 ("current", "x'02'"),
2023 ("deleted_at", "1"),
2024 ("hold", "0"),
2025 ],
2026 ),
2027 (
2028 "version",
2029 &[
2030 ("version", "x'02'"),
2031 ("uuid", "x'01'"),
2032 ("form", "'unsigned'"),
2033 ("gzip", "0"),
2034 ("size", "1"),
2035 ("stored_at", "1"),
2036 ("hidden_at", "1"),
2037 ("tenant_key_id", "x'03'"),
2038 ("wrapped_key", "x'04'"),
2039 ],
2040 ),
2041 ("tombstone", &[("uuid", "x'01'"), ("purged_at", "1")]),
2042 ("key_use", &[("tenant_key_id", "x'03'"), ("wraps", "1")]),
2043 (
2044 "audit",
2045 &[
2046 ("at", "'2026-10-23T12:00Z'"),
2047 ("actor", "'cli'"),
2048 ("action", "'create'"),
2049 ("target", "'x'"),
2050 ("version", "'y'"),
2051 ("status", "201"),
2052 ("count", "1"),
2053 ],
2054 ),
2055 ];
2056 let may_be_empty: Vec<String> = tables
2057 .iter()
2058 .flat_map(|(table, row)| columns_taking_null(&conn, table, row))
2059 .collect();
2060 assert_eq!(
2061 may_be_empty,
2062 [
2063 "vcon.current",
2064 "vcon.deleted_at",
2065 "version.hidden_at",
2066 "audit.target",
2067 "audit.version"
2068 ]
2069 );
2070 }
2071
2072 #[test]
2073 fn a_new_stores_objects_directory_survives_a_crash() {
2074 let f = fixture();
2075 f.fs.crash();
2076 assert_eq!(
2077 f.fs.list(f.dir.path()).expect("list"),
2078 [std::ffi::OsString::from("objects")]
2079 );
2080 }
2081
2082 #[test]
2083 fn opening_removes_temporary_and_orphaned_files_and_nothing_else() {
2084 let mut f = fixture();
2085 let (found, version) = stored(&f, "first");
2086 let orphan = VersionId([0x0e; 16]);
2087 let objects = f.store.objects.clone();
2088 for name in [
2089 format!("{orphan}"),
2090 format!("{orphan}.tmp"),
2091 format!("{version}.tmp"),
2092 "notes.txt".to_owned(),
2093 ] {
2094 f.fs.write_new(&objects.join(name), b"x").expect("write");
2095 }
2096 f.reopen();
2097 let names: Vec<String> =
2098 f.fs.list(&objects)
2099 .expect("list")
2100 .into_iter()
2101 .map(|n| n.to_string_lossy().into_owned())
2102 .collect();
2103 assert_eq!(names, [version.to_string(), "notes.txt".to_owned()]);
2104 assert_eq!(
2105 f.store
2106 .get(&f.caller, found.uuid)
2107 .expect("still reads")
2108 .version,
2109 version
2110 );
2111 let log = f.fs.log();
2112 let last_removal = log
2113 .iter()
2114 .rposition(|op| matches!(op, Op::Remove(_)))
2115 .expect("files were removed");
2116 assert!(
2117 log[last_removal..].contains(&Op::SyncDir(objects)),
2118 "the removals are synced"
2119 );
2120 }
2121
2122 #[test]
2125 fn opening_beside_a_server_removes_nothing() {
2126 let f = fixture();
2127 let (found, version) = stored(&f, "first");
2128 let orphan = VersionId([0x0e; 16]);
2129 let objects = f.store.objects.clone();
2130 let mut written = vec![version.to_string()];
2131 for name in [
2132 format!("{orphan}"),
2133 format!("{orphan}.tmp"),
2134 format!("{version}.tmp"),
2135 ] {
2136 f.fs.write_new(&objects.join(&name), b"x").expect("write");
2137 written.push(name);
2138 }
2139 written.sort();
2140 let ops = f.fs.log().len();
2141 let beside = f.open_beside().expect("opens beside");
2142 let mut names: Vec<String> =
2143 f.fs.list(&objects)
2144 .expect("list")
2145 .into_iter()
2146 .map(|n| n.to_string_lossy().into_owned())
2147 .collect();
2148 names.sort();
2149 assert_eq!(names, written);
2150 assert!(
2151 !f.fs.log()[ops..]
2152 .iter()
2153 .any(|op| matches!(op, Op::Remove(_))),
2154 "nothing removed"
2155 );
2156 assert_eq!(
2157 beside.get(&f.caller, found.uuid).expect("reads").version,
2158 version
2159 );
2160 }
2161
2162 #[test]
2163 fn opening_checkpoints_the_wal_with_truncation() {
2164 let mut f = fixture();
2165 stored(&f, "first");
2166 let wal = f.dir.path().join("catalog.sqlite-wal");
2167 assert!(
2168 std::fs::metadata(&wal).expect("a WAL").len() > 0,
2169 "the WAL holds the write"
2170 );
2171 f.reopen();
2172 assert_eq!(std::fs::metadata(&wal).expect("a WAL").len(), 0);
2173 }
2174
2175 #[test]
2176 fn check_reports_a_version_whose_file_is_missing() {
2177 let f = fixture();
2178 let (_, version) = stored(&f, "first");
2179 assert_eq!(f.store.check(), Ok(vec![]));
2180 f.fs.remove(&f.store.objects.join(version.to_string()))
2181 .expect("remove");
2182 assert_eq!(f.store.check(), Ok(vec![version]));
2183 }
2184
2185 #[test]
2186 fn opening_refuses_while_a_reader_stops_the_wal_from_being_truncated() {
2187 let f = fixture();
2188 stored(&f, "first");
2189 let ctx = Context::Tenant { tenant: TENANT };
2190 let tenant_key = envelope::unwrap(&f.master, &ctx, &f.wrapped).expect("unwraps");
2191 let catalog_key = envelope::catalog_key(&tenant_key).expect("catalog key");
2192 let mut reader = catalog::open(&f.dir.path().join("catalog.sqlite"), &catalog_key, "")
2193 .expect("a second connection");
2194 let read = reader.transaction().expect("read transaction");
2195 let _: i64 = read
2196 .query_row("SELECT count(*) FROM vcon", [], |r| r.get(0))
2197 .expect("a read holds a snapshot");
2198 assert!(matches!(f.open(), Err(StoreError::Io)));
2199 drop(read);
2200 assert!(f.open().is_ok(), "opens once the reader is done");
2201 }
2202
2203 const WRITE_STEPS: usize = 4;
2206
2207 fn assert_whole(f: &Fixture, step: usize) {
2209 assert_eq!(
2210 f.store.check(),
2211 Ok(vec![]),
2212 "a row without its file at step {step}"
2213 );
2214 for name in f.fs.list(&f.store.objects).expect("list") {
2215 let name = name.to_string_lossy().into_owned();
2216 let version = VersionId::parse(&name)
2217 .unwrap_or_else(|| panic!("{name} left over at step {step}"));
2218 let rows = count(
2219 &f.store,
2220 &format!("SELECT count(*) FROM version WHERE hex(version) = upper('{version}')"),
2221 );
2222 assert_eq!(rows, 1, "{name} has no row at step {step}");
2223 }
2224 }
2225
2226 pub(super) fn block_commits(f: &Fixture) -> Connection {
2229 let ctx = Context::Tenant { tenant: TENANT };
2230 let tenant_key = envelope::unwrap(&f.master, &ctx, &f.wrapped).expect("unwraps");
2231 let catalog_key = envelope::catalog_key(&tenant_key).expect("catalog key");
2232 catalog::open(&f.dir.path().join("catalog.sqlite"), &catalog_key, "")
2233 .expect("second connection")
2234 }
2235
2236 fn die_before_commit(f: &Fixture) {
2237 let other = block_commits(f);
2238 let fs = f.fs.clone();
2239 *f.store.hooks.between.lock().expect("hook") = Some(Box::new(move || {
2240 fs.fail_from(0);
2241 other
2242 .execute_batch(
2243 "CREATE TRIGGER dead BEFORE INSERT ON audit
2244 BEGIN SELECT RAISE(ABORT, 'the process has died'); END;",
2245 )
2246 .expect("trigger");
2247 }));
2248 }
2249
2250 pub(super) fn restart(f: &mut Fixture) {
2251 f.fs.crash();
2252 f.store
2253 .conn()
2254 .execute_batch("DROP TRIGGER IF EXISTS dead")
2255 .expect("drop trigger");
2256 f.reopen();
2257 }
2258
2259 #[test]
2260 fn a_create_cut_off_at_any_step_leaves_no_vcon_or_the_whole_vcon() {
2261 let (found, body) = vcon(UUID, "first");
2262 for step in 0..=WRITE_STEPS {
2263 let mut f = fixture();
2264 f.fs.fail_from(f.fs.log().len() + step);
2265 let result = f.store.create(&f.caller, &found, &body);
2266 assert_eq!(
2267 result.is_ok(),
2268 step == WRITE_STEPS,
2269 "step {step}: {result:?}"
2270 );
2271 restart(&mut f);
2272 match f.store.get(&f.caller, found.uuid) {
2273 Ok(stored) => assert_eq!(stored.body, body, "step {step}"),
2274 Err(StoreError::NotFound) => {
2275 assert!(result.is_err(), "acknowledged, then lost, at step {step}")
2276 }
2277 other => panic!("half a vCon at step {step}: {other:?}"),
2278 }
2279 assert_whole(&f, step);
2280 }
2281 }
2282
2283 #[test]
2284 fn a_create_cut_off_between_its_file_and_its_commit_leaves_nothing() {
2285 let mut f = fixture();
2286 let (found, body) = vcon(UUID, "first");
2287 die_before_commit(&f);
2288 assert!(f.store.create(&f.caller, &found, &body).is_err());
2289 restart(&mut f);
2290 assert_eq!(
2291 f.store.get(&f.caller, found.uuid),
2292 Err(StoreError::NotFound)
2293 );
2294 assert_whole(&f, WRITE_STEPS);
2295 }
2296
2297 #[test]
2298 fn a_replace_cut_off_at_any_step_leaves_the_old_version_or_the_new() {
2299 let (found, old_body) = vcon(UUID, "first");
2300 let (_, new_body) = vcon(UUID, "second");
2301 for step in 0..=WRITE_STEPS {
2302 let mut f = fixture();
2303 let Ok(Created::New(old)) = f.store.create(&f.caller, &found, &old_body) else {
2304 panic!("stored")
2305 };
2306 f.fs.fail_from(f.fs.log().len() + step);
2307 let result = f.store.replace(&f.caller, old, &found, &new_body);
2308 assert_eq!(
2309 result.is_ok(),
2310 step == WRITE_STEPS,
2311 "step {step}: {result:?}"
2312 );
2313 restart(&mut f);
2314 let stored = f
2315 .store
2316 .get(&f.caller, found.uuid)
2317 .unwrap_or_else(|e| panic!("step {step}: {e:?}"));
2318 match result {
2319 Ok(new) => assert_eq!(
2320 (stored.version, stored.body),
2321 (new, new_body.clone()),
2322 "step {step}"
2323 ),
2324 Err(_) => assert_eq!(
2325 (stored.version, stored.body),
2326 (old, old_body.clone()),
2327 "step {step}"
2328 ),
2329 }
2330 assert_whole(&f, step);
2331 }
2332 }
2333
2334 #[test]
2335 fn a_replace_cut_off_between_its_file_and_its_commit_keeps_the_old_version() {
2336 let mut f = fixture();
2337 let (found, old) = stored(&f, "first");
2338 let (_, newer) = vcon(UUID, "second");
2339 die_before_commit(&f);
2340 assert!(f.store.replace(&f.caller, old, &found, &newer).is_err());
2341 restart(&mut f);
2342 assert_eq!(
2343 f.store.get(&f.caller, found.uuid).expect("reads").version,
2344 old
2345 );
2346 assert_eq!(hidden_versions(&f), 0);
2347 assert_whole(&f, WRITE_STEPS);
2348 }
2349
2350 #[test]
2351 fn a_delete_cut_off_before_its_commit_leaves_the_vcon_live() {
2352 let mut f = fixture();
2353 let (found, version) = stored(&f, "first");
2354 let other = block_commits(&f);
2355 other
2356 .execute_batch(
2357 "CREATE TRIGGER dead BEFORE INSERT ON audit
2358 BEGIN SELECT RAISE(ABORT, 'the process has died'); END;",
2359 )
2360 .expect("trigger");
2361 assert!(
2362 f.store
2363 .delete(&f.caller, found.uuid, Retention::Years(7))
2364 .is_err()
2365 );
2366 drop(other);
2367 restart(&mut f);
2368 assert_eq!(
2369 f.store
2370 .get(&f.caller, found.uuid)
2371 .expect("still live")
2372 .version,
2373 version
2374 );
2375 assert_eq!(hidden_versions(&f), 0);
2376 }
2377}