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

nordisk/holger

server/repository/znippy-deb/src/lib.rs

//! Debian (APT) repository backend over a read-only znippy archive — the deb
//! sibling of znippy-rpm/conda/go, implementing `RepositoryBackendTrait`.
//!
//! APT fetches the index (`dists/{dist}/Release` + per-component/arch
//! `Packages`) then the `pool/*.deb` bodies. Two tiers answer a request, in
//! order: authoritative path-passthrough (`get_file`) serves package bytes and
//! any mirrored upstream metadata packed into the archive verbatim; when that
//! misses, `synthesize_index` builds the native `Packages`/`Release` from the
//! pool `.deb`s — each stanza's `Filename`/`Size`/`SHA256` computed over the
//! real served bytes so an `[trusted=yes]` apt client can verify. Control fields
//! come from the typed deb view (`as_deb`) when present; otherwise holger parses
//! them NATIVELY out of the served `.deb` bytes — an `ar` + `control.tar.*` read
//! of the real Debian `control` stanza (`control` module) — and only when THAT
//! misses falls back to the `{name}_{ver}_{arch}.deb` filename (a 3-way `_` split,
//! exact since name and version never contain `_`).
//!
//! Gotcha: synthesized indexes are cached because the archive is immutable —
//! without it every `Release` request re-hashes the whole pool, a DoS-
//! amplification vector (same guard the conda plugin documents for
//! `repodata.json`). The cache is keyed on the **derived, bounded identity of the
//! work**, never on the request path: one `Packages` per `(component, arch)` the
//! pool actually contains, and one dist-independent `Release` skeleton. Keying on
//! the path remainder was itself the vector — `dists/<anything>/Release` missed
//! the cache, so an attacker re-ran the whole decompress+SHA-256+control-parse
//! pool walk by varying one path segment. Index coordinates outside
//! [`DebRepoZnippy::pool_dimensions`] 404 before any work. An empty `Packages`
//! synthesizes to `None` → 404, never an empty index.

mod control;
/// The parser of untrusted package bytes, public for holger's fuzz targets (`fuzz/`).
pub use control::control_from_deb;
/// The test builders (feature `fixtures`): the fuzz seeds are made with them.
#[cfg(feature = "fixtures")]
pub use control::tests_support as fixtures;

use anyhow::Result;
use std::collections::{BTreeSet, HashMap};
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};

use traits::znippy_base::{not_found_bytes, ok_bytes};
use traits::{ArtifactFormat, ArtifactId, RepositoryBackendTrait, ZnippyBase};
use znippy_common::ZnippyReader;

/// znippy-backed Debian (APT) repository.
///
/// APT first fetches the index (`Release` + per-component/arch `Packages`),
/// then the packages out of `pool/`:
///   * `GET /{repo}/dists/{dist}/Release`
///   * `GET /{repo}/dists/{dist}/{component}/binary-{arch}/Packages`
///   * `GET /{repo}/pool/{component}/…/{pkg}_{ver}_{arch}.deb`
///
/// **Native protocol (mirrors the conda/go plugins):** the package files are
/// served straight from the archive, and the **index is synthesized from the
/// `.deb` files present** when the archive doesn't already carry it. Each
/// `Packages` stanza is built from the pool filename (`{name}_{ver}_{arch}.deb`)
/// plus the archive byte length and a real **SHA256** of the served bytes, and
/// the `Release` references each synthesized `Packages` by SHA256 + size — so an
/// `apt` client (`[trusted=yes]`) can resolve and verify downloads. If the
/// archive already contains authoritative upstream metadata (a mirrored repo),
/// that is served verbatim (path-passthrough wins); synthesis is the fallback.
///
/// Synthesized indexes are cached (the archive is immutable) so each request
/// doesn't re-hash the whole pool — the same DoS-amplification guard the conda
/// plugin documents for `repodata.json`. The cache is keyed on the DERIVED
/// identity of the work (`(component, arch)` for a `Packages`, nothing at all for
/// the dist-independent `Release` skeleton), so a caller cannot miss it by
/// renaming a path segment.
pub struct DebRepoZnippy {
    /// The typed view (`as_deb`) is the source of the authoritative `control`
    /// stanza (Depends/Maintainer/Description) the filename can't carry.
    base: ZnippyBase,
    /// The `(components, arches)` the pool actually contains — derived once (the
    /// archive is immutable) and the ONLY index coordinates that can be asked for.
    /// It is what bounds `packages_cache` below.
    dims_cache: OnceLock<(BTreeSet<String>, BTreeSet<String>)>,
    /// Synthesized `Packages`, keyed by the derived `(component, arch)`. Only ever
    /// populated for a pair present in [`DebRepoZnippy::pool_dimensions`], so the
    /// map can never grow past the archive's own dimensions.
    packages_cache: Mutex<HashMap<(String, String), Arc<PackagesIndex>>>,
    /// The dist-INDEPENDENT part of `Release`: the `SHA256:` block over every
    /// synthesized `Packages` plus the component/architecture lists. `dists/{dist}`
    /// only names the suite, so this is computed once and the (attacker-chosen)
    /// dist is formatted into it — no cache keyed on the suite name, and no pool
    /// walk per request.
    release_cache: OnceLock<ReleaseParts>,
    /// The rows of the pool's `.deb`s (lane HOL-PERF-2), when the backend is a chain view:
    /// the control stanza, the size and the sha256 the scan worker read at intake.
    rows: Option<Arc<dyn holger_index_rows::RowSource>>,
}

/// One synthesized `Packages` index plus the SHA-256 the `Release` references it
/// by — hashed once, when the index is built.
struct PackagesIndex {
    /// Rendered once, handed out as the same bytes (lane HOL-PERF-2).
    body: traits::Bytes,
    /// `Packages.gz`, written once beside it (lane HOL-PERF-2), and its SHA-256.
    gz: traits::Bytes,
    gz_sha256: String,
    sha256: String,
}

/// The dist-independent body of a synthesized `Release`.
struct ReleaseParts {
    /// `Date:` (RFC 2822, UTC): when this body was built, which is when the set it indexes
    /// was read. apt warns on a Release without one (`Invalid 'Date' entry`), and a signed
    /// Release (HOL-SIGNED-APT-DNF) is checked against it (`not valid yet`).
    date: String,
    /// ` {sha} {size} {component}/binary-{arch}/Packages` lines, in a deterministic order.
    sha_lines: String,
    components: String,
    architectures: String,
}

impl DebRepoZnippy {
    pub fn new(name: String) -> Self {
        Self::over(ZnippyBase::new(name))
    }

    fn over(base: ZnippyBase) -> Self {
        Self {
            base,
            dims_cache: OnceLock::new(),
            packages_cache: Mutex::new(HashMap::new()),
            release_cache: OnceLock::new(),
            rows: None,
        }
    }

