Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

crates/gunnar-ui-server/src/state.rs

//! The BFF's small persistent state: opaque bytes under string keys.
//!
//! Three things live here and nothing else: the registered passkeys, the key
//! that MACs session cookies, and the session epoch that revokes them all at
//! once. The store stays ignorant of what any of them mean, which is why the
//! passkey code can change shape without a migration.
//!
//! redb rather than a file, because [`StateStore::get_or_init`] has to be atomic:
//! two requests racing to create the session key must not end up with two
//! different keys, one of which silently invalidates the other's cookies.

use std::collections::HashMap;
use std::fmt;
use std::path::Path;
use std::sync::{Arc, Mutex, PoisonError, RwLock};

use redb::{Database, ReadableTable, ReadableTableMetadata, TableDefinition};

const AUTH: TableDefinition<&str, &[u8]> = TableDefinition::new("auth");

/// **A second, BOUNDED table, and the reason it is not the first one.**
///
/// [`AUTH`] holds three values that must never be dropped: lose the session key
/// and every cookie the console minted stops verifying. What goes in here is
/// the opposite — derived bytes that can always be made again, and that would
/// otherwise grow without a limit. Keeping them in the same table would put a
/// registered passkey one eviction away from being deleted, so they are two
/// tables and the eviction only ever walks this one.
///
/// Today its only tenant is the rendered README SVG (see
/// [`crate::readme::cache`]).
const CACHE: TableDefinition<&str, &[u8]> = TableDefinition::new("cache");

/// **A third table: what one PERSON asked the console to remember.**
///
/// Neither of the other two fits, and the difference is what the tables are
/// for. [`AUTH`] holds three values whose loss breaks the console, so nothing in
/// it is ever evicted; [`CACHE`] holds derived bytes that can always be made
/// again, so everything in it is. A preference is a third thing: losing one
/// costs a person one click and nothing else, but it must not be *evicted* by a
/// large repository's README — a setting that quietly forgets itself under load
/// is worse than one that was never offered.
///
/// Keyed `"<user>\u{1f}<setting>"`, the shape `reviewer::store_key` already uses
/// for a per-person record, so a second setting is a second key and never a
/// migration. The value is opaque bytes here, as everything in this file is; it
/// is [`crate::reading`] that knows one of them spells a way of reading
/// markdown.
const PREFS: TableDefinition<&str, &[u8]> = TableDefinition::new("prefs");

/// Insertion order over [`CACHE`]: tick -> key. The lowest tick is the oldest
/// entry and the next one to go.
const CACHE_ORDER: TableDefinition<u64, &str> = TableDefinition::new("cache_order");

/// key -> the tick it was inserted at, so a rewrite can retire its old place in
/// the queue instead of leaving a dangling one.
const CACHE_TICK: TableDefinition<&str, u64> = TableDefinition::new("cache_tick");

/// The running totals: `"tick"` is the next number to hand out, `"bytes"` is
/// what [`CACHE`] currently holds.
///
/// Kept rather than recomputed because the alternative is summing every value's
/// length on every insert, and the values here are hundreds of kilobytes each.
const CACHE_META: TableDefinition<&str, u64> = TableDefinition::new("cache_meta");

/// A persistent key-value corner for authentication state.
#[derive(Clone)]
pub struct StateStore {
    db: Arc<Database>,
    /// The auth table as redb holds it, for the keys asked about: what every
    /// request reads (the epoch, a person's floor and session keys) is
    /// answered from memory, not from the disk on an async worker.
    auth: Arc<AuthMemory>,
    /// How many read transactions [`StateStore::get`] has begun: the cost the
    /// request path must not pay per request.
    #[cfg(test)]
    reads: Arc<std::sync::atomic::AtomicU64>,
}

impl fmt::Debug for StateStore {
    /// Never renders values: they include the session MAC key.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("StateStore").finish_non_exhaustive()
    }
}

/// The state store could not be read or written.
#[derive(Debug)]
pub struct StateError {
    what: String,
    source: Box<dyn std::error::Error + Send + Sync>,
}

impl fmt::Display for StateError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.what, self.source)
    }
}

impl std::error::Error for StateError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(self.source.as_ref())
    }
}

fn err(
    what: impl Into<String>,
    source: impl std::error::Error + Send + Sync + 'static,
) -> StateError {
    StateError {
        what: what.into(),
        source: Box::new(source),
    }
}

