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

nordisk/holger

server/lib/src/raw.rs

//! Generic / **raw** writable backend: a disk-backed, path-addressed bucket for
//! arbitrary blobs — the Nexus/Artifactory "raw hosted repository" (the misc
//! bucket for tarballs, installers, reports, firmware, anything that has no
//! ecosystem protocol of its own). A client `PUT`s bytes at an arbitrary path
//! and `GET`s them back byte-identical; the path *is* the address.
//!
//! Deliberately the leanest writable backend: no index synthesis, no metadata
//! generation, no ecosystem parsing — just a safe, content-verifiable loose-file
//! store. Because it implements the full [`RepositoryBackendTrait`] it drops into
//! every existing seam unchanged: the checksum-deploy gate verifies uploads at
//! the HTTP door, cross-repo [`search`](crate::search) finds blobs by
//! name/path/checksum, the retention/GC executor reclaims them
//! ([`delete_artifact`](RawBackend::delete_artifact) is the SHA-256-verified safe
//! unlink, agreeing with `holger-server-lib::sha256_hex`), and
//! [`replication`](crate::replication) copies them content-id-idempotently.
//!
//! **Coordinate ↔ path bijection (the one rule both entry points obey).** The
//! gRPC / in-process side addresses a blob by an [`ArtifactId`]; the HTTP side
//! addresses it by a raw path. They meet at one deterministic mapping:
//!   - `id → relpath` = the `/`-join of the non-empty parts of
//!     `[namespace, name, version]`.
//!   - [`list`](RawBackend::list) reports every stored file as
//!     `ArtifactId { namespace: None, name: <relpath>, version: "" }`.
//!
//! So `id_to_relpath(listed_id) == <relpath>` round-trips exactly — which is why
//! a listing fed back into `fetch`/`delete_artifact` (what retention, GC,
//! replication and search all do) resolves to the very same file.
//!
//! **Traversal-closed.** Every path segment is validated ([`safe_relpath`]): an
//! empty segment, `.`, `..`, a backslash or a NUL is refused, so no id and no
//! HTTP path can climb out of `base_dir`. (This mirrors the per-segment guard the
//! sibling loose-file backends keep privately — `sanitize_repo` in `promote`
//! collapses to a *single* segment, which would destroy the nested paths a raw
//! bucket exists to hold, so it is intentionally not reused here.)

use crate::lock::LockOrRecover as _;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::SystemTime;

use anyhow::Context as _;
use traits::{ArtifactEntry, ArtifactFormat, ArtifactId, RepositoryBackendTrait};

/// A writable, disk-backed, path-addressed blob store (the "raw hosted repo").
/// The bounded buffer a streamed write or read moves the blob through: the
/// whole of what a request costs this process in memory, whatever the blob's
/// size.
const STREAM_BUF_BYTES: usize = 1024 * 1024;

pub struct RawBackend {
    name: String,
    base_dir: PathBuf,
    /// Memoized `relpath → (stamp, sha256_hex)` for blobs whose bytes this
    /// process has already had in hand — see [`RawBackend::peek_digest`].
    digests: Mutex<HashMap<String, (FileStamp, String)>>,
}

/// Identity of a stored raw blob, used to decide whether a memoized digest is
/// still the digest OF THE BYTES ON DISK NOW.
///
/// Raw blobs are MUTABLE — unlike `file-rust`'s crate files, which are immutable
/// by the registry's own rules, a raw path can be overwritten. So the memo may
/// never be trusted on the strength of the path alone: `(len, mtime)` must still
/// match, and a blob with no readable modification time is not memoized at all,
/// because a stamp with no clock in it cannot be invalidated.
///
/// ⚠ The residual hole, named rather than hidden: an out-of-band overwrite that
/// lands the SAME length at the SAME mtime tick would keep a stale digest. It
/// cannot happen through holger's own door — every write goes through
/// [`RawBackend::write_relpath`], which re-memoizes — and it is the identical
/// hole `file-rust` has carried since it was written. What it is NOT is a
/// trust boundary: `ArtifactEntry::sha256`'s own contract says an absent or
/// present digest never means the bytes are trusted.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct FileStamp {
    len: u64,
    mtime: SystemTime,
}

/// The memo's memory bound, in entries.
///
/// A raw bucket can hold millions of blobs and this map would otherwise grow
/// with every one this process ever touched. At the cap the memo stops GROWING
/// rather than evicting: a full memo still answers for everything already in it,
/// and the only consequence of the cap is that a listing beyond it reports
/// `None` — which is exactly what the field means. Eviction would instead make
/// the answer depend on access order, which is worse than an honest unknown.
const DIGEST_MEMO_CAP: usize = 65_536;

/// Lowercase-hex SHA-256 of `data`. The content id the retention/GC executor
/// verifies before an unlink — SHA-256 (not blake3) so it agrees with
/// `holger-server-lib::sha256_hex` and the other loose-file backends.
fn sha256_hex(data: &[u8]) -> String {
    // LAW #5 dedup: consume the shared `nornir-hash` leaf (edda) so this backend's
    // content id agrees with `holger-server-lib::sha256_hex` and the siblings.
    nornir_hash::sha256_hex(data)
}

/// Validate a `/`-delimited relative path and join it under `base_dir`.
///
/// Every segment must be a benign single directory level — a hostile segment
/// (empty, `.`, `..`, containing a backslash or a NUL byte) is refused, so the
/// resulting path can never escape `base_dir`. An empty path (no segments) is
/// itself invalid (a raw blob needs a name). Returns the joined absolute path.
fn safe_relpath(base_dir: &Path, relpath: &str) -> anyhow::Result<PathBuf> {
    let mut out = base_dir.to_path_buf();
    let mut any = false;
    for seg in relpath.split('/') {
        if seg.is_empty() || seg == "." || seg == ".." || seg.contains('\\') || seg.contains('\0') {
            anyhow::bail!("invalid raw path segment {seg:?} in {relpath:?}");
        }
        out.push(seg);
        any = true;
    }
    if !any {
        anyhow::bail!("empty raw artifact path");
    }
    Ok(out)
}

impl RawBackend {
    /// Build a raw repo named `name`, storing blobs under `base_dir`.
    pub fn new(name: String, base_dir: PathBuf) -> Self {
        Self {
            name,
            base_dir,
            digests: Mutex::new(HashMap::new()),
        }
    }

    /// Read a path's `(len, mtime)` stamp, or `None` when it has no readable
    /// modification time — which is the one case the memo refuses to key on.
    fn stamp_of(path: &Path) -> Option<FileStamp> {
        let meta = std::fs::metadata(path).ok()?;
        Some(FileStamp {
            len: meta.len(),
            mtime: meta.modified().ok()?,
        })
    }

