Skip to main content

vcon_server/
store.rs

1//! One tenant's vCons: a sealed object file per kept version and a catalog
2//! row for it. The catalog is the truth: a version exists once its row is
3//! committed, and an object file with no row is garbage.
4
5use 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
33/// The tables of a tenant's catalog, besides its audit log. A key column
34/// says NOT NULL because SQLite lets a PRIMARY KEY other than an INTEGER
35/// one hold NULL.
36const 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/// Who is calling, for the audit record.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct Caller {
70    pub key: KeyId,
71}
72
73/// What `create` did.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum Created {
76    /// Stored as a new vCon.
77    New(VersionId),
78    /// The same bytes were already current; nothing was written.
79    Same(VersionId),
80}
81
82/// A version as `get` returns it.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct Stored {
85    pub version: VersionId,
86    pub form: vcon::Form,
87    /// Whether the body is gzipped, as it arrived.
88    pub gzip: bool,
89    /// The body exactly as it arrived.
90    pub body: Vec<u8>,
91}
92
93/// What a tenant's Store keeps, as removal checks it.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct Counts {
96    /// Versions kept, hidden ones included.
97    pub versions: u64,
98    /// Uuids under a legal hold.
99    pub held: u64,
100}
101
102/// Why a Store call was refused or failed.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum StoreError {
105    Conflict,
106    Gone,
107    NotFound,
108    NotDeleted,
109    PreconditionFailed,
110    Immutable,
111    /// The tenant key is at its wrap limit and must be rotated.
112    LimitReached,
113    /// A row's object file is missing or does not open.
114    Damaged,
115    /// The audit record could not be written, so nothing was done.
116    AuditFailed,
117    Io,
118}
119
120/// Why a tenant's Store did not open: kept apart so a key that does not
121/// open its catalog is never confused with files this process may not use.
122#[derive(Debug)]
123pub enum OpenError {
124    /// The catalog does not open under the tenant's catalog key.
125    WrongKey,
126    /// This process may not use `path`, a file or directory the Store
127    /// needs; `error` is the system's.
128    Access {
129        path: PathBuf,
130        error: std::io::Error,
131    },
132    /// Anything else.
133    Store(StoreError),
134}
135
136impl From<OpenError> for StoreError {
137    /// How the Store's callers see a Store that did not open.
138    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
159/// One tenant's vCons.
160pub 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    /// Opens the tenant's catalog under its catalog key, making it if new,
173    /// then runs start-up recovery.
174    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    /// Opens the store as a command running beside a server does: without
187    /// start-up recovery, which would take the server's writes in progress
188    /// for a crash's leftovers.
189    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        // Each version is written into objects/, so it must be writable too.
202        catalog::may_use(&objects, libc::W_OK | libc::X_OK)?;
203        // A new directory survives a crash only once the directory that
204        // holds it is synced; without this a crash could take the objects
205        // directory, synced files and all, while the catalog keeps its rows.
206        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    /// Start-up recovery: the WAL is checkpointed with truncation, and
220    /// temporary files and object files with no row are removed. A name
221    /// that is not a version id, with or without `.tmp`, is not ours and
222    /// is left alone.
223    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    /// Stores a new vCon. The uuid's state is checked before anything is
262    /// sealed, the body is written in the order below, and the
263    /// commit checks the state again, so of two racing writers one wins
264    /// and the other's file is removed.
265    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                // The wrap is counted before it is made, so every wrap is
299                // counted once whether this write wins or loses.
300                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            // The version's time, and the uuid's first: the list keeps
332            // its place by it through replace, delete and restore.
333            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            // Best effort: start-up recovery removes any file left over.
361            let _ = self.fs.remove(&file);
362        }
363        committed
364    }
365
366    /// Reads the current version of `uuid`. The body is decrypted whole,
367    /// then one transaction checks that the version is still current and
368    /// commits the audit record; only then is the body returned. If the
369    /// version changed meanwhile, the read starts again.
370    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    /// Replaces the current version of `found.uuid`, if it is `if_match`
422    /// and neither signed nor encrypted. The caller has checked that the
423    /// body's uuid is the one asked for. The old version is hidden, and
424    /// kept for legal reads until it is purged. As in `create`, the state
425    /// is checked before sealing and again at the commit, so of two racing
426    /// replaces one wins and the other's file is removed.
427    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        // Why the state does not allow this replace, if it does not.
444        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            // Best effort: start-up recovery removes any file left over.
519            let _ = self.fs.remove(&file);
520        }
521        committed
522    }
523
524    /// Deletes `uuid`: one transaction marks it deleted, empties its
525    /// current version and hides every kept version, with the audit
526    /// record. Its versions stay for legal reads until they are purged;
527    /// any already as old as `retention` is purged at once, so a zero
528    /// period erases the vCon inside the request.
529    /// Deleting a deleted or purged uuid succeeds again. A legal hold does
530    /// not stop a delete, only a purge.
531    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        // A version already as old as `retention` is erased now, not at
565        // the next sweep.
566        self.purge_hidden(Actor::Key(who.key), retention, Some(uuid))?;
567        Ok(())
568    }
569
570    /// Every version whose object file is missing. Such a version answers
571    /// 500 when read; it is reported, never dropped.
572    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    /// Seals `body` as a new version of `uuid` under a new data key, wraps
596    /// that key under the tenant key, and writes the object file. Returns
597    /// the version id, the wrapped key and the file's path.
598    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    /// In tests, runs the hook set for the gap between work done outside
617    /// the catalog lock and the transaction that confirms it.
618    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    /// Writes a sealed body in order: a temporary file, synced,
629    /// renamed to its version id, and the directory synced. Returns the
630    /// file's path.
631    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            // Best effort: start-up recovery removes any file left over.
642            let _ = self.fs.remove(&tmp);
643            let _ = self.fs.remove(&file);
644            return Err(StoreError::Io);
645        }
646        Ok(file)
647    }
648
649    /// The plaintext of a stored version. A missing row or file, or one
650    /// that does not open, is damage.
651    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    /// Opens a version's object file with its wrapped data key.
662    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    /// How many versions the Store keeps and how many uuids are held.
683    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    /// Writes the tenant's log after `since` as JSON lines, oldest first.
696    /// Not recorded: reading the log changes nothing.
697    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    /// A page of the tenant's log as JSON lines, the lines `export` writes
706    /// for the same records. The read is recorded as audit_read in the same
707    /// log and transaction, after the page is read, so a page never holds
708    /// its own record.
709    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    /// Records, in a transaction of its own, an action on the tenant taken
733    /// elsewhere, such as a change to its retention period.
734    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    /// Makes every audit insert on this Store's connection fail, for tests.
742    #[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    /// Sets the hook a backup runs at each try, after the catalog is
753    /// copied, for tests outside this module.
754    #[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    /// The catalog connection, for tests that damage or inspect it.
760    #[cfg(test)]
761    pub(crate) fn conn_for_test(&self) -> MutexGuard<'_, Connection> {
762        self.conn()
763    }
764
765    /// The tenant's log as `export` writes it, for tests.
766    #[cfg(test)]
767    pub(crate) fn records(&self) -> Vec<serde_json::Value> {
768        crate::audit::testing::exported(&self.conn(), None)
769    }
770
771    /// Counts, in a transaction of its own, an event the tenant's log
772    /// keeps one record per minute for, such as a failed check of one of
773    /// its keys.
774    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    /// Writes an audit record in `tx`; if it cannot, nothing is done.
782    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
793/// What the catalog says about a uuid.
794enum State {
795    /// Never stored.
796    Absent,
797    /// Stored, with this current version.
798    Live(VersionId),
799    /// Deleted or purged.
800    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        // A live row always names its current version.
828        Some((None, None)) => Err(StoreError::Damaged),
829    }
830}
831
832/// What a version row holds that a read needs.
833struct VersionRow {
834    form: vcon::Form,
835    gzip: bool,
836    wrapped: Vec<u8>,
837}
838
839/// A version's row. One that is missing or unreadable is damage.
840fn 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
863/// Data keys wrapped so far under the tenant key `key`.
864fn 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/// Stored vCons, for tests.
908#[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    /// An unsigned vCon with this uuid, and what `inspect` makes of it.
916    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    /// Each version is written into objects/: a Store that may not write
941    /// there does not open, and says so with the path and the system's
942    /// error, as it does for its catalog.
943    #[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    /// What a backup runs at each try, after it copies the catalog.
970    pub(crate) type BackupHook = Box<dyn FnMut(&Store) + Send>;
971
972    /// Test-only points where a test can step in.
973    #[derive(Default)]
974    pub(super) struct Hooks {
975        /// Runs once, between work done outside the catalog lock (a
976        /// write's files, a read's decryption) and the transaction that
977        /// confirms it.
978        pub(super) between: Mutex<Option<Box<dyn FnOnce() + Send>>>,
979        /// Runs at each try of a backup, after the catalog is copied and
980        /// before its versions' files are.
981        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        /// The tenant key, wrapped under `master`, so a test can open the
989        /// store again with the same key.
990        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        /// `open`, saying why a Store that did not open did not.
1004        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        /// The tenant key the Store was made under.
1017        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        /// Opens the store again as a command beside a server would.
1027        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        /// Closes the store and opens it again, as a restart would.
1041        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    /// The body the store holds for `version`, opened as `get` will.
1089    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        // A page of two from seq 1: the second and third creates.
1139        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        // Past the end: nothing but the earlier reads' records.
1142        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    /// A record that cannot be read back (a status past any HTTP status)
1148    /// fails the page, and the read is still recorded, as 500.
1149    #[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        // The same length as the first body, so only the bytes differ.
1253        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        // Every audit insert now fails, as a full disk or a broken log would.
1416        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    /// Stores `subject` under UUID and returns its version.
1625    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    /// A command beside a running server must not take the server's
2123    /// writes in progress for a crash's leftovers.
2124    #[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    /// The filesystem changes a create or replace makes when nothing
2204    /// fails: write, sync, rename, sync the directory.
2205    const WRITE_STEPS: usize = 4;
2206
2207    /// After a restart, no row lacks its file and every file has a row.
2208    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    /// Makes the next audit insert fail for every connection, as if the
2227    /// process died before its commit; `unblock` removes it again.
2228    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}