/// **The mode this file is created with, and why it is not the umask's.**
///
/// What is in here is the session MAC key (`auth::session_key`) and every
/// reviewer's ed25519 private key (`reviewer::signing_key`). Read the first and
/// you can mint a cookie for any user at any role; read the second and you can
/// sign an approval as a person who never saw the change.
///
/// `Database::create` opened it with whatever the process's umask allowed,
/// which in a container image that never sets one is **0644** — world readable.
/// Nothing about the file's contents says "secret" to anybody looking at a
/// directory listing, so the mode is stated here rather than inherited.
///
/// Both halves are needed and they are not the same half: `mode()` applies only
/// when the file is CREATED, so it protects a new deployment; `set_permissions`
/// tightens one that already exists, which is every deployment that ran before
/// this commit.
#[cfg(unix)]
const PRIVATE_FILE: u32 = 0o600;

/// The mode the directory holding it is created with. Same argument; a
/// traversable directory is how a file with a good mode is found.
#[cfg(unix)]
pub const PRIVATE_DIR: u32 = 0o700;

/// Create `path` and every missing parent, readable only by this user.
///
/// `std::fs::create_dir_all` takes no mode, so the mode is the umask's — and
/// `main` used it for the directory the state file lives in. This is the same
/// climb with a mode on every directory it makes.
///
/// # Errors
///
/// Fails if a directory cannot be created.
pub fn create_private_dir(path: &Path) -> std::io::Result<()> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::DirBuilderExt as _;
        std::fs::DirBuilder::new()
            .recursive(true)
            .mode(PRIVATE_DIR)
            .create(path)
    }
    #[cfg(not(unix))]
    {
        std::fs::create_dir_all(path)
    }
}

/// Make sure the file at `path` exists and is this user's alone.
///
/// Called BEFORE redb opens it, so there is never a moment where the store is
/// on disk with a mode somebody else can read. See [`PRIVATE_FILE`].
#[cfg(unix)]
fn claim_the_file(path: &Path) -> std::io::Result<()> {
    use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
    // Created at 0600 rather than created and then chmodded: the gap between
    // the two is short and it is still a gap.
    std::fs::OpenOptions::new()
        .create(true)
        .truncate(false)
        .write(true)
        .mode(PRIVATE_FILE)
        .open(path)?;
    // And a store that already exists keeps whatever mode it was made with
    // until something changes it. This is that something.
    std::fs::set_permissions(path, std::fs::Permissions::from_mode(PRIVATE_FILE))
}

/// **The auth table, remembered.** Coherent because nothing else writes it:
/// redb holds the file lock, so this process is its only writer, and every
/// write below goes through [`AuthMemory::writing`] and lands here after its
/// commit, overwriting whatever a racing reader filed.
#[derive(Default)]
struct AuthMemory {
    /// key → its value, or `None` for a key redb does not hold.
    rows: RwLock<HashMap<String, Option<Vec<u8>>>>,
    /// Writers one at a time, so the memory is left in redb's commit order.
    writing: Mutex<()>,
}

/// How many keys the memory holds before it stops remembering ABSENT ones:
/// a value is a row of the table and so bounded by it, an absence is bounded
/// only by what is asked about.
const REMEMBERED_KEYS: usize = 4096;

impl AuthMemory {
    fn held(&self, key: &str) -> Option<Option<Vec<u8>>> {
        self.rows.read().unwrap_or_else(PoisonError::into_inner).get(key).cloned()
    }

    /// File what a reader found, unless a writer filed something since.
    fn found(&self, key: &str, value: Option<&[u8]>) {
        let mut rows = self.rows.write().unwrap_or_else(PoisonError::into_inner);
        if value.is_some() || rows.len() < REMEMBERED_KEYS {
            rows.entry(key.to_owned()).or_insert_with(|| value.map(<[u8]>::to_vec));
        }
    }

    /// File what a writer committed, or forget the key if the write failed.
    fn wrote(&self, key: &str, value: Result<Option<&[u8]>, ()>) {
        let mut rows = self.rows.write().unwrap_or_else(PoisonError::into_inner);
        match value {
            Ok(Some(value)) => {
                rows.insert(key.to_owned(), Some(value.to_vec()));
            }
            Ok(None) if rows.contains_key(key) || rows.len() < REMEMBERED_KEYS => {
                rows.insert(key.to_owned(), None);
            }
            Ok(None) | Err(()) => {
                rows.remove(key);
            }
        }
    }

    fn write_lock(&self) -> std::sync::MutexGuard<'_, ()> {
        self.writing.lock().unwrap_or_else(PoisonError::into_inner)
    }
}