    /// Record the digest of bytes this process has in hand ANYWAY.
    ///
    /// Called from the two places raw already holds a whole blob — a write and a
    /// read — so the hash is the only added cost and it is paid once per blob
    /// per change, never once per listing. This is what makes `unchanged`
    /// reachable downstream: an upstream holger that has SERVED a fetch of a
    /// blob can answer with that blob's digest the next time it is listed, and a
    /// mirror that already has those bytes then does not re-fetch them.
    fn memoize_digest(&self, relpath: &str, path: &Path, data: &[u8]) {
        self.memoize_digest_hex(relpath, path, sha256_hex(data));
    }

    /// The same memo, for a digest computed WHILE the bytes streamed past —
    /// the write side of `put_from_reader`, where the blob was never whole in
    /// memory and so cannot be hashed after the fact without a second read.
    fn memoize_digest_hex(&self, relpath: &str, path: &Path, hex: String) {
        let Some(stamp) = Self::stamp_of(path) else {
            return;
        };
        let mut memo = self.digests.lock_or_recover();
        if memo.len() >= DIGEST_MEMO_CAP && !memo.contains_key(relpath) {
            return;
        }
        memo.insert(relpath.to_string(), (stamp, hex));
    }

    /// Forget a path's digest — called on every unlink, so a later blob at the
    /// same path can never inherit the dead one's answer.
    fn forget_digest(&self, relpath: &str) {
        {
            let mut memo = self.digests.lock_or_recover();
            memo.remove(relpath);
        }
    }

    /// **The listing path's digest: a memo PEEK, and never a hash.**
    ///
    /// The listing walk already holds each file's length and mtime, so this
    /// costs a map lookup and two comparisons. It NEVER opens a file: a raw
    /// listing that hashed its store would be O(bytes of the whole repository)
    /// and that is the property the test
    /// `a_raw_listing_never_hashes_a_blob_it_has_not_handled` exists to keep.
    ///
    /// `None` means "not hashed yet by this process, or changed since", never
    /// "no digest exists" and never "the bytes are untrusted".
    fn peek_digest(&self, relpath: &str, len: u64, mtime: Option<SystemTime>) -> Option<String> {
        let mtime = mtime?;
        let memo = self.digests.lock_or_recover();
        let (stamp, hex) = memo.get(relpath)?;
        (stamp.len == len && stamp.mtime == mtime).then(|| hex.clone())
    }

    /// The one coordinate→path map: the `/`-join of the non-empty parts of
    /// `[namespace, name, version]`. A raw blob's address is its path, so a
    /// caller that leaves `namespace`/`version` empty simply addresses by `name`;
    /// a listing round-trips because `list` puts the whole relpath in `name`.
    fn id_to_relpath(id: &ArtifactId) -> String {
        let mut parts: Vec<&str> = Vec::with_capacity(3);
        if let Some(ns) = id.namespace.as_deref() {
            if !ns.is_empty() {
                parts.push(ns);
            }
        }
        if !id.name.is_empty() {
            parts.push(&id.name);
        }
        if !id.version.is_empty() {
            parts.push(&id.version);
        }
        parts.join("/")
    }

    /// Read the bytes stored at a validated relpath (`None` if absent).
    fn read_relpath(&self, relpath: &str) -> anyhow::Result<Option<Vec<u8>>> {
        let path = safe_relpath(&self.base_dir, relpath)?;
        match std::fs::read(&path) {
            Ok(bytes) => {
                // The bytes are in hand for the caller's sake anyway, so the
                // digest costs one hash and nothing else. See `memoize_digest`.
                self.memoize_digest(relpath, &path, &bytes);
                Ok(Some(bytes))
            }
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
            Err(e) => Err(e.into()),
        }
    }

    /// **Durably** store `data` at a validated relpath (write → fsync → rename →
    /// fsync the directory).
    ///
    /// # Atomic is not the same as durable, and this used to conflate them
    ///
    /// The previous shape was `fs::write(tmp)` + `fs::rename(tmp, path)` with no
    /// `fsync` anywhere. That is atomic **against a concurrent reader** — nobody
    /// ever observes a half-written artifact — and it was documented as
    /// "atomically store", which is why the gap read as finished. It is not durable
    /// against **power loss**: both the file's data blocks and the rename itself can
    /// still be nothing but page cache when the machine dies, and then the artifact
    /// the door already answered `2xx` for is simply gone.
    ///
    /// MEASURED 2026-08-15 on the installed appliance (`installer_iso_fleet_kvm::
    /// served_content_survives_a_power_cycle`): boot 1 accepted a 4096-byte PUT with
    /// a 2xx; the VM was power-cycled; boot 2 came up, served, and 404'd the blob
    /// for 90 s. Scanning the 64 GiB raw disk afterwards found ZERO occurrences of
    /// the artifact's filename, and two of `identity.pub` — so this was not a slow
    /// boot, not a race in the prober, and not a reformatted data home. The
    /// ceremony's write had landed and the registry's had not. An artifact server
    /// whose pitch is immutable, auditable, content-addressed storage must not
    /// acknowledge a write it can still lose; ack-before-durable is a correctness
    /// bug here, not a tuning choice.
    ///
    /// # Why THREE fsyncs
    ///
    /// `sync_all` on the temp file makes the BYTES durable. It says nothing about
    /// the directory entry, and the entry is created by the `rename` — so a crash
    /// between the two leaves durable data no name points at. On FAT the entry is
    /// only landed by fsync'ing the file AT ITS FINAL NAME (`fat_file_fsync`
    /// writes the dirent of the fd being synced — syncing the temp name syncs an
    /// entry the rename throws away; measured 2026-08-17, see step 3). fsync'ing
    /// the parent directory after that covers the filesystems where the rename
    /// lives in the directory instead. Dropping any of them leaves a window,
    /// which is why they are not separable and live in one writer: every raw PUT
    /// and every raw copy funnels through this function.
    fn write_relpath(&self, relpath: &str, data: &[u8]) -> anyhow::Result<()> {
        self.write_relpath_from(relpath, &mut std::io::Cursor::new(data))
    }