    /// Backed by a specific znippy archive file. Holds the open archive so the
    /// typed deb view is built once and reused.
    pub fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> {
        Ok(Self::over(ZnippyBase::with_archive(name, archive_path)?))
    }

    pub fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self {
        Self::over(ZnippyBase::with_reader(name, reader))
    }

    /// Over `reader`, with the rows of its files (lane HOL-PERF-2): the index is rendered
    /// from the rows and reads no package.
    pub fn with_reader_and_rows(
        name: String,
        reader: Arc<dyn ZnippyReader>,
        rows: Arc<dyn holger_index_rows::RowSource>,
    ) -> Self {
        Self {
            rows: Some(rows),
            ..Self::with_reader(name, reader)
        }
    }

    pub fn list_files(&self) -> Vec<String> {
        self.base.list_files()
    }

    pub fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> {
        self.base.get_file(relative_path)
    }

    /// Authoritative `(name, version, arch) -> control` from the typed deb view
    /// (the control tarball the znippy `deb_native` plugin parsed at ingest), keyed
    /// by NEVRA. Empty when the archive carries no deb sub-index (a mirrored repo,
    /// a non-deb placeholder, or `.deb`s whose control codec isn't supported yet) —
    /// the synthesis then falls back to filename-only stanzas, exactly as before.
    fn control_stanzas(&self) -> HashMap<(String, String, String), String> {
        let mut map = HashMap::new();
        if let Some(archive) = self.base.typed() {
            if let Some(view) = archive.as_deb() {
                for (name, version, arch, control) in view.list() {
                    if let Some(control) = control {
                        map.insert((name, version, arch), control);
                    }
                }
            }
        }
        map
    }

    /// Parse a pool `.deb`/`.udeb` filename into `(name, version, arch)`.
    /// Debian pool filenames are `{name}_{version}_{arch}.deb`; neither the
    /// package name nor the version contains `_`, so a 3-way split is exact.
    fn parse_deb_filename(fname: &str) -> Option<(String, String, String)> {
        let stem = fname
            .strip_suffix(".deb")
            .or_else(|| fname.strip_suffix(".udeb"))?;
        let mut it = stem.splitn(3, '_');
        let name = it.next()?;
        let version = it.next()?;
        let arch = it.next()?;
        if name.is_empty() || version.is_empty() || arch.is_empty() {
            return None;
        }
        Some((name.to_string(), version.to_string(), arch.to_string()))
    }

    /// The `.deb` files under `pool/{component}/`, with their full archive paths.
    fn pool_debs(&self, component: &str) -> Vec<String> {
        let prefix = format!("pool/{component}/");
        let mut files: Vec<String> = self
            .list_files()
            .into_iter()
            .filter(|f| f.starts_with(&prefix) && (f.ends_with(".deb") || f.ends_with(".udeb")))
            .collect();
        files.sort();
        files
    }

    /// The `(components, arches)` present in `pool/` — the dimensions of the
    /// synthesized index, and the ONLY coordinates a request may name. Derived
    /// once (the archive is immutable) and memoized: it is consulted on every
    /// index request, and it is what keeps the `Packages` cache bounded.
    fn pool_dimensions(&self) -> &(BTreeSet<String>, BTreeSet<String>) {
        self.dims_cache.get_or_init(|| self.build_pool_dimensions())
    }

    fn build_pool_dimensions(&self) -> (BTreeSet<String>, BTreeSet<String>) {
        let mut components = BTreeSet::new();
        let mut arches = BTreeSet::new();
        for f in self.list_files() {
            if !(f.ends_with(".deb") || f.ends_with(".udeb")) {
                continue;
            }
            // pool/{component}/...
            let mut segs = f.split('/');
            if segs.next() != Some("pool") {
                continue;
            }
            if let Some(component) = segs.next() {
                components.insert(component.to_string());
            }
            if let Some(fname) = f.rsplit('/').next() {
                if let Some((_, _, arch)) = Self::parse_deb_filename(fname) {
                    arches.insert(arch);
                }
            }
        }
        (components, arches)
    }

    /// Build the `Packages` index for `(component, arch)` from the pool `.deb`s.
    /// A `.deb` with architecture `all` is included under every requested arch.
    fn build_packages(&self, component: &str, arch: &str) -> Vec<u8> {
        let controls = self.control_stanzas();
        let mut out = String::new();
        for f in self.pool_debs(component) {
            let Some(fname) = f.rsplit('/').next() else {
                continue;
            };
            let Some((name, version, deb_arch)) = Self::parse_deb_filename(fname) else {
                continue;
            };
            if deb_arch != arch && deb_arch != "all" {
                continue;
            }
            let typed = controls
                .get(&(name.clone(), version.clone(), deb_arch.clone()))
                .cloned();
            // Authoritative control stanza (real Depends/Maintainer/Description),
            // in precedence order:
            //   1. the typed deb view (the znippy `deb_native` plugin's sub-index),
            //   2. the file's index row (a chain view, lane HOL-PERF-2: the stanza, size
            //      and sha256 the scan worker read at intake; the .deb is not read), or
            //      for an archive without rows a NATIVE `ar`+`control.tar.*` parse of the
            //      served `.deb` bytes (mirrored/plugin-less archives), then
            //   3. the filename-only fields as a last resort.
            // The repo-level Filename/Size/SHA256 are always appended by holger.
            let row = self.rows.as_ref().and_then(|r| r.row(&f));
            let (control, size, sha) = match (&self.rows, row) {
                (Some(_), Some(r)) => {
                    let from_row = match &r.row {
                        holger_index_rows::IndexRow::Deb { control } => Some(control.clone()),
                        _ => None,
                    };
                    (typed.or(from_row), r.size, r.sha256.clone())
                }
                // A view's file with no row: the bytes for its size and hash, never parsed.
                (Some(_), None) => {
                    let Ok(bytes) = self.get_file(&f) else {
                        continue;
                    };
                    (typed, bytes.len() as u64, hex_sha256(&bytes))
                }
                (None, _) => {
                    let Ok(bytes) = self.get_file(&f) else {
                        continue;
                    };
                    let control = typed.or_else(|| control::control_from_deb(&bytes));
                    (control, bytes.len() as u64, hex_sha256(&bytes))
                }
            };
            match control {
                Some(control) => match clean_control_stanza(&control, &name, &version, &deb_arch) {
                    Some(stanza) => out.push_str(&stanza),
                    None => {
                        log::warn!(
                            "deb: {f} is not listed: its control names another package, version \
                             or architecture than its pool coordinate"
                        );
                        continue;
                    }
                },
                None => {
                    out.push_str(&format!("Package: {name}\n"));
                    out.push_str(&format!("Version: {version}\n"));
                    out.push_str(&format!("Architecture: {deb_arch}\n"));
                }
            }
            out.push_str(&format!("Filename: {f}\n"));
            out.push_str(&format!("Size: {size}\n"));
            out.push_str(&format!("SHA256: {sha}\n"));
            out.push('\n');
        }
        out.into_bytes()
    }