impl StateStore {
    /// Open, creating the file if it is not there.
    ///
    /// On unix the file is this user's alone — mode 0600 — whatever the
    /// process's umask says; see [`PRIVATE_FILE`] for what is in it.
    ///
    /// # Errors
    ///
    /// Fails if the file cannot be created, secured or opened.
    pub fn open(path: impl AsRef<Path>) -> Result<StateStore, StateError> {
        #[cfg(unix)]
        claim_the_file(path.as_ref())
            .map_err(|e| err("cannot create the state store with a private file mode", e))?;
        let db =
            Database::create(path.as_ref()).map_err(|e| err("cannot open the state store", e))?;
        let store = StateStore {
            db: Arc::new(db),
            auth: Arc::default(),
            #[cfg(test)]
            reads: Arc::default(),
        };
        // Materialise the table so a later read-only transaction does not fail
        // on a database that has never been written.
        let tx = store
            .db
            .begin_write()
            .map_err(|e| err("cannot begin a write", e))?;
        tx.open_table(AUTH)
            .map_err(|e| err("cannot open the auth table", e))?;
        tx.open_table(CACHE)
            .map_err(|e| err("cannot open the cache table", e))?;
        tx.open_table(PREFS)
            .map_err(|e| err("cannot open the preferences table", e))?;
        tx.open_table(CACHE_ORDER)
            .map_err(|e| err("cannot open the cache order table", e))?;
        tx.open_table(CACHE_TICK)
            .map_err(|e| err("cannot open the cache tick table", e))?;
        tx.open_table(CACHE_META)
            .map_err(|e| err("cannot open the cache meta table", e))?;
        tx.commit().map_err(|e| err("cannot commit", e))?;
        Ok(store)
    }

    /// Read a value: from memory once redb has been asked about the key.
    ///
    /// # Errors
    ///
    /// Fails on a store-level read error (never remembered: the next read asks
    /// redb again). A missing key is `Ok(None)`.
    pub fn get(&self, key: &str) -> Result<Option<Vec<u8>>, StateError> {
        if let Some(held) = self.auth.held(key) {
            return Ok(held);
        }
        #[cfg(test)]
        self.reads.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
        let tx = self
            .db
            .begin_read()
            .map_err(|e| err("cannot begin a read", e))?;
        let value = {
            let table = tx
                .open_table(AUTH)
                .map_err(|e| err("cannot open the auth table", e))?;
            let held = table
                .get(key)
                .map_err(|e| err(format!("cannot read {key:?}"), e))?;
            held.map(|v| v.value().to_vec())
        };
        self.auth.found(key, value.as_deref());
        Ok(value)
    }

    /// Write a value, replacing any previous one.
    ///
    /// # Errors
    ///
    /// Fails on a store-level write error.
    pub fn put(&self, key: &str, value: &[u8]) -> Result<(), StateError> {
        let _writing = self.auth.write_lock();
        let written = self.put_in_redb(key, value);
        self.auth.wrote(key, written.as_ref().map(|()| Some(value)).map_err(|_| ()));
        written
    }

    fn put_in_redb(&self, key: &str, value: &[u8]) -> Result<(), StateError> {
        let tx = self
            .db
            .begin_write()
            .map_err(|e| err("cannot begin a write", e))?;
        {
            let mut table = tx
                .open_table(AUTH)
                .map_err(|e| err("cannot open the auth table", e))?;
            table
                .insert(key, value)
                .map_err(|e| err(format!("cannot write {key:?}"), e))?;
        }
        tx.commit().map_err(|e| err("cannot commit", e))?;
        Ok(())
    }

    /// Forget a value.
    ///
    /// **A key that was not there is `Ok(false)`, not an error.** The one caller
    /// is [`crate::tenant::TenantKeys::forget`], retiring a credential — and
    /// "it was already gone" and "it is gone now" are the same state to a
    /// revocation, which must be safe to repeat.
    ///
    /// # Errors
    ///
    /// Fails on a store-level write error.
    pub fn remove(&self, key: &str) -> Result<bool, StateError> {
        let _writing = self.auth.write_lock();
        let removed = self.remove_in_redb(key);
        self.auth.wrote(key, removed.as_ref().map(|_| None).map_err(|_| ()));
        removed
    }

    fn remove_in_redb(&self, key: &str) -> Result<bool, StateError> {
        let tx = self
            .db
            .begin_write()
            .map_err(|e| err("cannot begin a write", e))?;
        let had = {
            let mut table = tx
                .open_table(AUTH)
                .map_err(|e| err("cannot open the auth table", e))?;
            let gone = table
                .remove(key)
                .map_err(|e| err(format!("cannot remove {key:?}"), e))?;
            // Scoped, so the borrowed value is dropped before `table` is: an
            // `AccessGuard` holds the table open.
            gone.is_some()
        };
        tx.commit().map_err(|e| err("cannot commit", e))?;
        Ok(had)
    }