    /// The write path proper, fed from a reader: the blob is written to the
    /// scratch file AS IT ARRIVES through a bounded buffer and hashed on the
    /// way past, so a 1 GiB upload costs 1 MiB of this process's memory and one
    /// pass over the bytes. The durability chain (steps 1-4 below) is unchanged.
    /// A read error from `body` is the door refusing the upload (over the cap,
    /// or a digest that did not match): the scratch file is unlinked and the
    /// final name is never touched.
    fn write_relpath_from(
        &self,
        relpath: &str,
        body: &mut dyn std::io::Read,
    ) -> anyhow::Result<()> {
        use std::io::Write as _;

        let path = safe_relpath(&self.base_dir, relpath)?;
        let parent = path
            .parent()
            .ok_or_else(|| anyhow::anyhow!("raw artifact path has no parent directory"))?;
        std::fs::create_dir_all(parent)?;
        // Unique temp name per concurrent write (PID + a process-wide counter) so
        // two simultaneous puts into one dir can't race the same `.tmp`.
        static TMP_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
        let seq = TMP_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let tmp = parent.join(format!(".{}.{}.tmp", std::process::id(), seq));

        // 1. the BYTES, durable before anything can name them.
        let hex = {
            let mut f = std::fs::File::create(&tmp)?;
            let mut hasher = <sha2::Sha256 as sha2::Digest>::new();
            let mut buf = vec![0u8; STREAM_BUF_BYTES];
            let filled = (|| -> anyhow::Result<()> {
                loop {
                    let n = body.read(&mut buf)?;
                    if n == 0 {
                        break;
                    }
                    sha2::Digest::update(&mut hasher, &buf[..n]);
                    f.write_all(&buf[..n])?;
                }
                f.sync_all()?;
                Ok(())
            })();
            if let Err(e) = filled {
                drop(f);
                let _ = std::fs::remove_file(&tmp);
                return Err(e);
            }
            hex::encode(sha2::Digest::finalize(hasher))
        };
        // 2. the NAME, atomically.
        if let Err(e) = std::fs::rename(&tmp, &path) {
            // Never leave the scratch file behind on a failed publish.
            let _ = std::fs::remove_file(&tmp);
            return Err(e.into());
        }
        // 3. the FILE AT ITS FINAL NAME, durable — and this one is MANDATORY.
        //
        //    MEASURED 2026-08-17, second round of the power-cycle proof, with a
        //    FRESHLY packed binary carrying steps 1–2 + the best-effort step 4:
        //    the blob's 4096 bytes WERE in the raw disk image (17 pattern hits at
        //    ~17.26 GB — step 1 did its job) and the GET still 404'd for 90 s
        //    after the reboot. The bytes survived; the NAME did not. On FAT a
        //    file's metadata IS its directory entry, and the kernel's
        //    `fat_file_fsync` writes the dirent OF THE FD BEING SYNCED — step 1
        //    synced the TEMP name's entry, which the rename then discarded in
        //    page cache. Syncing the file at its FINAL name is what lands the
        //    real entry (and on ext4/xfs it is a correct, cheap belt over the
        //    same window). Its failure is a real "your write is not durable yet"
        //    and maps to a 5xx — unlike step 4, this is fsync on a plain FILE fd,
        //    which every filesystem the data home can be implements.
        {
            let f = std::fs::File::open(&path)?;
            f.sync_all()?;
        }
        // 4. the ANCESTOR DIRECTORY CHAIN, durable — every level, up to the
        //    filesystem root, and MANDATORY.
        //
        //    MEASURED 2026-08-17, third round of the power-cycle proof, with
        //    steps 1–3 all in the driven binary: the blob's bytes were on disk
        //    AND the crash-surviving FAT tree had NO `store/` AT ALL — not a
        //    missing file, a missing SUBTREE. `create_dir_all` had made
        //    `store/bundles/persist` in page cache; syncing the file (step 3)
        //    flushes the file's OWN dirent, but `store`'s entry lives in the
        //    data home's ROOT directory, `bundles`' in `store`, and so on — and
        //    none of those clusters were ever flushed, so the power cut orphaned
        //    the whole tree and boot 2 honestly served 404s over durable bytes
        //    no name could reach.
        //
        //    So: fsync every ancestor from the file's parent upward. The walk is
        //    bounded by path depth and stops at the root; crossing above the
        //    mount point costs a few no-op fsyncs on the initramfs and is
        //    cheaper than knowing where the mount is. This used to be ONE
        //    best-effort fsync of the immediate parent, argued as "failing the
        //    request costs the artifact" — the measurement inverted that: NOT
        //    failing the request costs the artifact while answering 2xx, which
        //    is the exact lie this function exists to not tell. Every
        //    filesystem the data home can be (vfat P3, ext4/xfs dev hosts,
        //    tmpfs CI) implements fsync on a directory fd.
        for dir in path.ancestors().skip(1) {
            if dir.as_os_str().is_empty() {
                break;
            }
            let d = std::fs::File::open(dir).with_context(|| {
                format!("open ancestor dir {} to sync the rename", dir.display())
            })?;
            d.sync_all().with_context(|| {
                format!(
                    "fsync ancestor dir {} — without it the artifact's name can \
                     evaporate on power loss while its bytes survive",
                    dir.display()
                )
            })?;
        }
        // The bytes just written are the bytes on disk, and they are in hand —
        // so the digest is free here in the only sense that matters: no second
        // read. Memoized AFTER the durability chain, so a blob whose name never
        // landed never leaves a digest claiming it did.
        self.memoize_digest_hex(relpath, &path, hex);
        Ok(())
    }

    /// Unlink the file at a validated relpath. `Ok(true)` if a file was removed,
    /// `Ok(false)` if there was nothing there (operator DELETE is idempotent-ish:
    /// the caller maps absence to 404).
    fn remove_relpath(&self, relpath: &str) -> anyhow::Result<bool> {
        let path = safe_relpath(&self.base_dir, relpath)?;
        match std::fs::remove_file(&path) {
            Ok(()) => {
                // A later blob at this path must never inherit the dead one's
                // digest. The `(len, mtime)` stamp would almost always catch it;
                // "almost always" is not a guarantee, and an unlink knows.
                self.forget_digest(relpath);
                Ok(true)
            }
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
            Err(e) => Err(e.into()),
        }
    }

    /// The repo-name-stripped, query-stripped relpath of an HTTP suburl, or
    /// `None` if the first segment is not this repo (never our route).
    fn http_relpath(&self, suburl: &str) -> Option<String> {
        let path = suburl.split('?').next().unwrap_or(suburl);
        let mut segs = path
            .trim_start_matches('/')
            .split('/')
            .filter(|s| !s.is_empty());
        match segs.next() {
            Some(first) if first == self.name => {
                let rest: Vec<&str> = segs.collect();
                if rest.is_empty() {
                    None
                } else {
                    Some(rest.join("/"))
                }
            }
            _ => None,
        }
    }