    /// The synthesized `Packages` for a `(component, arch)` the pool contains,
    /// built at most once. The key is DERIVED from the pool, never taken from the
    /// request path, so the map is bounded by the archive's own dimensions and a
    /// caller cannot force a rebuild by renaming a path segment.
    fn packages(&self, component: &str, arch: &str) -> Arc<PackagesIndex> {
        let key = (component.to_string(), arch.to_string());
        if let Ok(cache) = self.packages_cache.lock() {
            if let Some(hit) = cache.get(&key) {
                return Arc::clone(hit);
            }
        }
        let body = self.build_packages(component, arch);
        let gz = traits::Bytes::from(gzip(&body));
        let built = Arc::new(PackagesIndex {
            sha256: hex_sha256(&body),
            gz_sha256: hex_sha256(&gz),
            gz,
            body: traits::Bytes::from(body),
        });
        if let Ok(mut cache) = self.packages_cache.lock() {
            cache.insert(key, Arc::clone(&built));
        }
        built
    }

    /// The dist-independent `Release` body, built at most once: the `SHA256:`
    /// block over every synthesized `Packages` plus the component/architecture
    /// lists. This is the whole pool walk; `dists/{dist}` only names the suite.
    fn release_parts(&self) -> &ReleaseParts {
        self.release_cache
            .get_or_init(|| self.build_release_parts())
    }

    fn build_release_parts(&self) -> ReleaseParts {
        let (components, arches) = self.pool_dimensions();
        let mut sha_lines = String::new();
        // Deterministic order: (component, arch) ascending.
        for component in components {
            for arch in arches {
                let pkgs = self.packages(component, arch);
                if pkgs.body.is_empty() {
                    continue;
                }
                sha_lines.push_str(&format!(
                    " {} {} {component}/binary-{arch}/Packages\n",
                    pkgs.sha256,
                    pkgs.body.len()
                ));
                sha_lines.push_str(&format!(
                    " {} {} {component}/binary-{arch}/Packages.gz\n",
                    pkgs.gz_sha256,
                    pkgs.gz.len()
                ));
            }
        }
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0);
        ReleaseParts {
            date: rfc2822_utc(now),
            sha_lines,
            components: components.iter().cloned().collect::<Vec<_>>().join(" "),
            architectures: arches.iter().cloned().collect::<Vec<_>>().join(" "),
        }
    }

    /// Render the dist `Release` index, referencing every synthesized `Packages`
    /// by SHA256 + size (the section `apt` verifies downloaded indexes against).
    /// Pure formatting over [`Self::release_parts`] — the suite name is the only
    /// per-request input, so a varied `dists/<suite>` costs a `format!`, not a
    /// fresh decompress + hash of every package in the pool.
    fn build_release(&self, dist: &str) -> Vec<u8> {
        let parts = self.release_parts();
        let mut out = String::new();
        out.push_str("Origin: Holger\n");
        out.push_str("Label: Holger\n");
        out.push_str(&format!("Suite: {dist}\n"));
        out.push_str(&format!("Codename: {dist}\n"));
        out.push_str(&format!("Date: {}\n", parts.date));
        out.push_str(&format!("Architectures: {}\n", parts.architectures));
        out.push_str(&format!("Components: {}\n", parts.components));
        out.push_str("Description: Holger synthesized APT repository\n");
        out.push_str("SHA256:\n");
        out.push_str(&parts.sha_lines);
        out.into_bytes()
    }

    /// Synthesize the index for `remainder` if it names a known APT index file.
    /// `None` for non-index paths (caller then 404s). An empty `Packages`
    /// (component/arch with no packages) is reported as `None` → 404.
    fn synthesize_index(&self, remainder: &str) -> Option<(traits::Bytes, &'static str)> {
        let parts: Vec<&str> = remainder.split('/').filter(|s| !s.is_empty()).collect();
        match parts.as_slice() {
            // dists/{dist}/Release — the suite name is decoration on a body that
            // is otherwise identical whatever `dists/<suite>` asks for it.
            ["dists", dist, "Release"] => {
                Some((traits::Bytes::from(self.build_release(dist)), "text/plain"))
            }
            // dists/{dist}/{component}/binary-{arch}/Packages. The identity of the
            // work is (component, arch) — the suite is irrelevant to the body. A
            // coordinate the pool does not carry 404s BEFORE any work, which is
            // also what keeps the cache bounded to the archive's dimensions.
            ["dists", _dist, component, binary_arch, file @ ("Packages" | "Packages.gz")] => {
                let arch = binary_arch.strip_prefix("binary-")?;
                let (components, arches) = self.pool_dimensions();
                if !components.contains(*component) || !arches.contains(arch) {
                    return None;
                }
                let index = self.packages(component, arch);
                if index.body.is_empty() {
                    return None;
                }
                // The same bytes every request (lane HOL-PERF-2): a refcount, no copy.
                if *file == "Packages.gz" {
                    return Some((index.gz.clone(), "application/gzip"));
                }
                Some((index.body.clone(), "text/plain"))
            }
            _ => None,
        }
    }
}

/// `secs` since the epoch as an RFC 2822 date in UTC (`Sun, 01 Nov 2026 08:39:20 UTC`), the
/// form apt reads in `Release`.
fn rfc2822_utc(secs: u64) -> String {
    const DAYS: [&str; 7] = ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"];
    const MONTHS: [&str; 12] = [
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
    ];
    let days = (secs / 86_400) as i64;
    let rem = secs % 86_400;
    // H. Hinnant's civil_from_days.
    let z = days + 719_468;
    let era = z.div_euclid(146_097);
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = yoe + era * 400 + i64::from(m <= 2);
    format!(
        "{}, {d:02} {} {y} {:02}:{:02}:{:02} UTC",
        DAYS[(days % 7) as usize],
        MONTHS[(m - 1) as usize],
        rem / 3600,
        rem % 3600 / 60,
        rem % 60
    )
}

/// `data`, gzip-compressed (the header's time is zero, so the bytes are the content's).
fn gzip(data: &[u8]) -> Vec<u8> {
    use std::io::Write as _;
    let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
    e.write_all(data).expect("an in-memory write");
    e.finish().expect("an in-memory finish")
}

fn hex_sha256(data: &[u8]) -> String {
    // REUSE-law: the ONE shared content-hash primitive (edda `nornir_hash`),
    // consumed rather than re-rolling `Sha256::new(); hex(...)` locally. apt
    // verifies these against the served bytes, so the digest must be byte-identical.
    nornir_hash::sha256_hex(data)
}