    /// Read a value, creating it from `make` if it is not there yet.
    ///
    /// The read and the write share one transaction, so concurrent callers agree
    /// on the value. They must: two session keys means one of them silently
    /// invalidates every cookie the other minted.
    ///
    /// # Errors
    ///
    /// Fails on a store-level read or write error.
    pub fn get_or_init(
        &self,
        key: &str,
        make: impl FnOnce() -> Vec<u8>,
    ) -> Result<Vec<u8>, StateError> {
        let _writing = self.auth.write_lock();
        let held = self.get_or_init_in_redb(key, make);
        self.auth.wrote(key, held.as_deref().map(Some).map_err(|_| ()));
        held
    }

    fn get_or_init_in_redb(
        &self,
        key: &str,
        make: impl FnOnce() -> Vec<u8>,
    ) -> Result<Vec<u8>, StateError> {
        let tx = self
            .db
            .begin_write()
            .map_err(|e| err("cannot begin a write", e))?;
        let value = {
            let mut table = tx
                .open_table(AUTH)
                .map_err(|e| err("cannot open the auth table", e))?;
            // The read guard borrows the table, so it has to be gone before
            // the insert below can take it mutably.
            let existing = table
                .get(key)
                .map_err(|e| err(format!("cannot read {key:?}"), e))?
                .map(|v| v.value().to_vec());
            match existing {
                Some(value) => value,
                None => {
                    let fresh = make();
                    table
                        .insert(key, fresh.as_slice())
                        .map_err(|e| err(format!("cannot write {key:?}"), e))?;
                    fresh
                }
            }
        };
        tx.commit().map_err(|e| err("cannot commit", e))?;
        Ok(value)
    }

    /// Forget everything remembered, so the next reads go to redb as every
    /// read did before the memory existed. Tests and the bench only.
    #[cfg(test)]
    pub(crate) fn forget_for_tests(&self) {
        self.auth.rows.write().unwrap_or_else(PoisonError::into_inner).clear();
    }

    /// How many times the store itself was read by [`StateStore::get`].
    #[cfg(test)]
    pub(crate) fn reads_for_tests(&self) -> u64 {
        self.reads.load(std::sync::atomic::Ordering::SeqCst)
    }

    // ── One person's settings ───────────────────────────────────────────────

    /// What this key was set to, if anybody set it.
    ///
    /// # Errors
    ///
    /// Fails on a store-level read error. An unset preference is `Ok(None)`,
    /// which every caller reads as "the default" — a person who has never
    /// pressed the button and a store that cannot be read are the same
    /// situation from the reader's chair.
    pub fn pref_get(&self, key: &str) -> Result<Option<Vec<u8>>, StateError> {
        let tx = self
            .db
            .begin_read()
            .map_err(|e| err("cannot begin a read", e))?;
        let table = tx
            .open_table(PREFS)
            .map_err(|e| err("cannot open the preferences table", e))?;
        let held = table
            .get(key)
            .map_err(|e| err(format!("cannot read {key:?}"), e))?;
        Ok(held.map(|v| v.value().to_vec()))
    }

    /// Remember this key's value, replacing any previous one.
    ///
    /// # Errors
    ///
    /// Fails on a store-level write error.
    pub fn pref_put(&self, key: &str, value: &[u8]) -> Result<(), StateError> {
        let tx = self
            .db
            .begin_write()
            .map_err(|e| err("cannot begin a write", e))?;
        {
            let mut table = tx
                .open_table(PREFS)
                .map_err(|e| err("cannot open the preferences table", e))?;
            table
                .insert(key, value)
                .map_err(|e| err(format!("cannot write {key:?}"), e))?;
        }
        tx.commit().map_err(|e| err("cannot commit", e))?;
        Ok(())
    }

    /// How many preferences are remembered, across everybody.
    ///
    /// A count and not a listing: a preference key is `"<user>\u{1f}<setting>"`
    /// and the users are the whole of it, so a listing would be a roster of
    /// everyone who has ever changed a setting. [`crate::reset`] needs the
    /// number to say what a `--all` would take, and nothing needs the names.
    ///
    /// # Errors
    ///
    /// Fails on a store-level read error.
    pub fn pref_count(&self) -> Result<usize, StateError> {
        let tx = self
            .db
            .begin_read()
            .map_err(|e| err("cannot begin a read", e))?;
        let table = tx
            .open_table(PREFS)
            .map_err(|e| err("cannot open the preferences table", e))?;
        Ok(table
            .len()
            .map_err(|e| err("cannot count the preferences", e))? as usize)
    }