    /// Recursively collect stored files as `(relpath, size, modified)` under
    /// `base_dir`, capped at `cap`, optionally filtered to relpaths CONTAINING
    /// `filter`.
    ///
    /// The modification time is carried out of the `metadata` call the size
    /// already needed — no extra syscall — and is `None` when the platform or
    /// the filesystem does not answer, because a listing says "I do not know"
    /// rather than inventing a time.
    #[allow(clippy::type_complexity)]
    fn walk(
        &self,
        filter: Option<&str>,
        cap: usize,
    ) -> anyhow::Result<Vec<(String, u64, Option<std::time::SystemTime>)>> {
        let mut out = Vec::new();
        // Stack of directories still to visit (relative-prefix, absolute-path).
        let mut stack: Vec<(String, PathBuf)> = vec![(String::new(), self.base_dir.clone())];
        while let Some((prefix, dir)) = stack.pop() {
            let iter = match std::fs::read_dir(&dir) {
                Ok(it) => it,
                // A missing base_dir (nothing stored yet) is an empty listing.
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
                Err(e) => return Err(e.into()),
            };
            for entry in iter {
                let entry = entry?;
                let file_name = match entry.file_name().into_string() {
                    Ok(s) => s,
                    Err(_) => continue, // non-UTF-8 name: skip gracefully
                };
                // Skip in-flight `.<pid>.<seq>.tmp` staging files.
                if file_name.starts_with('.') && file_name.ends_with(".tmp") {
                    continue;
                }
                let rel = if prefix.is_empty() {
                    file_name.clone()
                } else {
                    format!("{prefix}/{file_name}")
                };
                let ft = entry.file_type()?;
                if ft.is_dir() {
                    stack.push((rel, entry.path()));
                } else if ft.is_file() && filter.map(|f| rel.contains(f)).unwrap_or(true) {
                    let meta = entry.metadata().ok();
                    let size = meta.as_ref().map(|m| m.len()).unwrap_or(0);
                    let modified = meta.and_then(|m| m.modified().ok());
                    out.push((rel, size, modified));
                    if out.len() >= cap {
                        return Ok(out);
                    }
                }
            }
        }
        Ok(out)
    }
}

impl RepositoryBackendTrait for RawBackend {
    fn name(&self) -> &str {
        &self.name
    }

    fn format(&self) -> ArtifactFormat {
        ArtifactFormat::Raw
    }

    fn is_writable(&self) -> bool {
        true
    }

    fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<traits::Bytes>> {
        self.read_relpath(&Self::id_to_relpath(id))
            .map(|o| o.map(traits::Bytes::from))
    }

    fn put(&self, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()> {
        self.write_relpath(&Self::id_to_relpath(id), data)
    }

    /// The streamed write: the blob goes to disk as it arrives, through one
    /// bounded buffer, hashed on the way past. Same durability chain as `put`.
    fn put_from_reader(
        &self,
        id: &ArtifactId,
        body: &mut dyn std::io::Read,
        _size_hint: Option<u64>,
    ) -> anyhow::Result<()> {
        self.write_relpath_from(&Self::id_to_relpath(id), body)
    }

    /// The streamed read: the file is opened once and handed out `chunk_bytes`
    /// at a time, so the first piece leaves before the last is read and a
    /// door serving a 1 GiB blob holds one chunk of it.
    fn fetch_chunks(
        &self,
        id: &ArtifactId,
        chunk_bytes: usize,
    ) -> anyhow::Result<Option<traits::ChunkIter>> {
        let path = safe_relpath(&self.base_dir, &Self::id_to_relpath(id))?;
        let file = match std::fs::File::open(&path) {
            Ok(f) => f,
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
            Err(e) => return Err(e.into()),
        };
        Ok(Some(traits::backend::file_chunks(
            file,
            chunk_bytes.clamp(1, STREAM_BUF_BYTES),
        )))
    }

    /// Everything: a real listing, streamed reads and writes off the disk,
    /// a total path mapping (`raw` is path-addressed) and a verified delete.
    /// No archive — the store is loose files.
    fn caps(&self) -> traits::Caps {
        traits::Caps {
            archive: false,
            group: false,
            ..traits::Caps::ALL
        }
    }

    /// `PUT`/`POST` stream straight into `write_relpath_from`; every other
    /// write verb carries no body raw reads, so it is answered by the whole-body
    /// door with an empty one.
    fn handle_http2_upload(
        &self,
        method: &str,
        suburl: &str,
        body: &mut dyn std::io::Read,
        _size_hint: Option<u64>,
    ) -> anyhow::Result<Option<traits::HttpReply>> {
        match method {
            "PUT" | "POST" => {
                let relpath = match self.http_relpath(suburl) {
                    Some(r) => r,
                    None => return Ok(Some((404, Vec::new(), b"Not found".to_vec()))),
                };
                self.write_relpath_from(&relpath, body)?;
                crate::grpc::functional_status("holger-raw/http", "raw_put", true, &relpath);
                let octet = vec![(
                    "Content-Type".to_string(),
                    "application/octet-stream".to_string(),
                )];
                Ok(Some((201, octet, Vec::new())))
            }
            _ => self.handle_http2_request(method, suburl, &[]).map(Some),
        }
    }

    /// Reclaim one stored blob — the retention/GC executor's safe unlink. Read
    /// the on-disk bytes, recompute their SHA-256, and remove **only** if it
    /// matches `expected_content_id` (the digest the executor planned to drop); a
    /// mismatch means the store changed under the plan → refuse. A missing file
    /// is an error (nothing to reclaim). Mirrors the loose-file rust backend.
    fn delete_artifact(&self, id: &ArtifactId, expected_content_id: &str) -> anyhow::Result<()> {
        let relpath = Self::id_to_relpath(id);
        let bytes = match self.read_relpath(&relpath)? {
            Some(b) => b,
            None => anyhow::bail!("raw artifact {relpath:?} not present — nothing to reclaim"),
        };
        let actual = sha256_hex(&bytes);
        if actual != expected_content_id {
            anyhow::bail!(
                "refusing to delete raw {relpath:?}: on-disk content id {actual} != expected \
                 {expected_content_id} — the store changed under the retention plan"
            );
        }
        self.remove_relpath(&relpath)?;
        Ok(())
    }

    /// Every stored file, reported as `ArtifactId { namespace: None, name:
    /// <relpath>, version: "" }` so the id round-trips back to the same path.
    /// `name_filter` is a substring match on the relpath; `limit == 0` falls back
    /// to a sane cap (1024). A missing `base_dir` yields `[]`, not an error.
    fn list(&self, name_filter: Option<&str>, limit: usize) -> anyhow::Result<Vec<ArtifactEntry>> {
        const DEFAULT_CAP: usize = 1024;
        let cap = if limit == 0 { DEFAULT_CAP } else { limit };
        let files = self.walk(name_filter, cap)?;
        Ok(files
            .into_iter()
            .map(|(relpath, size, modified)| {
                // `published_unix_ms`: the stored file's mtime, already in hand
                // from the walk's own `metadata` call.
                //
                // `sha256`: a memo PEEK — the digest of this blob IF this process
                // has already had its bytes in hand (a write, or a read it served)
                // AND the `(len, mtime)` stamp still matches. Otherwise `None`.
                //
                // ⚠ The listing still does NOT hash. That is the invariant the
                // old comment here was protecting and it is kept: a raw blob has
                // no content-addressed name and no sidecar digest, and hashing
                // the store on every listing is O(bytes of the repository). What
                // changed is that "holger has never seen these bytes" and "holger
                // refuses to say" stopped being the same answer — because a
                // mirror asking "did this change?" against an upstream that
                // SERVED it the bytes deserves the digest it already paid for.
                // Without this, `unchanged` was unreachable against a raw repo
                // and a downstream re-fetched the whole repository every tick.
                let digest = self.peek_digest(&relpath, size, modified);
                ArtifactEntry::unknown_provenance(
                    ArtifactId {
                        namespace: None,
                        name: relpath,
                        version: String::new(),
                    },
                    size as i64,
                    "application/octet-stream",
                )
                .with_published_system_time(modified)
                .with_sha256(digest)
            })
            .collect())
    }

    /// Raw HTTP door: `GET`/`HEAD` reads, `PUT`/`POST` writes (the checksum-deploy
    /// gate has already verified the body at the gateway), `DELETE` removes.
    /// The path after the repo name is the blob address.
    fn handle_http2_request(
        &self,
        method: &str,
        suburl: &str,
        body: &[u8],
    ) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
        let relpath = match self.http_relpath(suburl) {
            Some(r) => r,
            None => return Ok((404, Vec::new(), b"Not found".to_vec())),
        };
        let octet = || {
            vec![(
                "Content-Type".to_string(),
                "application/octet-stream".to_string(),
            )]
        };
        match method {
            // A HEAD asks the size: the file's metadata answers it (HOL-HEAD-1).
            "HEAD" => match std::fs::metadata(safe_relpath(&self.base_dir, &relpath)?) {
                Ok(m) if m.is_file() => {
                    let mut h = octet();
                    h.push(("Content-Length".to_string(), m.len().to_string()));
                    Ok((200, h, Vec::new()))
                }
                _ => Ok(traits::backend::head_of((
                    404,
                    Vec::new(),
                    b"Not found".to_vec(),
                ))),
            },
            "GET" => match self.read_relpath(&relpath) {
                Ok(Some(bytes)) => {
                    let ok = true;
                    crate::grpc::functional_status("holger-raw/http", "raw_get", ok, &relpath);
                    Ok((200, octet(), bytes))
                }
                Ok(None) => {
                    crate::grpc::functional_status(
                        "holger-raw/http",
                        "raw_get_miss",
                        true,
                        &relpath,
                    );
                    Ok((404, Vec::new(), b"Not found".to_vec()))
                }
                Err(e) => Err(e),
            },
            "PUT" | "POST" => {
                self.write_relpath(&relpath, body)?;
                crate::grpc::functional_status("holger-raw/http", "raw_put", true, &relpath);
                Ok((201, octet(), Vec::new()))
            }
            "DELETE" => {
                let removed = self.remove_relpath(&relpath)?;
                crate::grpc::functional_status("holger-raw/http", "raw_delete", removed, &relpath);
                if removed {
                    Ok((204, Vec::new(), Vec::new()))
                } else {
                    Ok((404, Vec::new(), b"Not found".to_vec()))
                }
            }
            other => Ok((
                405,
                vec![("Allow".into(), "GET, HEAD, PUT, POST, DELETE".into())],
                format!("method {other} not allowed on a raw repository").into_bytes(),
            )),
        }
    }