/// Normalize a `.deb` `control` stanza into APT `Packages` field lines: take the
/// single stanza (stop at the first blank line — control files carry exactly one),
/// and drop any repo-level `Filename`/`Size`/checksum fields (which holger computes
/// and appends itself; a control file shouldn't carry them, but the archive is
/// attacker-influenced, so this is defence-in-depth against field injection).
/// Multi-line continuation lines (e.g. `Description`) are preserved. Always ends
/// with a trailing newline so the caller can append its own fields.
/// **The control stanza `Packages` lists for one pool `.deb`** (SEC-REVIEW 2026-11-01 K1):
/// `None` when its `Package`, `Version` (less an epoch, which no file name carries) or
/// `Architecture` disagrees with the pool coordinate `name_version_arch.deb`, so the box never
/// signs a listing that names another package. Holger's own fields (Filename, Size, the
/// digests) are left out, a line that is not `Field: value` or a continuation is dropped, a
/// repeated field is dropped, and a missing coordinate field is taken from the file name.
fn clean_control_stanza(control: &str, name: &str, version: &str, arch: &str) -> Option<String> {
    let mut out = String::new();
    let mut seen = std::collections::BTreeSet::new();
    let mut keep_continuation = false;
    let (mut has_pkg, mut has_ver, mut has_arch) = (false, false, false);
    for line in control.lines() {
        if line.trim().is_empty() {
            break;
        }
        if line.starts_with(' ') || line.starts_with('\t') {
            if keep_continuation {
                out.push_str(line);
                out.push('\n');
            }
            continue;
        }
        keep_continuation = false;
        let Some((key, value)) = line.split_once(':') else {
            continue;
        };
        if key.is_empty()
            || key.starts_with('-')
            || key.starts_with('#')
            || !key.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
        {
            continue;
        }
        let lower = key.to_ascii_lowercase();
        if !seen.insert(lower.clone()) {
            continue;
        }
        if ["filename", "size", "md5sum", "sha1", "sha256", "sha512"].contains(&lower.as_str()) {
            continue;
        }
        let value = value.trim();
        match lower.as_str() {
            "package" if value != name => return None,
            "architecture" if value != arch => return None,
            "version" => {
                let bare = match value.split_once(':') {
                    Some((epoch, rest)) if epoch.bytes().all(|b| b.is_ascii_digit()) => rest,
                    _ => value,
                };
                if bare != version && value != version {
                    return None;
                }
                has_ver = true;
            }
            "package" => has_pkg = true,
            "architecture" => has_arch = true,
            _ => {}
        }
        out.push_str(line);
        out.push('\n');
        keep_continuation = true;
    }
    let mut head = String::new();
    if !has_pkg {
        head.push_str(&format!("Package: {name}\n"));
    }
    if !has_ver {
        head.push_str(&format!("Version: {version}\n"));
    }
    if !has_arch {
        head.push_str(&format!("Architecture: {arch}\n"));
    }
    Some(head + &out)
}

fn content_type_for(path: &str) -> &'static str {
    if path.ends_with(".deb") || path.ends_with(".udeb") {
        "application/vnd.debian.binary-package"
    } else if path.ends_with(".gz") {
        "application/gzip"
    } else {
        "text/plain"
    }
}

impl RepositoryBackendTrait for DebRepoZnippy {
    traits::znippy_read_only!(base);

    /// A package download in pieces (lane HOL-RSS): the path-passthrough the whole-body
    /// door answers first, so the door holds a piece, not the package.
    fn stream_get(
        &self,
        suburl: &str,
        piece_bytes: usize,
    ) -> anyhow::Result<Option<traits::StreamedGet>> {
        let remainder = self.base.strip_repo(suburl);
        if remainder.is_empty() {
            return Ok(None);
        }
        self.base
            .stream_get(remainder, content_type_for(remainder), piece_bytes)
    }