    // ── The bounded half ────────────────────────────────────────────────────

    /// Read a cached value.
    ///
    /// A read transaction and nothing else: no touch, no bookkeeping, no
    /// commit. That is what makes a hit cheap enough to be worth having, and
    /// it is why eviction is by insertion order — see [`crate::readme::cache`].
    ///
    /// # Errors
    ///
    /// Fails on a store-level read error. A missing key is `Ok(None)`.
    pub fn cache_get(&self, key: &str) -> Result<Option<Vec<u8>>, StateError> {
        let tx = self
            .db
            .begin_read()
            .map_err(|e| err("cannot begin a read", e))?;
        let table = tx
            .open_table(CACHE)
            .map_err(|e| err("cannot open the cache table", e))?;
        let held = table
            .get(key)
            .map_err(|e| err(format!("cannot read {key:?}"), e))?;
        Ok(held.map(|v| v.value().to_vec()))
    }

    /// Write a cached value, evicting the oldest entries until both caps hold.
    ///
    /// The whole thing — insert, bookkeeping and eviction — is ONE transaction.
    /// Two would leave a window in which the totals disagree with the table,
    /// and the totals are what the eviction loop trusts; a crash between them
    /// would leave a cache that believed it was empty and grew forever.
    ///
    /// A value larger than `max_bytes` on its own is refused rather than
    /// stored: keeping it would evict everything else and then still be over.
    ///
    /// # Errors
    ///
    /// Fails on a store-level read or write error.
    pub fn cache_put(
        &self,
        key: &str,
        value: &[u8],
        max_entries: usize,
        max_bytes: u64,
    ) -> Result<(), StateError> {
        let size = value.len() as u64;
        if size > max_bytes {
            return Ok(());
        }
        let tx = self
            .db
            .begin_write()
            .map_err(|e| err("cannot begin a write", e))?;
        {
            let mut cache = tx
                .open_table(CACHE)
                .map_err(|e| err("cannot open the cache table", e))?;
            let mut order = tx
                .open_table(CACHE_ORDER)
                .map_err(|e| err("cannot open the cache order table", e))?;
            let mut ticks = tx
                .open_table(CACHE_TICK)
                .map_err(|e| err("cannot open the cache tick table", e))?;
            let mut meta = tx
                .open_table(CACHE_META)
                .map_err(|e| err("cannot open the cache meta table", e))?;

            let mut next_tick = meta
                .get("tick")
                .map_err(|e| err("cannot read the cache tick", e))?
                .map_or(0, |v| v.value());
            let mut bytes = meta
                .get("bytes")
                .map_err(|e| err("cannot read the cache size", e))?
                .map_or(0, |v| v.value());

            // Replacing a key retires its old bytes and its old place in the
            // queue, so the same README rendered twice does not count twice.
            if let Some(old) = cache
                .get(key)
                .map_err(|e| err("cannot read the cache table", e))?
            {
                bytes = bytes.saturating_sub(old.value().len() as u64);
            }
            if let Some(old) = ticks
                .get(key)
                .map_err(|e| err("cannot read the cache tick table", e))?
            {
                let old = old.value();
                order
                    .remove(old)
                    .map_err(|e| err("cannot retire a cache entry", e))?;
            }

            cache
                .insert(key, value)
                .map_err(|e| err(format!("cannot cache {key:?}"), e))?;
            order
                .insert(next_tick, key)
                .map_err(|e| err("cannot record a cache entry", e))?;
            ticks
                .insert(key, next_tick)
                .map_err(|e| err("cannot record a cache tick", e))?;
            bytes += size;
            next_tick += 1;

            // Evict from the front until both caps hold. `first()` is the
            // lowest tick, which is the oldest insertion.
            loop {
                let over = cache.len().map_err(|e| err("cannot count the cache", e))? as usize
                    > max_entries
                    || bytes > max_bytes;
                if !over {
                    break;
                }
                let Some((tick, victim)) = order
                    .first()
                    .map_err(|e| err("cannot read the cache order", e))?
                    .map(|(k, v)| (k.value(), v.value().to_owned()))
                else {
                    break;
                };
                if let Some(gone) = cache
                    .remove(victim.as_str())
                    .map_err(|e| err("cannot evict a cache entry", e))?
                {
                    bytes = bytes.saturating_sub(gone.value().len() as u64);
                }
                order
                    .remove(tick)
                    .map_err(|e| err("cannot evict a cache entry", e))?;
                ticks
                    .remove(victim.as_str())
                    .map_err(|e| err("cannot evict a cache tick", e))?;
            }

            meta.insert("tick", next_tick)
                .map_err(|e| err("cannot record the cache tick", e))?;
            meta.insert("bytes", bytes)
                .map_err(|e| err("cannot record the cache size", e))?;
        }
        tx.commit().map_err(|e| err("cannot commit", e))?;
        Ok(())
    }