    /// A raw repo's HTTP path IS its coordinate: `/<repo>/<relpath>` maps back to
    /// `ArtifactId { namespace: None, name: <relpath>, version: "" }` — exactly the
    /// coordinate [`list`](RawBackend::list) reports and [`fetch`](RawBackend::fetch)
    /// resolves. This lets the serve-time quarantine gate look up a raw blob's
    /// properties before serving it over HTTP.
    /// The mapping is total, so a `None` genuinely means "no artifact here".
    fn coordinate_for_path(&self, suburl: &str) -> anyhow::Result<Option<ArtifactId>> {
        Ok(self.http_relpath(suburl).map(|relpath| ArtifactId {
            namespace: None,
            name: relpath,
            version: String::new(),
        }))
    }
}

/// A stored blob read `chunk` bytes at a time — the iterator behind
/// [`RawBackend::fetch_chunks`]. One `read` per piece, one allocation per
/// piece (the piece itself, which the door hands to the wire and drops), no
/// buffer holding more than one piece.
#[cfg(test)]
mod tests {
    use super::*;

    fn tmpdir() -> PathBuf {
        let p = std::env::temp_dir().join(format!(
            "holger-raw-test-{}-{}",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        std::fs::create_dir_all(&p).unwrap();
        p
    }

    fn id(ns: Option<&str>, name: &str, version: &str) -> ArtifactId {
        ArtifactId {
            namespace: ns.map(|s| s.to_string()),
            name: name.into(),
            version: version.into(),
        }
    }

    #[test]
    fn put_then_fetch_round_trip() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        let coord = id(Some("firmware"), "device.bin", "1.2.0");
        repo.put(&coord, b"\x00binary payload\xff").unwrap();
        // Byte-identical read-back.
        assert_eq!(
            repo.fetch(&coord).unwrap().as_deref(),
            Some(b"\x00binary payload\xff".as_slice())
        );
        // The path is the join of the non-empty coordinate parts.
        assert!(dir
            .join("firmware")
            .join("device.bin")
            .join("1.2.0")
            .is_file());
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn fetch_missing_returns_none() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        assert_eq!(repo.fetch(&id(None, "ghost", "")).unwrap(), None);
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn put_and_fetch_reject_path_traversal() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        // A `..` segment must be refused on BOTH the write and read paths — never
        // silently written outside base_dir, never read from outside it.
        let hostile = id(Some(".."), "etc", "passwd");
        assert!(
            repo.put(&hostile, b"pwn").is_err(),
            "traversal put must be refused"
        );
        assert!(
            repo.fetch(&hostile).is_err(),
            "traversal fetch must be refused"
        );
        // An empty coordinate (no segments) is likewise rejected.
        assert!(
            repo.put(&id(None, "", ""), b"x").is_err(),
            "empty path refused"
        );
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn http_put_get_delete_round_trip() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        // PUT stores.
        let (s, _h, _b) = repo
            .handle_http2_request("PUT", "/raw/reports/2024/q1.pdf", b"PDF-BYTES")
            .unwrap();
        assert_eq!(s, 201, "PUT creates");
        // GET reads it back byte-identical.
        let (s, _h, body) = repo
            .handle_http2_request("GET", "/raw/reports/2024/q1.pdf", &[])
            .unwrap();
        assert_eq!(s, 200);
        assert_eq!(body, b"PDF-BYTES");
        // HEAD is 200 with no body.
        let (s, _h, body) = repo
            .handle_http2_request("HEAD", "/raw/reports/2024/q1.pdf", &[])
            .unwrap();
        assert_eq!(s, 200);
        assert!(body.is_empty(), "HEAD carries no body");
        // DELETE removes it; a second GET is 404.
        let (s, _h, _b) = repo
            .handle_http2_request("DELETE", "/raw/reports/2024/q1.pdf", &[])
            .unwrap();
        assert_eq!(s, 204);
        let (s, _h, _b) = repo
            .handle_http2_request("GET", "/raw/reports/2024/q1.pdf", &[])
            .unwrap();
        assert_eq!(s, 404, "gone after delete");
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn http_get_missing_is_404_and_wrong_repo_is_404() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        let (s, _h, _b) = repo
            .handle_http2_request("GET", "/raw/nope.bin", &[])
            .unwrap();
        assert_eq!(s, 404, "absent blob → 404");
        // A path whose first segment is not this repo never resolves here.
        let (s, _h, _b) = repo
            .handle_http2_request("GET", "/other-repo/x.bin", &[])
            .unwrap();
        assert_eq!(s, 404, "not our route → 404");
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn http_rejects_traversal_in_path() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        // `..` in the HTTP path must error out of safe_relpath, not escape.
        assert!(
            repo.handle_http2_request("PUT", "/raw/../escape.bin", b"pwn")
                .is_err(),
            "HTTP traversal must be refused"
        );
        std::fs::remove_dir_all(&dir).ok();
    }

    /// ★ **A raw listing stamps a time, and never hashes a blob it has not handled.**
    ///
    /// The time is free — it comes out of the `metadata` call the walk already
    /// makes for the size. The digest is NOT free: a raw blob has no
    /// content-addressed name and no sidecar, so answering from disk would mean
    /// reading every file in the store on every listing.
    ///
    /// So this proves the half that must never change: a blob that appeared in
    /// the store WITHOUT passing through holger's door is listed with no digest.
    /// The listing did not open it, and it does not pretend otherwise. RED if
    /// somebody "helpfully" makes the listing hash its store: this test would be
    /// the only thing standing between a listing and O(bytes of the repository).
    #[test]
    fn a_raw_listing_never_hashes_a_blob_it_has_not_handled() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        let before = std::time::SystemTime::now() - std::time::Duration::from_secs(5);

        // Straight onto the disk, behind holger's back — no put, no fetch.
        std::fs::create_dir_all(dir.join("a/b")).unwrap();
        std::fs::write(dir.join("a/b/one.bin"), b"111").unwrap();

        let listed = repo.list(None, 0).unwrap();
        assert_eq!(listed.len(), 1);
        let e = &listed[0];

        let ms = e
            .published_unix_ms
            .expect("a loose file has a modification time");
        let floor = traits::system_time_to_unix_ms(before).expect("after the epoch");
        assert!(
            ms >= floor,
            "the stamp is the file's own mtime, not something older: {ms} < {floor}"
        );
        assert!(ms > 0, "never a fabricated zero");

        assert_eq!(
            e.sha256, None,
            "the listing must not have opened this file — a digest here means the walk hashed \
             the store, which is O(bytes of the whole repository)"
        );
        std::fs::remove_dir_all(&dir).ok();
    }