    fn handle_http2_request(
        &self,
        method: &str,
        suburl: &str,
        body: &[u8],
    ) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
        self.handle_http2_request_bytes(method, suburl, body)
            .map(|(s, h, b)| (s, h, b.to_vec()))
    }

    fn handle_http2_request_bytes(
        &self,
        method: &str,
        suburl: &str,
        body: &[u8],
    ) -> anyhow::Result<traits::HttpBytesReply> {
        if method == "HEAD" {
            // HOL-HEAD-1: the GET answer's status and headers, its length, no body.
            return self
                .handle_http2_request_bytes("GET", suburl, &[])
                .map(traits::backend::head_of_bytes);
        }
        let _ = body;
        log::debug!("deb repo znippy handle_http2_request.suburl={}", suburl);

        let remainder = self.base.strip_repo(suburl);
        if remainder.is_empty() {
            return Ok(not_found_bytes("Not found"));
        }

        // 1) Authoritative path-passthrough: packages, and any mirrored upstream
        //    metadata (a real Release/Packages packed into the archive wins).
        if let Ok(data) = self.get_file(remainder) {
            return Ok(ok_bytes(
                content_type_for(remainder),
                traits::Bytes::from(data),
            ));
        }

        // 2) Native synthesis: build the APT index from the pool `.deb`s, once.
        if let Some((data, ctype)) = self.synthesize_index(remainder) {
            return Ok(ok_bytes(ctype, data));
        }

        Ok(not_found_bytes("Not found in archive"))
    }

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

    fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<traits::Bytes>> {
        // Match on the PARSED filename, exact name + version — never a string
        // prefix (which made `hello 2.10` collide with `hello_2.100`), and
        // include `.udeb` (the prior `.deb`-only test skipped udebs entirely).
        // When the id carries an arch in `namespace`, require it too.
        let mut files: Vec<String> = self
            .list_files()
            .into_iter()
            .filter(|f| f.ends_with(".deb") || f.ends_with(".udeb"))
            .collect();
        files.sort();
        for f in files {
            let Some(fname) = f.rsplit('/').next() else {
                continue;
            };
            let Some((name, version, arch)) = Self::parse_deb_filename(fname) else {
                continue;
            };
            if name != id.name || version != id.version {
                continue;
            }
            if let Some(want_arch) = &id.namespace {
                if &arch != want_arch {
                    continue;
                }
            }
            if let Ok(data) = self.get_file(&f) {
                return Ok(Some(traits::Bytes::from(data)));
            }
        }
        Ok(None)
    }

    /// List packages parsed from the pool `.deb` filenames — `(name, version)`
    /// coords with the arch riding in `namespace`. Empty when no reader.
    fn list(
        &self,
        name_filter: Option<&str>,
        limit: usize,
    ) -> anyhow::Result<Vec<traits::ArtifactEntry>> {
        // Sort by full path for stable output across components.
        let mut files = self.list_files();
        // ONE index read for the whole listing; every token after it is memory,
        // and no chunk of any artifact is touched.
        let tokens = self.base.tokens().open();
        files.sort();
        let mut out = Vec::new();
        for f in files {
            if !(f.ends_with(".deb") || f.ends_with(".udeb")) {
                continue;
            }
            let Some(fname) = f.rsplit('/').next() else {
                continue;
            };
            let Some((name, version, arch)) = Self::parse_deb_filename(fname) else {
                continue;
            };
            if let Some(flt) = name_filter {
                if !name.contains(flt) {
                    continue;
                }
            }
            let size_bytes = self
                .base
                .reader()
                .and_then(|r| r.file_size(&f))
                .unwrap_or(0) as i64;
            out.push(
                traits::ArtifactEntry {
                    id: ArtifactId {
                        namespace: Some(arch),
                        name,
                        version,
                    },
                    size_bytes,
                    content_type: "application/vnd.debian.binary-package".into(),
                    // A znippy file table publishes no mtime and no per-entry
                    // digest; answering either would mean extracting and hashing
                    // every artifact on the listing path. Unknown, never a
                    // fabricated zero. The CHANGE TOKEN below is the one
                    // provenance answer the index can give for free.
                    ..traits::ArtifactEntry::UNKNOWN_PROVENANCE
                }
                .with_change_token(tokens.as_ref().and_then(|t| t.token(&f))),
            );
            if limit != 0 && out.len() >= limit {
                break;
            }
        }
        Ok(out)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;
    use traits::test_support::{CountingReader, MemReader};

    #[cfg(feature = "testmatrix")]
    use traits::test_support::fstatus;

    /// The synthesized-index cache must be keyed on the DERIVED identity of the
    /// work, not on the request path. Keyed by the path remainder, an
    /// unauthenticated client re-ran the whole pool walk (decompress + SHA-256 +
    /// control-parse of every `.deb`) just by varying `dists/<suite>` — a small
    /// request buying arbitrarily much server work. Counted through the reader:
    /// after the first index request, no amount of renaming may cause one further
    /// extraction.
    #[test]
    fn the_release_date_is_rfc2822_utc() {
        assert_eq!(rfc2822_utc(0), "Thu, 01 Jan 1970 00:00:00 UTC");
        assert_eq!(
            rfc2822_utc(951_782_400 + 3_723),
            "Tue, 29 Feb 2000 01:02:03 UTC"
        );
        assert_eq!(rfc2822_utc(1_793_522_360), "Sun, 01 Nov 2026 08:39:20 UTC");
    }

    #[test]
    fn index_work_is_not_repeatable_by_renaming_the_path() {
        let deb = super::control::tests_support::sample_deb();
        let reader = CountingReader::new(MemReader::new([
            ("pool/main/h/hello/hello_2.10-2_amd64.deb", deb.clone()),
            ("pool/main/h/hello/hello_2.11_amd64.deb", deb.clone()),
            ("pool/main/c/curl/curl_8.5.0_amd64.deb", deb.clone()),
        ]));
        let repo = DebRepoZnippy::with_reader("debrepo".into(), Arc::clone(&reader) as Arc<_>);

        // First Release: the pool walk actually happens.
        let (st, _h, body) = repo
            .handle_http2_request("GET", "/debrepo/dists/stable/Release", b"")
            .expect("ok");
        let first = String::from_utf8(body).expect("utf8");
        assert_eq!(st, 200);
        assert!(first.contains("Suite: stable\n"), "{first}");
        let after_first = reader.hits();
        assert!(
            after_first >= 3,
            "the first Release must read every pool .deb, got {after_first}"
        );

        // Now the attack: same Release, a fresh suite name every time.
        for suite in ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"] {
            let (s, _h, b) = repo
                .handle_http2_request("GET", &format!("/debrepo/dists/{suite}/Release"), b"")
                .expect("ok");
            let rendered = String::from_utf8(b).expect("utf8");
            assert_eq!(s, 200);
            // Still correct: the suite is reflected…
            assert!(
                rendered.contains(&format!("Suite: {suite}\n")),
                "{rendered}"
            );
            // …over an identical, already-computed body.
            assert_eq!(
                rendered.replace(&format!("Suite: {suite}\nCodename: {suite}\n"), ""),
                first.replace("Suite: stable\nCodename: stable\n", ""),
                "only the suite may differ between Release renderings"
            );
        }
        let after_renames = reader.hits();
        assert_eq!(
            after_renames, after_first,
            "renaming dists/<suite> must not buy a single further pool read \
             ({after_first} -> {after_renames})"
        );

        // Same for Packages: the suite segment is not part of the work's identity.
        for suite in ["stable", "k", "l", "m"] {
            let (s, _h, b) = repo
                .handle_http2_request(
                    "GET",
                    &format!("/debrepo/dists/{suite}/main/binary-amd64/Packages"),
                    b"",
                )
                .expect("ok");
            assert_eq!(s, 200);
            assert!(String::from_utf8_lossy(&b).contains("Package: hello\n"));
        }
        let after_packages = reader.hits();
        assert_eq!(
            after_packages, after_first,
            "the Packages body is identified by (component, arch), not by the suite"
        );

        // A coordinate the pool does not carry 404s without doing any work — which
        // is what keeps the cache bounded to the archive's own dimensions.
        for url in [
            "/debrepo/dists/stable/nosuchcomponent/binary-amd64/Packages",
            "/debrepo/dists/stable/main/binary-nosucharch/Packages",
        ] {
            let (s, _h, _b) = repo.handle_http2_request("GET", url, b"").expect("ok");
            assert_eq!(s, 404, "unknown index coordinate must 404: {url}");
        }
        let after_unknown = reader.hits();
        assert_eq!(
            after_unknown, after_first,
            "an unknown coordinate must cost nothing"
        );
        assert!(
            repo.packages_cache.lock().unwrap().len() <= 1,
            "only the one real (component, arch) may be cached, got {:?}",
            repo.packages_cache
                .lock()
                .unwrap()
                .keys()
                .collect::<Vec<_>>()
        );

        #[cfg(feature = "testmatrix")]
        fstatus(
            "znippy-deb",
            "index_cache_keyed_on_derived_identity",
            after_unknown == after_first,
            &format!(
                "{after_first} pool reads for the first index; 14 renamed index requests + 2 unknown coords cost 0 more"
            ),
        );
    }

    #[test]
    fn test_new() {
        let repo = DebRepoZnippy::new("deb-test".to_string());
        assert_eq!(repo.name(), "deb-test");
        assert!(!repo.is_writable());

        #[cfg(feature = "testmatrix")]
        fstatus(
            "znippy-deb",
            "new_name_and_readonly",
            repo.name() == "deb-test" && !repo.is_writable(),
            &format!("name={} writable={}", repo.name(), repo.is_writable()),
        );
    }

    /// **Capability row** (CLAUDE.md prod-vs-prototype: Deb control parse). The
    /// native `Packages`/`Release` index is synthesized with real SHA256, and the
    /// per-package control fields now come from a real `ar`+`control.tar.*` parse of
    /// the served `.deb` bytes (the `control` module) when the typed deb view is
    /// absent — filename-derivation is only the last-resort fallback. Proven by
    /// [`native_control_parse_enriches_packages`]; emitted `ok=true`.
    #[test]
    fn deb_control_parse_capability() {
        // A bare `.deb` (no typed view) must surface a real control field.
        let deb = super::control::tests_support::sample_deb();
        let parsed = super::control::control_from_deb(&deb)
            .map(|c| c.contains("Depends:"))
            .unwrap_or(false);
        assert!(
            parsed,
            "native ar+control.tar parse must extract the control stanza"
        );
        #[cfg(feature = "testmatrix")]
        fstatus(
            "znippy-deb",
            "binary_control_parse",
            parsed,
            "Packages control fields parsed from ar+control.tar.{,gz,xz,zst}; real SHA256",
        );
    }

    #[test]
    fn serves_injected_archive() {
        // Real APT pool layout: pool/{component}/{prefix}/{pkg}/{pkg}_{ver}_{arch}.deb
        const KEY: &str = "pool/main/h/hello/hello_2.10-2_amd64.deb";
        let deb_bytes: Vec<u8> = b"!<arch>\nDEBIAN-FAKE-PAYLOAD".to_vec();

        let reader = MemReader::new([(KEY, deb_bytes.clone())]);
        let repo = DebRepoZnippy::with_reader("debrepo".to_string(), reader);

        // fetch() scans pool/ for a .deb whose filename starts `name_version`.
        let id = ArtifactId {
            namespace: None,
            name: "hello".into(),
            version: "2.10-2".into(),
        };
        let fetched = repo.fetch(&id).expect("fetch ok");
        assert_eq!(fetched.as_deref(), Some(deb_bytes.as_slice()));

        // fetch() miss → None.
        let miss_id = ArtifactId {
            namespace: None,
            name: "absent".into(),
            version: "9.9".into(),
        };
        let fetched_miss = repo.fetch(&miss_id).expect("fetch ok");
        assert!(fetched_miss.is_none());

        // HTTP/2 path-passthrough hit: /{repo}/{key} → 200 + deb content type + body.
        let url = format!("/debrepo/{KEY}");
        let (status, headers, body) = repo
            .handle_http2_request("GET", &url, b"")
            .expect("handle ok");
        assert_eq!(status, 200);
        assert_eq!(body, deb_bytes);
        let ctype = headers
            .iter()
            .find(|(k, _)| k == "Content-Type")
            .map(|(_, v)| v.as_str());
        assert_eq!(ctype, Some("application/vnd.debian.binary-package"));

        // HTTP/2 miss → 404.
        let (miss_status, _miss_headers, _miss_body) = repo
            .handle_http2_request("GET", "/debrepo/pool/main/h/hello/nope_0_amd64.deb", b"")
            .expect("handle ok");
        assert_eq!(miss_status, 404);

        #[cfg(feature = "testmatrix")]
        {
            fstatus(
                "znippy-deb",
                "fetch_serves_injected_deb",
                fetched.as_deref() == Some(deb_bytes.as_slice()) && fetched_miss.is_none(),
                &format!(
                    "key={KEY} fetched_len={:?} miss={:?}",
                    fetched.map(|d| d.len()),
                    fetched_miss
                ),
            );
            fstatus(
                "znippy-deb",
                "http2_serves_injected_deb",
                status == 200
                    && body == deb_bytes
                    && ctype == Some("application/vnd.debian.binary-package")
                    && miss_status == 404,
                &format!("url={url} status={status} ctype={ctype:?} miss_status={miss_status}"),
            );
        }
    }

    /// Native protocol: the `Packages` index and the dist `Release` are
    /// synthesized from the pool `.deb`s (with real sizes + SHA256), and the
    /// `Release` references the synthesized `Packages` by hash — the index path
    /// an `apt` client resolves before downloading.
    #[test]
    fn synthesizes_apt_index_from_pool() {
        const KEY: &str = "pool/main/h/hello/hello_2.10-2_amd64.deb";
        let deb_bytes: Vec<u8> = b"!<arch>\nDEBIAN-FAKE-PAYLOAD".to_vec();
        let reader = MemReader::new([(KEY, deb_bytes.clone())]);
        let repo = DebRepoZnippy::with_reader("debrepo".to_string(), reader);

        // Packages index: 200 + text/plain, carrying the package's control fields
        // (built from the filename) + the real size + SHA256 of the .deb bytes.
        let pkgs_url = "/debrepo/dists/stable/main/binary-amd64/Packages";
        let (st, headers, body) = repo.handle_http2_request("GET", pkgs_url, b"").expect("ok");
        let pkgs = String::from_utf8(body.clone()).expect("utf8");
        let ctype = headers
            .iter()
            .find(|(k, _)| k == "Content-Type")
            .map(|(_, v)| v.as_str());
        let expected_sha = hex_sha256(&deb_bytes);
        let packages_ok = st == 200
            && ctype == Some("text/plain")
            && pkgs.contains("Package: hello\n")
            && pkgs.contains("Version: 2.10-2\n")
            && pkgs.contains("Architecture: amd64\n")
            && pkgs.contains(&format!("Filename: {KEY}\n"))
            && pkgs.contains(&format!("Size: {}\n", deb_bytes.len()))
            && pkgs.contains(&format!("SHA256: {expected_sha}\n"));
        assert!(packages_ok, "Packages index malformed:\n{pkgs}");

        // Release: 200, lists the component/arch and references the synthesized
        // Packages by its SHA256 — the same hash a fresh build of Packages yields.
        let rel_url = "/debrepo/dists/stable/Release";
        let (rst, _rh, rbody) = repo.handle_http2_request("GET", rel_url, b"").expect("ok");
        let release = String::from_utf8(rbody).expect("utf8");
        let pkgs_sha = hex_sha256(&body);
        let release_ok = rst == 200
            && release.contains("Suite: stable\n")
            && release.contains("Components: main\n")
            && release.contains("Architectures: amd64\n")
            && release.contains(&format!(
                " {pkgs_sha} {} main/binary-amd64/Packages\n",
                body.len()
            ));
        assert!(release_ok, "Release malformed:\n{release}");

        // An arch with no packages → 404 (no empty index served).
        let (miss_st, _h, _b) = repo
            .handle_http2_request(
                "GET",
                "/debrepo/dists/stable/main/binary-arm64/Packages",
                b"",
            )
            .expect("ok");
        assert_eq!(miss_st, 404);

        #[cfg(feature = "testmatrix")]
        {
            fstatus(
                "znippy-deb",
                "synthesizes_packages_index",
                packages_ok,
                &format!("url={pkgs_url} status={st} sha={expected_sha}"),
            );
            fstatus(
                "znippy-deb",
                "synthesizes_release_index",
                release_ok && miss_st == 404,
                &format!(
                    "url={rel_url} status={rst} pkgs_sha={pkgs_sha} empty_arch_status={miss_st}"
                ),
            );
        }
    }

    /// The native `ar`+`control.tar.*` parse ENRICHES the synthesized `Packages`
    /// index: served a bare `.deb` (no typed deb view, so `control_stanzas()` is
    /// empty), holger still surfaces the real `Depends`/`Maintainer`/`Description`
    /// from the package's own control tarball — not just the filename-derived
    /// name/version/arch. This is the capability the maturity row now asserts green.
    /// **The signed `Packages` lists a `.deb` only as its approved coordinate** (SEC-REVIEW
    /// 2026-11-01 K1). The stanza came from the package's own control, so an approved
    /// `hello_1.0_amd64.deb` whose control says `Package: libc6`, `Version: 99:1` was listed
    /// (and box-signed) as libc6 99, and `apt upgrade` installed it; a control line with no
    /// colon broke the index for every client. A stanza whose Package, Version or
    /// Architecture disagrees with the pool coordinate is not listed; a malformed line or a
    /// repeated field is dropped.
    #[test]
    fn a_control_that_names_another_package_is_not_listed() {
        for control in [
            "Package: libc6\nVersion: 99:1\nArchitecture: amd64\n",
            "Package: hello\nVersion: 99:1\nArchitecture: amd64\n",
            "Package: hello\nVersion: 1.0\nArchitecture: arm64\n",
        ] {
            assert_eq!(
                clean_control_stanza(control, "hello", "1.0", "amd64"),
                None,
                "{control}"
            );
        }
        let got = clean_control_stanza(
            "Package: hello\nVersion: 2:1.0\nArchitecture: amd64\nno colon here\nDepends: libc6\n continued\nDepends: evil\nSHA256: x\n",
            "hello",
            "1.0",
            "amd64",
        )
        .expect("listed: the epoch is not in a file name");
        assert_eq!(
            got,
            "Package: hello\nVersion: 2:1.0\nArchitecture: amd64\nDepends: libc6\n continued\n"
        );
        let got = clean_control_stanza("Maintainer: m\n", "hello", "1.0", "all").unwrap();
        assert!(
            got.starts_with("Package: hello\nVersion: 1.0\nArchitecture: all\n"),
            "{got}"
        );
    }

    #[test]
    fn native_control_parse_enriches_packages() {
        const KEY: &str = "pool/main/h/hello/hello_2.10-2_amd64.deb";
        let deb = super::control::tests_support::sample_deb();
        let reader = MemReader::new([(KEY, deb.clone())]);
        // `with_reader` ⇒ no typed archive view ⇒ control_stanzas() is empty, so the
        // ONLY way these fields appear is the native ar+control.tar parse.
        let repo = DebRepoZnippy::with_reader("debrepo".into(), reader);
        assert!(
            repo.control_stanzas().is_empty(),
            "no typed view in with_reader mode"
        );

        let url = "/debrepo/dists/stable/main/binary-amd64/Packages";
        let (st, _h, body) = repo.handle_http2_request("GET", url, b"").expect("ok");
        let pkgs = String::from_utf8(body).expect("utf8");
        assert_eq!(st, 200);
        // Filename-derived coords still present…
        assert!(pkgs.contains("Package: hello\n"), "index:\n{pkgs}");
        assert!(pkgs.contains("Version: 2.10-2\n"), "index:\n{pkgs}");
        // …plus the NATIVELY-parsed authoritative control fields.
        assert!(
            pkgs.contains("Maintainer: Santiago <santiago@debian.org>\n"),
            "index:\n{pkgs}"
        );
        assert!(
            pkgs.contains("Depends: libc6 (>= 2.14)\n"),
            "index:\n{pkgs}"
        );
        assert!(
            pkgs.contains("Description: friendly GNU greeter\n"),
            "index:\n{pkgs}"
        );
        // holger's own repo-level fields are appended over the top.
        assert!(
            pkgs.contains(&format!("Filename: {KEY}\n")),
            "index:\n{pkgs}"
        );
        assert!(
            pkgs.contains(&format!("Size: {}\n", deb.len())),
            "index:\n{pkgs}"
        );

        #[cfg(feature = "testmatrix")]
        fstatus(
            "znippy-deb",
            "native_control_parse_enriches_packages",
            pkgs.contains("Depends: libc6 (>= 2.14)\n") && pkgs.contains("Maintainer: Santiago"),
            "ar+control.tar.gz parse surfaced Depends/Maintainer/Description with no typed view",
        );
    }

    #[test]
    fn test_format() {
        let f = DebRepoZnippy::new("x".into()).format();
        assert_eq!(f, ArtifactFormat::Deb);

        #[cfg(feature = "testmatrix")]
        fstatus(
            "znippy-deb",
            "format_is_deb",
            f == ArtifactFormat::Deb,
            &format!("format = {f:?}"),
        );
    }

    /// T20: fetch must not collide on a version prefix (`2.10` vs `2.100`), must
    /// honour an arch hint, and must be able to return a `.udeb` (the prior
    /// `.deb`-only test skipped udebs entirely).
    #[test]
    fn fetch_no_version_collision_udeb_and_arch() {
        let b2102 = b"hello-2.10-2-body".to_vec();
        let b2100 = b"hello-2.100-body-different".to_vec();
        let bi386 = b"hello-2.10-2-i386-body".to_vec();
        let budeb = b"hello-udeb-installer-body".to_vec();
        let reader = MemReader::new([
            ("pool/main/h/hello/hello_2.10-2_amd64.deb", b2102.clone()),
            ("pool/main/h/hello/hello_2.100_amd64.deb", b2100.clone()),
            ("pool/main/h/hello/hello_2.10-2_i386.deb", bi386.clone()),
            (
                "pool/main/h/hello-udeb/hello-udeb_2.10-2_amd64.udeb",
                budeb.clone(),
            ),
        ]);
        let repo = DebRepoZnippy::with_reader("debrepo".into(), reader);

        // A bare "2.10" matches NO exact version (the debs are 2.10-2 / 2.100).
        let id_bare = ArtifactId {
            namespace: None,
            name: "hello".into(),
            version: "2.10".into(),
        };
        assert!(
            repo.fetch(&id_bare).expect("ok").is_none(),
            "2.10 must not match 2.10-2 or 2.100"
        );

        // Exact "2.10-2" returns a 2.10-2 build, never the 2.100 body.
        let id_2102 = ArtifactId {
            namespace: None,
            name: "hello".into(),
            version: "2.10-2".into(),
        };
        let got = repo.fetch(&id_2102).expect("ok").expect("2.10-2 found");
        assert_ne!(
            got, b2100,
            "fetch(hello,2.10-2) must NOT return the 2.100 body"
        );
        assert!(got == b2102 || got == bi386, "returned a real 2.10-2 build");

        // Arch hint pins the build.
        let id_i386 = ArtifactId {
            namespace: Some("i386".into()),
            name: "hello".into(),
            version: "2.10-2".into(),
        };
        assert_eq!(
            repo.fetch(&id_i386).expect("ok").as_deref(),
            Some(bi386.as_slice())
        );

        // A .udeb is fetchable.
        let id_udeb = ArtifactId {
            namespace: None,
            name: "hello-udeb".into(),
            version: "2.10-2".into(),
        };
        assert_eq!(
            repo.fetch(&id_udeb).expect("ok").as_deref(),
            Some(budeb.as_slice())
        );

        let ok = repo.fetch(&id_bare).expect("ok").is_none()
            && got != b2100
            && repo.fetch(&id_udeb).expect("ok").as_deref() == Some(budeb.as_slice());
        assert!(ok);

        #[cfg(feature = "testmatrix")]
        fstatus(
            "znippy-deb",
            "fetch_no_version_collision_udeb",
            ok,
            "2.10!=2.10-2, 2.10-2!=2.100, arch-pinned, udeb fetchable",
        );
    }

    /// Caps and methods agree, with and without an archive loaded.
    #[test]
    fn caps_match_methods() {
        struct One;
        impl znippy_common::ZnippyReader for One {
            fn list_files(&self) -> anyhow::Result<Vec<String>> {
                Ok(vec!["x".into()])
            }
            fn extract_file(&self, _: &str) -> anyhow::Result<Vec<u8>> {
                Ok(Vec::new())
            }
            fn contains(&self, p: &str) -> bool {
                p == "x"
            }
            fn file_size(&self, _: &str) -> Option<u64> {
                Some(0)
            }
        }
        traits::backend::conformance::check(&DebRepoZnippy::new("r".into())).unwrap();
        traits::backend::conformance::check(&DebRepoZnippy::with_reader(
            "r".into(),
            std::sync::Arc::new(One),
        ))
        .unwrap();
    }

    /// **A chain view's `Packages` is rendered from the rows** (lane HOL-PERF-2): byte for
    /// byte the index the in-process parse gives, and not one `.deb` is read for it.
    #[test]
    fn with_rows_the_packages_index_reads_no_deb_and_is_the_parsed_one() {
        use traits::test_support::{CountingReader, MemReader};
        let deb = super::control::tests_support::sample_deb();
        let path = "pool/main/h/hello/hello_2.10-2_amd64.deb";
        let url = "/d/dists/stable/main/binary-amd64/Packages";
        let parsed = DebRepoZnippy::with_reader("d".into(), MemReader::new([(path, deb.clone())]));
        let want = parsed.handle_http2_request("GET", url, &[]).unwrap();
        assert_eq!(want.0, 200);
        assert!(String::from_utf8_lossy(&want.2).contains("Depends: libc6 (>= 2.14)"));
        let counting = CountingReader::new(MemReader::new([(path, deb.clone())]));
        let rows = holger_index_rows::MapRows::of("deb", &[(path, &deb)]);
        let viewed = DebRepoZnippy::with_reader_and_rows("d".into(), counting.clone(), rows);
        let got = viewed.handle_http2_request("GET", url, &[]).unwrap();
        assert_eq!(
            String::from_utf8_lossy(&got.2),
            String::from_utf8_lossy(&want.2),
            "byte for byte"
        );
        // The same Release but its `Date:`, which is when each backend rendered it.
        let undated = |r: &DebRepoZnippy| {
            let (_, _, b) = r
                .handle_http2_request("GET", "/d/dists/stable/Release", &[])
                .unwrap();
            String::from_utf8_lossy(&b)
                .lines()
                .filter(|l| !l.starts_with("Date: "))
                .collect::<Vec<_>>()
                .join("\n")
        };
        assert_eq!(undated(&viewed), undated(&parsed));
        assert_eq!(counting.hits(), 0, "no .deb is read to index it");
    }

    /// **Rendered once, handed out as the same bytes** (lane HOL-PERF-2, X5): a second
    /// request for `Packages` gets the very allocation the first got, never a copy.
    #[test]
    fn the_packages_index_is_one_allocation_handed_out_again() {
        use traits::test_support::MemReader;
        let deb = super::control::tests_support::sample_deb();
        let repo = DebRepoZnippy::with_reader(
            "d".into(),
            MemReader::new([("pool/main/h/hello/hello_2.10-2_amd64.deb", deb)]),
        );
        let url = "/d/dists/stable/main/binary-amd64/Packages";
        let a = repo.handle_http2_request_bytes("GET", url, &[]).unwrap();
        let b = repo.handle_http2_request_bytes("GET", url, &[]).unwrap();
        assert_eq!((a.0, !a.2.is_empty()), (200, true));
        assert_eq!(a.2.as_ptr(), b.2.as_ptr(), "the same bytes, not a copy");
        let h = repo.handle_http2_request_bytes("HEAD", url, &[]).unwrap();
        assert!(
            h.2.is_empty()
                && h.1
                    .iter()
                    .any(|(k, v)| k == "Content-Length" && *v == a.2.len().to_string()),
            "{:?}",
            h.1
        );
    }

    /// **`Packages.gz` is written once and listed in `Release`** (lane HOL-PERF-2, step
    /// 6): apt moves about a tenth of the bytes; the plain `Packages` stays.
    #[test]
    fn packages_gz_is_written_once_and_listed_in_release() {
        use std::io::Read as _;
        use traits::test_support::MemReader;
        let deb = super::control::tests_support::sample_deb();
        let repo = DebRepoZnippy::with_reader(
            "d".into(),
            MemReader::new([("pool/main/h/hello/hello_2.10-2_amd64.deb", deb)]),
        );
        let plain = repo
            .handle_http2_request_bytes("GET", "/d/dists/stable/main/binary-amd64/Packages", &[])
            .unwrap()
            .2;
        let gz = repo
            .handle_http2_request_bytes("GET", "/d/dists/stable/main/binary-amd64/Packages.gz", &[])
            .unwrap();
        assert_eq!(gz.0, 200);
        let mut out = Vec::new();
        flate2::read::GzDecoder::new(&gz.2[..])
            .read_to_end(&mut out)
            .unwrap();
        assert_eq!(out, &plain[..], "Packages.gz is Packages");
        let again = repo
            .handle_http2_request_bytes("GET", "/d/dists/stable/main/binary-amd64/Packages.gz", &[])
            .unwrap();
        assert_eq!(gz.2.as_ptr(), again.2.as_ptr(), "written once");
        let release = String::from_utf8(
            repo.handle_http2_request("GET", "/d/dists/stable/Release", &[])
                .unwrap()
                .2,
        )
        .unwrap();
        assert!(
            release.contains(&format!(
                " {} {} main/binary-amd64/Packages.gz\n",
                hex_sha256(&gz.2),
                gz.2.len()
            )),
            "{release}"
        );
    }
}