    /// What the bounded table currently holds.
    ///
    /// # Errors
    ///
    /// Fails on a store-level read error.
    pub fn cache_stats(&self) -> Result<CacheStats, StateError> {
        let tx = self
            .db
            .begin_read()
            .map_err(|e| err("cannot begin a read", e))?;
        let cache = tx
            .open_table(CACHE)
            .map_err(|e| err("cannot open the cache table", e))?;
        let meta = tx
            .open_table(CACHE_META)
            .map_err(|e| err("cannot open the cache meta table", e))?;
        Ok(CacheStats {
            entries: cache.len().map_err(|e| err("cannot count the cache", e))? as usize,
            bytes: meta
                .get("bytes")
                .map_err(|e| err("cannot read the cache size", e))?
                .map_or(0, |v| v.value()),
        })
    }

    /// **Every key in [`AUTH`], sorted.** Names only; no value is read.
    ///
    /// The one caller is [`crate::reset`], which has to be able to SAY what it
    /// is about to destroy before it destroys it. Values are deliberately left
    /// behind: one of them is the key that MACs every session cookie, and a
    /// listing that carried it would put it wherever the listing went.
    ///
    /// # Errors
    ///
    /// Fails on a store-level read error.
    pub fn auth_keys(&self) -> Result<Vec<String>, StateError> {
        let tx = self
            .db
            .begin_read()
            .map_err(|e| err("cannot begin a read", e))?;
        let table = tx
            .open_table(AUTH)
            .map_err(|e| err("cannot open the auth table", e))?;
        let mut keys = Vec::new();
        for row in table
            .iter()
            .map_err(|e| err("cannot walk the auth table", e))?
        {
            let (key, _) = row.map_err(|e| err("cannot read an auth row", e))?;
            keys.push(key.value().to_owned());
        }
        keys.sort();
        Ok(keys)
    }

    /// **Empty every table in one transaction**, and say how many rows went.
    ///
    /// One transaction because the tables refer to each other: [`CACHE`],
    /// [`CACHE_ORDER`], [`CACHE_TICK`] and [`CACHE_META`] are four views of one
    /// structure, and a crash between two separate clears would leave an
    /// eviction queue pointing at entries that are gone and a byte total for
    /// bytes nobody holds. redb commits all of it or none of it.
    ///
    /// The file itself is left in place rather than deleted and recreated,
    /// which is the whole reason this exists as a method: `rm state.redb` on a
    /// running console removes the name and leaves the open file behind, so
    /// the process keeps serving from a store nobody can see, and the next
    /// restart silently starts a second one. Emptying the tables is visible to
    /// the process that has it open — and on a running console this call does
    /// not get that far, because redb holds the file lock and
    /// [`StateStore::open`] refuses.
    ///
    /// # Errors
    ///
    /// Fails on a store-level write error, having changed nothing.
    pub fn clear(&self) -> Result<Cleared, StateError> {
        let _writing = self.auth.write_lock();
        let cleared = self.clear_in_redb();
        self.auth.rows.write().unwrap_or_else(PoisonError::into_inner).clear();
        cleared
    }