    /// ★ **A blob holger has already had in its hands IS listed with its digest.**
    ///
    /// This is the half that makes `unchanged` reachable. Before it, `raw`
    /// published a time and no digest, so a downstream mirror comparing a
    /// listing against what it already held could never conclude "nothing
    /// changed" — and re-fetched the entire repository on every tick. The
    /// upstream had already paid for the hash (it wrote the bytes, or it served
    /// them); it was simply throwing the answer away.
    ///
    /// Both doors are proven, because a mirror's upstream reaches blobs through
    /// both: the one it WROTE, and the one it only ever READ.
    #[test]
    fn a_blob_holger_has_handled_is_listed_with_the_digest_it_already_paid_for() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());

        // Door 1: written through holger.
        repo.put(&id(Some("a/b"), "written.bin", ""), b"111")
            .unwrap();

        // Door 2: appeared behind holger's back, then SERVED by it once.
        std::fs::write(dir.join("served.bin"), b"2222").unwrap();
        let before_serving = repo.list(None, 0).unwrap();
        let served_first = before_serving
            .iter()
            .find(|e| e.id.name == "served.bin")
            .expect("listed");
        assert_eq!(
            served_first.sha256, None,
            "not yet — holger has not touched these bytes"
        );
        repo.fetch(&id(None, "served.bin", ""))
            .unwrap()
            .expect("present");

        let listed = repo.list(None, 0).unwrap();
        let written = listed
            .iter()
            .find(|e| e.id.name == "a/b/written.bin")
            .expect("listed");
        let served = listed
            .iter()
            .find(|e| e.id.name == "served.bin")
            .expect("listed");

        assert_eq!(
            written.sha256.as_deref(),
            Some(sha256_hex(b"111").as_str()),
            "the digest of the bytes the PUT wrote"
        );
        assert_eq!(
            served.sha256.as_deref(),
            Some(sha256_hex(b"2222").as_str()),
            "the digest of the bytes the FETCH read"
        );
        std::fs::remove_dir_all(&dir).ok();
    }

    /// ★ **A memo whose bytes changed underneath it is not served.**
    ///
    /// The memo is keyed by `(len, mtime)` precisely because a raw blob is
    /// MUTABLE — this is the one way the pattern differs from `file-rust`, whose
    /// crate files cannot change. An out-of-band overwrite must make the listing
    /// fall back to the honest `None`, never answer with the digest of bytes
    /// that are no longer there. A stale digest would be worse than no digest:
    /// it would tell a mirror "unchanged" about a blob that DID change, and the
    /// mirror would skip it forever.
    #[test]
    fn a_digest_whose_bytes_changed_underneath_it_is_not_served() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        repo.put(&id(None, "mutable.bin", ""), b"111").unwrap();
        assert_eq!(
            repo.list(None, 0).unwrap()[0].sha256.as_deref(),
            Some(sha256_hex(b"111").as_str())
        );

        // Behind holger's back, a different length at the same path.
        std::fs::write(dir.join("mutable.bin"), b"22222222").unwrap();

        assert_eq!(
            repo.list(None, 0).unwrap()[0].sha256,
            None,
            "the stamp no longer matches, so the memo must not answer — an honest unknown, \
             never the digest of bytes that are gone"
        );

        // And one read restores an HONEST answer, for the new bytes.
        repo.fetch(&id(None, "mutable.bin", ""))
            .unwrap()
            .expect("present");
        assert_eq!(
            repo.list(None, 0).unwrap()[0].sha256.as_deref(),
            Some(sha256_hex(b"22222222").as_str())
        );
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn list_enumerates_nested_and_round_trips_to_fetch() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        repo.put(&id(Some("a/b"), "one.bin", ""), b"111").unwrap();
        repo.put(&id(None, "two.bin", ""), b"22").unwrap();
        repo.put(&id(Some("a"), "three.txt", ""), b"3333").unwrap();
        let listed = repo.list(None, 0).unwrap();
        assert_eq!(listed.len(), 3, "all three nested blobs enumerated");
        // Every listed id round-trips: fetch by the listed coordinate resolves to
        // the same bytes (this is what retention/search/replication rely on).
        for e in &listed {
            let bytes = repo.fetch(&e.id).unwrap();
            assert!(bytes.is_some(), "listed id {:?} must re-fetch", e.id.name);
            assert_eq!(bytes.unwrap().len() as i64, e.size_bytes, "size matches");
        }
        // The nested blob's relpath is carried whole in `name`.
        assert!(
            listed.iter().any(|e| e.id.name == "a/b/one.bin"),
            "nested relpath preserved in the listed coordinate"
        );
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn list_filters_by_substring_and_respects_limit() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        repo.put(&id(None, "alpha.bin", ""), b"a").unwrap();
        repo.put(&id(None, "beta.bin", ""), b"b").unwrap();
        repo.put(&id(None, "alpine.txt", ""), b"c").unwrap();
        // Substring "alp" matches alpha + alpine, not beta.
        let matched = repo.list(Some("alp"), 0).unwrap();
        assert_eq!(matched.len(), 2, "substring filter selects alpha + alpine");
        assert!(matched.iter().all(|e| e.id.name.contains("alp")));
        // limit caps the count.
        let capped = repo.list(None, 1).unwrap();
        assert_eq!(capped.len(), 1, "limit=1 returns exactly one");
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn delete_artifact_verifies_content_id_before_unlink() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        let coord = id(None, "keep.bin", "");
        repo.put(&coord, b"real-bytes").unwrap();
        let good = sha256_hex(b"real-bytes");
        // RED-when-broken: a WRONG content id must NOT unlink (safe GC contract).
        let wrong = sha256_hex(b"different");
        assert!(
            repo.delete_artifact(&coord, &wrong).is_err(),
            "mismatched content id must refuse to delete"
        );
        assert!(
            repo.fetch(&coord).unwrap().is_some(),
            "blob survives a refused delete"
        );
        // The correct content id unlinks.
        repo.delete_artifact(&coord, &good).unwrap();
        assert_eq!(
            repo.fetch(&coord).unwrap(),
            None,
            "blob gone after verified delete"
        );
        // Deleting an absent blob is an error (nothing to reclaim).
        assert!(
            repo.delete_artifact(&coord, &good).is_err(),
            "absent → error"
        );
        std::fs::remove_dir_all(&dir).ok();
    }

    /// A raw repository really enumerates (`list` walks the files), so it must
    /// SAY so: `supports_listing` defaulted to false here, and a counter — or a
    /// purge, which refuses to call a repository it cannot enumerate empty —
    /// read a real listing as "cannot say".
    #[test]
    fn a_raw_repository_says_it_enumerates() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        assert!(repo.caps().listing, "raw overrides list, so it enumerates");
        traits::backend::conformance::check(&repo).unwrap();
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn writable_and_raw_format() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        assert!(repo.is_writable(), "raw is a writable hosted repo");
        assert_eq!(repo.format(), ArtifactFormat::Raw);
        assert_eq!(repo.name(), "raw");
        std::fs::remove_dir_all(&dir).ok();
    }

    /// **Every hostile segment shape is refused on every door.** `..` is only the
    /// obvious one: an absolute path (a leading `/` = an empty first segment), an
    /// empty segment from a doubled slash, a lone `.`, a Windows separator and an
    /// embedded NUL must all be rejected — on `put`, on `fetch`, and on the safe
    /// delete — and the store must be left completely empty afterwards.
    #[test]
    fn every_hostile_path_shape_is_refused_and_nothing_is_written() {
        let dir = tempfile::tempdir().unwrap();
        let repo = RawBackend::new("raw".into(), dir.path().to_path_buf());

        let hostile = [
            "..",
            ".",
            "",
            "/etc/passwd", // absolute → an empty leading segment
            "a//b",        // a doubled separator → an empty segment
            "a/../../etc", // traversal below the root
            "trailing/",   // an empty trailing segment
            "back\\slash", // a Windows separator inside a segment
            "nul\0byte",   // an embedded NUL
        ];
        for path in hostile {
            let coord = id(None, path, "");
            assert!(
                repo.put(&coord, b"pwn").is_err(),
                "put must refuse {path:?}"
            );
            assert!(repo.fetch(&coord).is_err(), "fetch must refuse {path:?}");
            assert!(
                repo.delete_artifact(&coord, &sha256_hex(b"pwn")).is_err(),
                "delete must refuse {path:?}"
            );
        }
        assert_eq!(
            std::fs::read_dir(dir.path()).unwrap().count(),
            0,
            "a refused path writes nothing at all"
        );
        assert!(repo.list(None, 0).unwrap().is_empty());
    }

    /// A zero-byte blob is a REAL blob: it stores, it lists, `fetch` returns empty
    /// bytes (never `None`), HTTP serves it as a 200 with an empty body, and the
    /// safe delete verifies the SHA-256 of nothing. RED-when-broken if emptiness is
    /// ever collapsed into absence — an empty marker file would become unservable
    /// and unreclaimable.
    #[test]
    fn a_zero_byte_blob_is_stored_served_and_reclaimed_like_any_other() {
        let dir = tempfile::tempdir().unwrap();
        let repo = RawBackend::new("raw".into(), dir.path().to_path_buf());
        let coord = id(None, "marker", "");

        repo.put(&coord, b"").unwrap();
        assert_eq!(
            repo.fetch(&coord).unwrap(),
            Some(traits::Bytes::new()),
            "empty ≠ absent"
        );
        let listed = repo.list(None, 0).unwrap();
        assert_eq!(listed.len(), 1);
        assert_eq!(listed[0].size_bytes, 0);

        let (status, _h, body) = repo
            .handle_http2_request("GET", "/raw/marker", &[])
            .unwrap();
        assert_eq!(status, 200, "an empty blob is found, not a 404");
        assert!(body.is_empty());

        repo.delete_artifact(&coord, &sha256_hex(b"")).unwrap();
        assert_eq!(repo.fetch(&coord).unwrap(), None, "and it reclaims cleanly");
    }

    /// Overwriting a blob replaces it WHOLLY — a shorter body must not leave the
    /// tail of the longer one behind. That is what the temp-then-rename buys, and
    /// only a shrinking overwrite can catch its absence.
    #[test]
    fn a_shorter_overwrite_leaves_no_tail_of_the_previous_blob() {
        let dir = tempfile::tempdir().unwrap();
        let repo = RawBackend::new("raw".into(), dir.path().to_path_buf());
        let coord = id(Some("firmware"), "device.bin", "1.0.0");

        repo.put(&coord, &[b'A'; 8192]).unwrap();
        repo.put(&coord, b"v2").unwrap();
        assert_eq!(
            repo.fetch(&coord).unwrap().as_deref(),
            Some(b"v2".as_slice()),
            "the new bytes are the WHOLE file"
        );
        // Same through the HTTP door.
        repo.handle_http2_request("PUT", "/raw/firmware/device.bin/1.0.0", b"v3")
            .unwrap();
        let (_s, _h, body) = repo
            .handle_http2_request("GET", "/raw/firmware/device.bin/1.0.0", &[])
            .unwrap();
        assert_eq!(body, b"v3");
    }

    /// An in-flight staging file (`.<pid>.<seq>.tmp` — what a crash mid-`put`
    /// leaves) is not a blob: it must not appear in a listing, because a listing is
    /// what retention, replication and search act on. A file that merely starts
    /// with a dot IS a blob and stays visible — the skip is narrow on purpose.
    #[test]
    fn a_leftover_staging_file_is_not_listed_as_a_blob() {
        let dir = tempfile::tempdir().unwrap();
        let repo = RawBackend::new("raw".into(), dir.path().to_path_buf());
        repo.put(&id(None, "real.bin", ""), b"real").unwrap();
        std::fs::write(dir.path().join(".4711.0.tmp"), b"half-written").unwrap();
        std::fs::write(dir.path().join(".hidden"), b"a dotfile is still a blob").unwrap();

        let names: Vec<String> = repo
            .list(None, 0)
            .unwrap()
            .into_iter()
            .map(|e| e.id.name)
            .collect();
        assert!(names.contains(&"real.bin".to_string()));
        assert!(
            names.contains(&".hidden".to_string()),
            "only staging files are skipped: {names:?}"
        );
        assert!(
            !names.iter().any(|n| n.ends_with(".tmp")),
            "an in-flight staging file must never be listed as an artifact: {names:?}"
        );
    }

    /// **The coordinate ↔ path bijection**, end to end: a blob written through the
    /// HTTP door resolves to the coordinate `coordinate_for_path` reports, that
    /// coordinate is the one `list` reports, and feeding it back to `fetch` returns
    /// the same bytes. Retention, GC, search and replication all rely on exactly
    /// this round-trip — if it breaks, they act on coordinates that resolve nowhere.
    #[test]
    fn an_http_path_a_listed_coordinate_and_a_fetch_all_name_the_same_blob() {
        let dir = tempfile::tempdir().unwrap();
        let repo = RawBackend::new("raw".into(), dir.path().to_path_buf());
        repo.handle_http2_request("PUT", "/raw/reports/2024/q1.pdf", b"PDF")
            .unwrap();

        let from_path = repo
            .coordinate_for_path("/raw/reports/2024/q1.pdf")
            .unwrap()
            .expect("our own route resolves to a coordinate");
        assert_eq!(from_path.name, "reports/2024/q1.pdf");
        assert_eq!(from_path.namespace, None);
        assert_eq!(from_path.version, "");
        assert_eq!(
            repo.fetch(&from_path).unwrap().as_deref(),
            Some(b"PDF".as_slice())
        );

        let listed = repo.list(None, 0).unwrap();
        assert_eq!(listed.len(), 1);
        assert_eq!(
            listed[0].id, from_path,
            "the listed coordinate IS the path coordinate"
        );

        // A path that is not our route yields no coordinate at all.
        assert_eq!(repo.coordinate_for_path("/other-repo/x.bin").unwrap(), None);
        assert_eq!(
            repo.coordinate_for_path("/raw").unwrap(),
            None,
            "the bare repo root is not a blob"
        );
    }

    /// The query string is not part of a blob's address: `?sig=…` (a signed-URL
    /// suffix a client may append) must resolve to the same stored blob, not to a
    /// second one. And a unicode path round-trips through both doors.
    #[test]
    fn the_query_string_is_not_part_of_the_address_and_unicode_paths_round_trip() {
        let dir = tempfile::tempdir().unwrap();
        let repo = RawBackend::new("raw".into(), dir.path().to_path_buf());

        repo.handle_http2_request("PUT", "/raw/räksmörgås/åäö.bin?sig=abc", b"unicode")
            .unwrap();
        let (status, _h, body) = repo
            .handle_http2_request("GET", "/raw/räksmörgås/åäö.bin", &[])
            .unwrap();
        assert_eq!(
            status, 200,
            "the query string was not stored as part of the name"
        );
        assert_eq!(body, b"unicode");

        // And the same blob answers when the query IS present.
        let (status, _h, body) = repo
            .handle_http2_request("GET", "/raw/räksmörgås/åäö.bin?sig=other", &[])
            .unwrap();
        assert_eq!(status, 200);
        assert_eq!(body, b"unicode");

        let listed = repo.list(None, 0).unwrap();
        assert_eq!(listed.len(), 1, "one blob, not one per query string");
        assert_eq!(listed[0].id.name, "räksmörgås/åäö.bin");
    }

    #[test]
    fn method_not_allowed_for_unknown_verb() {
        let dir = tmpdir();
        let repo = RawBackend::new("raw".into(), dir.clone());
        let (s, headers, _b) = repo
            .handle_http2_request("PATCH", "/raw/x.bin", &[])
            .unwrap();
        assert_eq!(s, 405, "PATCH is not a raw verb");
        assert!(
            headers.iter().any(|(k, _)| k == "Allow"),
            "405 advertises the allowed verbs"
        );
        std::fs::remove_dir_all(&dir).ok();
    }
}