    fn clear_in_redb(&self) -> Result<Cleared, StateError> {
        let tx = self
            .db
            .begin_write()
            .map_err(|e| err("cannot begin a write", e))?;
        let cleared = {
            let mut auth = tx
                .open_table(AUTH)
                .map_err(|e| err("cannot open the auth table", e))?;
            let mut cache = tx
                .open_table(CACHE)
                .map_err(|e| err("cannot open the cache table", e))?;
            let mut prefs = tx
                .open_table(PREFS)
                .map_err(|e| err("cannot open the preferences table", e))?;
            let mut order = tx
                .open_table(CACHE_ORDER)
                .map_err(|e| err("cannot open the cache order table", e))?;
            let mut tick = tx
                .open_table(CACHE_TICK)
                .map_err(|e| err("cannot open the cache tick table", e))?;
            let mut meta = tx
                .open_table(CACHE_META)
                .map_err(|e| err("cannot open the cache meta table", e))?;
            let cleared = Cleared {
                auth: auth.len().map_err(|e| err("cannot count the auth table", e))? as usize,
                cache: cache.len().map_err(|e| err("cannot count the cache", e))? as usize,
                prefs: prefs
                    .len()
                    .map_err(|e| err("cannot count the preferences", e))?
                    as usize,
            };
            // `retain(|_, _| false)` rather than collecting the keys and
            // removing them one by one: the keys would be a second copy of the
            // whole table in memory, and the cache's values are hundreds of
            // kilobytes each.
            auth.retain(|_, _| false)
                .map_err(|e| err("cannot empty the auth table", e))?;
            cache
                .retain(|_, _| false)
                .map_err(|e| err("cannot empty the cache", e))?;
            prefs
                .retain(|_, _| false)
                .map_err(|e| err("cannot empty the preferences", e))?;
            order
                .retain(|_, _| false)
                .map_err(|e| err("cannot empty the cache order", e))?;
            tick.retain(|_, _| false)
                .map_err(|e| err("cannot empty the cache ticks", e))?;
            meta.retain(|_, _| false)
                .map_err(|e| err("cannot empty the cache meta", e))?;
            cleared
        };
        tx.commit().map_err(|e| err("cannot commit", e))?;
        Ok(cleared)
    }
}

/// How many rows each table held when it was emptied.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Cleared {
    /// The passkeys, the session key, the epoch and the floors.
    pub auth: usize,
    /// Derived bytes that can always be made again.
    pub cache: usize,
    /// What people asked the console to remember.
    pub prefs: usize,
}

/// How much the bounded table is holding.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CacheStats {
    pub entries: usize,
    pub bytes: u64,
}

#[cfg(test)]
mod tests {
    use super::*;

    fn temp_store() -> (StateStore, tempfile::TempDir) {
        let dir = tempfile::tempdir().expect("a temp dir");
        let store = StateStore::open(dir.path().join("state.redb")).expect("a fresh store opens");
        (store, dir)
    }

    /// ★ **The file holding the session key and every reviewer's private key
    /// is this user's alone.**
    ///
    /// **Seen red.** `Database::create` opened it with the process's umask, so
    /// in a container image that sets none the store is 0644 — world readable.
    /// What is in it is the MAC key that mints session cookies and the ed25519
    /// keys that sign approvals, and nothing about a `.redb` in a directory
    /// listing says so.
    #[cfg(unix)]
    #[test]
    fn the_state_file_is_readable_by_nobody_else() {
        use std::os::unix::fs::PermissionsExt as _;

        let dir = tempfile::tempdir().expect("a temp dir");
        let path = dir.path().join("state.redb");
        let _store = StateStore::open(&path).expect("a fresh store opens");

        let mode = std::fs::metadata(&path)
            .expect("it is there")
            .permissions()
            .mode()
            & 0o777;
        assert_eq!(
            mode, PRIVATE_FILE,
            "the session key and every reviewer's private key are at mode {mode:o}"
        );
    }

    /// And a store that already exists at a loose mode is tightened when it is
    /// opened — which is every deployment that ran before this.
    #[cfg(unix)]
    #[test]
    fn a_world_readable_store_is_tightened_on_the_next_start() {
        use std::os::unix::fs::PermissionsExt as _;

        let dir = tempfile::tempdir().expect("a temp dir");
        let path = dir.path().join("state.redb");
        // A store from before, with what the umask gave it.
        let existing = StateStore::open(&path).expect("a fresh store opens");
        existing.put("k", b"v").expect("write");
        drop(existing);
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644))
            .expect("loosen it the way a 0022 umask would have");

        let reopened = StateStore::open(&path).expect("the store reopens");

        assert_eq!(
            std::fs::metadata(&path)
                .expect("it is there")
                .permissions()
                .mode()
                & 0o777,
            PRIVATE_FILE
        );
        // And the tightening did not cost the contents.
        assert_eq!(reopened.get("k").expect("read"), Some(b"v".to_vec()));
    }

    #[cfg(unix)]
    #[test]
    fn the_directory_it_lives_in_is_not_traversable_by_anybody_else() {
        use std::os::unix::fs::PermissionsExt as _;

        let dir = tempfile::tempdir().expect("a temp dir");
        let made = dir.path().join("var/lib/gunnar-ui");
        create_private_dir(&made).expect("the climb");

        assert_eq!(
            std::fs::metadata(&made)
                .expect("it is there")
                .permissions()
                .mode()
                & 0o777,
            PRIVATE_DIR,
            "a good mode on the file does not survive a directory anybody can walk"
        );
    }

    #[test]
    fn a_written_value_reads_back() {
        let (store, _dir) = temp_store();
        store.put("k", b"v").expect("write");

        assert_eq!(store.get("k").expect("read"), Some(b"v".to_vec()));
    }

    #[test]
    fn a_missing_key_is_not_an_error() {
        let (store, _dir) = temp_store();

        assert_eq!(store.get("absent").expect("read"), None);
    }

    /// The memory of the auth table is redb's, whatever order writers and
    /// readers race in: after the race, every key reads what redb holds.
    #[test]
    fn racing_writers_and_readers_leave_the_memory_as_redb_has_it() {
        let (store, _dir) = temp_store();
        let threads: Vec<_> = (0..8u8)
            .map(|t| {
                let store = store.clone();
                std::thread::spawn(move || {
                    for i in 0..50u8 {
                        let key = format!("k{}", i % 4);
                        match (t + i) % 3 {
                            0 => store.put(&key, &[t, i]).expect("a write"),
                            1 => {
                                store.remove(&key).expect("a removal");
                            }
                            _ => {
                                store.get(&key).expect("a read");
                            }
                        }
                    }
                })
            })
            .collect();
        for thread in threads {
            thread.join().expect("no panic");
        }
        for i in 0..4 {
            let key = format!("k{i}");
            let remembered = store.get(&key).expect("a read");
            store.forget_for_tests();
            assert_eq!(store.get(&key).expect("a read"), remembered, "{key}");
        }
    }

    /// A key is read from redb once and answered from memory after (what a
    /// request's reads cost; stinky #18).
    #[test]
    fn a_key_is_read_from_redb_once() {
        let (store, _dir) = temp_store();
        store.put("k", b"v").expect("a write");
        store.forget_for_tests();
        store.get("k").expect("a read");
        let after_one = store.reads_for_tests();
        for _ in 0..100 {
            store.get("k").expect("a read");
        }
        assert_eq!(store.reads_for_tests(), after_one, "a hundred reads went to redb");
    }

    #[test]
    fn a_write_through_any_clone_is_read_back_at_once() {
        let (store, _dir) = temp_store();
        let other = store.clone();
        assert_eq!(store.get("epoch").expect("a read"), None);
        other.put("epoch", b"1").expect("a write");
        assert_eq!(store.get("epoch").expect("a read").as_deref(), Some(&b"1"[..]));
        other.remove("epoch").expect("a removal");
        assert_eq!(store.get("epoch").expect("a read"), None);
        assert_eq!(other.get_or_init("epoch", || b"2".to_vec()).expect("made"), b"2");
        assert_eq!(store.get("epoch").expect("a read").as_deref(), Some(&b"2"[..]));
        store.clear().expect("emptied");
        assert_eq!(other.get("epoch").expect("a read"), None);
    }

    #[test]
    fn get_or_init_creates_once_and_then_keeps_it() {
        let (store, _dir) = temp_store();
        let first = store
            .get_or_init("session_key", || vec![1, 2, 3])
            .expect("init");
        let second = store
            .get_or_init("session_key", || vec![9, 9, 9])
            .expect("second call");

        assert_eq!(first, vec![1, 2, 3]);
        assert_eq!(
            second, first,
            "a second key would invalidate every cookie the first minted"
        );
    }

    /// ★ **A preference is its own table, and the README cache cannot evict
    /// it.**
    ///
    /// Red-first: filed in the bounded table beside the rendered pages, a
    /// person's setting was one busy afternoon of README renders away from being
    /// silently dropped — the cap is 256 entries and every repository anybody
    /// opens takes one.
    #[test]
    fn a_remembered_setting_outlives_a_cache_that_fills_up() {
        let (store, _dir) = temp_store();
        store
            .pref_put("alice\u{1f}markdown", b"source")
            .expect("write");

        for i in 0..(crate::readme::cache::MAX_ENTRIES + 8) {
            store
                .cache_put(&format!("page-{i}"), b"x", 256, 64 * 1024 * 1024)
                .expect("a cached page");
        }

        assert_eq!(
            store.pref_get("alice\u{1f}markdown").expect("read"),
            Some(b"source".to_vec()),
            "the cache evicted somebody's setting"
        );
        assert_eq!(store.pref_get("nobody\u{1f}markdown").expect("read"), None);
    }

    #[test]
    fn state_survives_reopening() {
        let dir = tempfile::tempdir().expect("a temp dir");
        let path = dir.path().join("state.redb");
        StateStore::open(&path)
            .expect("open")
            .put("k", b"v")
            .expect("write");

        let reopened = StateStore::open(&path).expect("reopen");

        assert_eq!(reopened.get("k").expect("read"), Some(b"v".to_vec()));
    }
}