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-rpm/src/lib.rs

//! znippy-backed RPM (yum/dnf) repository backend — serves an rpm-md index
//! synthesized from the `.rpm` files inside a read-only znippy archive, the rpm
//! sibling of the deb/npm/maven `znippy-*` backends behind `RepositoryBackendTrait`.
//!
//! Two-stage `handle_http2_request`: authoritative path-passthrough first (packages,
//! and any real createrepo `repodata/` the archive already carries wins), then native
//! synthesis of `repodata/repomd.xml` + `repodata/primary.xml` from the packages
//! present. `primary.xml` lists one `<package>` per rpm with a real SHA256 + size over
//! the served bytes; `repomd.xml` references it by hash so dnf can resolve and verify.
//! NEVRA is parsed from the filename (`{name}-{version}-{release}.{arch}.rpm`), enriched
//! with epoch + `<format>` from the znippy `rpm_native` header view (`as_rpm`) when the
//! archive carries the sub-index; otherwise holger parses the RPM header NATIVELY out of
//! the served bytes (`header` module — real epoch/license/vendor/url/summary/sourcerpm/
//! provides/requires), and only when THAT misses falls back to filename-only `epoch=0`.
//!
//! Gotcha: synthesized indexes are cached because hashing the whole `Packages/` set per
//! request is a DoS-amplification vector — the cache is sound only because the archive is
//! immutable. And `fetch` matches on the *parsed* NEVRA (exact name + version), never a
//! string prefix, so `zlib 1.3.1` can't collide with `zlib-1.3.10`.
mod header;
/// The parser of untrusted package bytes, public for holger's fuzz targets (`fuzz/`).
pub use header::parse_rpm_header;
/// The test builders (feature `fixtures`): the fuzz seeds are made with them.
#[cfg(feature = "fixtures")]
pub use header::tests_support as fixtures;

use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use anyhow::Result;

use traits::{ArtifactFormat, ArtifactId, RepositoryBackendTrait, ZnippyBase};
use znippy_common::views::RpmMetaRow;
use znippy_common::ZnippyReader;

/// znippy-backed RPM (yum/dnf) repository.
///
/// dnf fetches the repodata index then the packages:
///   * `GET /{repo}/repodata/repomd.xml`        → index of metadata files
///   * `GET /{repo}/repodata/primary.xml`       → per-package metadata
///   * `GET /{repo}/Packages/{pkg}-{ver}-{rel}.{arch}.rpm`
///
/// **Native protocol (mirrors the conda/go plugins):** the package files are
/// served straight from the archive, and the **rpm-md index is synthesized from
/// the `.rpm` files present** when the archive doesn't already carry it.
/// `primary.xml` lists each package — name/version/release/arch parsed from the
/// filename, plus the archive byte length and a real **SHA256** of the served
/// bytes — and `repomd.xml` references `primary.xml` by SHA256 + size, so a
/// `dnf` client can resolve and verify downloads. The index is served
/// **uncompressed** (`repomd.xml` declares the `.xml` href; dnf picks the codec
/// from the extension), so no gzip dependency is pulled. 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 `Packages/` set — the same DoS-amplification guard
/// the conda plugin documents for `repodata.json`.
pub struct RpmRepoZnippy {
    /// `base.typed()` drives the typed rpm view (`as_rpm`), the source of the
    /// authoritative NEVRA incl. **epoch** the filename can't carry.
    base: ZnippyBase,
    /// Synthesized-index cache (`repodata/primary.xml`, `repodata/repomd.xml`).
    index_cache: Mutex<HashMap<String, traits::Bytes>>,
    /// The rows of the `.rpm`s (lane HOL-PERF-2), when the backend is a chain view: the
    /// header fields, the size and the sha256 the scan worker read at intake.
    rows: Option<Arc<dyn holger_index_rows::RowSource>>,
}

impl RpmRepoZnippy {
    fn from_base(base: ZnippyBase) -> Self {
        Self {
            base,
            index_cache: Mutex::new(HashMap::new()),
            rows: None,
        }
    }

    /// 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::from_base(ZnippyBase::with_reader(name, reader))
        }
    }

    pub fn new(name: String) -> Self {
        Self::from_base(ZnippyBase::new(name))
    }

    /// Backed by a specific znippy archive file; the typed rpm view is built once.
    pub fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> {
        Ok(Self::from_base(ZnippyBase::with_archive(
            name,
            archive_path,
        )?))
    }

    pub fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self {
        Self::from_base(ZnippyBase::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 header metadata (NEVRA epoch + the rpm-md `primary.xml`
    /// fields) from the typed rpm view — the header tag table the znippy
    /// `rpm_native` plugin parsed at ingest — keyed by `(name, version, release,
    /// arch)`. Empty when the archive carries no rpm sub-index (a mirrored repo, or
    /// a non-rpm placeholder), so the synthesis falls back to filename-only coords
    /// with `epoch=0` and no `<format>`, exactly as before.
    fn header_meta(&self) -> HashMap<(String, String, String, String), RpmMetaRow> {
        let mut map = HashMap::new();
        if let Some(archive) = self.base.typed() {
            if let Some(view) = archive.as_rpm() {
                for m in view.list_meta() {
                    map.insert(
                        (
                            m.name.clone(),
                            m.version.clone(),
                            m.release.clone(),
                            m.arch.clone(),
                        ),
                        m,
                    );
                }
            }
        }
        map
    }

    /// Parse an RPM filename into `(name, version, release, arch)`.
    /// Canonical NEVRA filename: `{name}-{version}-{release}.{arch}.rpm`.
    /// `arch` is the segment after the final `.`; `release` and `version` are the
    /// last two `-`-separated fields of the remainder (neither name nor version
    /// contains `-` in a valid NEVRA).
    fn parse_rpm_filename(fname: &str) -> Option<(String, String, String, String)> {
        let stem = fname.strip_suffix(".rpm")?;
        let (rest, arch) = stem.rsplit_once('.')?;
        let (name_ver, release) = rest.rsplit_once('-')?;
        let (name, version) = name_ver.rsplit_once('-')?;
        if name.is_empty() || version.is_empty() || release.is_empty() || arch.is_empty() {
            return None;
        }
        Some((
            name.to_string(),
            version.to_string(),
            release.to_string(),
            arch.to_string(),
        ))
    }

    /// The `.rpm` files in the archive, sorted by archive path (determinism).
    fn rpm_files(&self) -> Vec<String> {
        let mut files: Vec<String> = self
            .list_files()
            .into_iter()
            .filter(|f| f.ends_with(".rpm"))
            .collect();
        files.sort();
        files
    }

    /// Build `primary.xml` from the `.rpm` files present — one `<package>` per rpm
    /// with real SHA256 + size + served location. The `epoch` and the rpm-md
    /// `<summary>`/`<url>`/`<format>` (license, vendor, sourcerpm, provides,
    /// requires) are sourced from the znippy rpm view (the header tag table) when
    /// the archive carries the rpm sub-index; otherwise it falls back to filename
    /// NEVRA with `epoch=0` and no `<format>`, exactly as before.
    fn build_primary(&self) -> Vec<u8> {
        let files = self.rpm_files();
        let meta = self.header_meta();
        let mut pkgs = String::new();
        let mut count = 0u64;
        for f in &files {
            let Some(fname) = f.rsplit('/').next() else {
                continue;
            };
            let Some((name, version, release, arch)) = Self::parse_rpm_filename(fname) else {
                continue;
            };
            // Authoritative header metadata, in precedence order:
            //   1. the typed rpm view (the znippy `rpm_native` plugin's sub-index),
            //   2. the file's index row (a chain view, lane HOL-PERF-2: the header fields,
            //      size and sha256 the scan worker read at intake; the .rpm is not read),
            //      or for an archive without rows a NATIVE parse of the served `.rpm`
            //      header (works for mirrored/plugin-less archives), then
            //   3. filename-only NEVRA with epoch=0 and no <format>.
            let typed = meta.get(&(name.clone(), version.clone(), release.clone(), arch.clone()));
            let m_native;
            let (size, sha) = match (&self.rows, self.rows.as_ref().and_then(|r| r.row(f))) {
                (Some(_), Some(r)) => {
                    m_native = match &r.row {
                        holger_index_rows::IndexRow::Rpm {
                            epoch,
                            summary,
                            license,
                            url,
                            vendor,
                            sourcerpm,
                            provides,
                            requires,
                        } => {
                            let h = holger_index_rows::rpm::RpmHeader {
                                epoch: epoch.clone(),
                                summary: summary.clone(),
                                license: license.clone(),
                                url: url.clone(),
                                vendor: vendor.clone(),
                                sourcerpm: sourcerpm.clone(),
                                provides: provides.clone(),
                                requires: requires.clone(),
                            };
                            Some(header::meta_row(&name, &version, &release, &arch, h))
                        }
                        _ => None,
                    };
                    (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;
                    };
                    m_native = None;
                    (bytes.len() as u64, traits::sha256_hex(&bytes))
                }
                (None, _) => {
                    let Ok(bytes) = self.get_file(f) else {
                        continue;
                    };
                    m_native = if typed.is_some() {
                        None
                    } else {
                        header::parse_rpm_header(&bytes, &name, &version, &release, &arch)
                    };
                    (bytes.len() as u64, traits::sha256_hex(&bytes))
                }
            };
            count += 1;
            let m = typed.or(m_native.as_ref());
            let epoch = m
                .and_then(|m| m.epoch.clone())
                .unwrap_or_else(|| "0".to_string());
            pkgs.push_str("  <package type=\"rpm\">\n");
            pkgs.push_str(&format!(
                "    <name>{}</name>\n",
                nornir_escape::xml_attr(&name)
            ));
            pkgs.push_str(&format!(
                "    <arch>{}</arch>\n",
                nornir_escape::xml_attr(&arch)
            ));
            pkgs.push_str(&format!(
                "    <version epoch=\"{}\" ver=\"{}\" rel=\"{}\"/>\n",
                nornir_escape::xml_attr(&epoch),
                nornir_escape::xml_attr(&version),
                nornir_escape::xml_attr(&release)
            ));
            pkgs.push_str(&format!(
                "    <checksum type=\"sha256\" pkgid=\"YES\">{sha}</checksum>\n"
            ));
            // Authoritative rpm-md metadata from the header, when the view has it.
            if let Some(m) = m {
                if let Some(s) = &m.summary {
                    pkgs.push_str(&format!(
                        "    <summary>{}</summary>\n",
                        nornir_escape::xml_attr(s)
                    ));
                }
                if let Some(u) = &m.url {
                    pkgs.push_str(&format!("    <url>{}</url>\n", nornir_escape::xml_attr(u)));
                }
            }
            pkgs.push_str(&format!("    <size package=\"{size}\"/>\n"));
            pkgs.push_str(&format!(
                "    <location href=\"{}\"/>\n",
                nornir_escape::xml_attr(f)
            ));
            if let Some(m) = m {
                pkgs.push_str(&format_block(m));
            }
            pkgs.push_str("  </package>\n");
        }
        let mut out = String::new();
        out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
        out.push_str(&format!(
            "<metadata xmlns=\"http://linux.duke.edu/metadata/common\" \
             xmlns:rpm=\"http://linux.duke.edu/metadata/rpm\" packages=\"{count}\">\n"
        ));
        out.push_str(&pkgs);
        out.push_str("</metadata>\n");
        out.into_bytes()
    }

    /// Build `repomd.xml`, referencing the synthesized `primary.xml` by SHA256 +
    /// size. Served uncompressed, so `checksum` == `open-checksum`.
    fn build_repomd(&self) -> Vec<u8> {
        // dnf fetches `primary.xml.gz` (written once, lane HOL-PERF-2): the checksum and
        // size are the compressed file's, the open-checksum and open-size the XML's.
        let primary = self.primary();
        let gz = self.primary_gz();
        let (gz_sha, gz_size) = (traits::sha256_hex(&gz), gz.len());
        let sha = traits::sha256_hex(&primary);
        let size = primary.len();
        let mut out = String::new();
        out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
        out.push_str(
            "<repomd xmlns=\"http://linux.duke.edu/metadata/repo\" \
             xmlns:rpm=\"http://linux.duke.edu/metadata/rpm\">\n",
        );
        out.push_str("  <data type=\"primary\">\n");
        out.push_str(&format!(
            "    <checksum type=\"sha256\">{gz_sha}</checksum>\n"
        ));
        out.push_str(&format!(
            "    <open-checksum type=\"sha256\">{sha}</open-checksum>\n"
        ));
        out.push_str("    <location href=\"repodata/primary.xml.gz\"/>\n");
        out.push_str(&format!("    <size>{gz_size}</size>\n"));
        out.push_str(&format!("    <open-size>{size}</open-size>\n"));
        out.push_str("  </data>\n");
        out.push_str("</repomd>\n");
        out.into_bytes()
    }

    /// Cached `primary.xml`.
    fn primary(&self) -> traits::Bytes {
        self.cached("repodata/primary.xml", || self.build_primary())
    }

    /// Cached `primary.xml.gz` (the header's time is zero, so the bytes are the content's).
    fn primary_gz(&self) -> traits::Bytes {
        self.cached("repodata/primary.xml.gz", || {
            use std::io::Write as _;
            let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
            e.write_all(&self.primary()).expect("an in-memory write");
            e.finish().expect("an in-memory finish")
        })
    }

    /// Rendered once, handed out as the same bytes (lane HOL-PERF-2): a refcount, no copy.
    fn cached<F: FnOnce() -> Vec<u8>>(&self, key: &str, build: F) -> traits::Bytes {
        if let Ok(cache) = self.index_cache.lock() {
            if let Some(hit) = cache.get(key) {
                return hit.clone();
            }
        }
        let built = traits::Bytes::from(build());
        if let Ok(mut cache) = self.index_cache.lock() {
            cache.insert(key.to_string(), built.clone());
        }
        built
    }

    /// Synthesize the rpm-md index for `remainder` if it names a known index
    /// file. `None` for any other path (caller then 404s).
    fn synthesize_index(&self, remainder: &str) -> Option<(traits::Bytes, &'static str)> {
        match remainder {
            "repodata/repomd.xml" => Some((
                self.cached(remainder, || self.build_repomd()),
                "application/xml",
            )),
            "repodata/primary.xml" => Some((self.primary(), "application/xml")),
            "repodata/primary.xml.gz" => Some((self.primary_gz(), "application/gzip")),
            _ => None,
        }
    }
}

/// The rpm-md `<format>` block for one package — `rpm:license` / `rpm:vendor` /
/// `rpm:sourcerpm` plus `rpm:provides` / `rpm:requires` dependency entries —
/// emitted only for the fields the header actually carried. Returns an empty
/// string when the package has none, so `<package>` simply omits `<format>`.
fn format_block(m: &RpmMetaRow) -> String {
    let mut inner = String::new();
    if let Some(l) = &m.license {
        inner.push_str(&format!(
            "      <rpm:license>{}</rpm:license>\n",
            nornir_escape::xml_attr(l)
        ));
    }
    if let Some(v) = &m.vendor {
        inner.push_str(&format!(
            "      <rpm:vendor>{}</rpm:vendor>\n",
            nornir_escape::xml_attr(v)
        ));
    }
    if let Some(s) = &m.sourcerpm {
        inner.push_str(&format!(
            "      <rpm:sourcerpm>{}</rpm:sourcerpm>\n",
            nornir_escape::xml_attr(s)
        ));
    }
    let entries = |names: &[String]| -> String {
        names
            .iter()
            .map(|n| {
                format!(
                    "        <rpm:entry name=\"{}\"/>\n",
                    nornir_escape::xml_attr(n)
                )
            })
            .collect()
    };
    if !m.provides.is_empty() {
        inner.push_str("      <rpm:provides>\n");
        inner.push_str(&entries(&m.provides));
        inner.push_str("      </rpm:provides>\n");
    }
    // `rpmlib(...)` requires are rpm's own feature markers, provided by no package: createrepo
    // leaves them out, and a client resolving them against the repository refuses the package.
    let requires: Vec<String> = m
        .requires
        .iter()
        .filter(|r| !r.starts_with("rpmlib("))
        .cloned()
        .collect();
    if !requires.is_empty() {
        inner.push_str("      <rpm:requires>\n");
        inner.push_str(&entries(&requires));
        inner.push_str("      </rpm:requires>\n");
    }
    if inner.is_empty() {
        String::new()
    } else {
        format!("    <format>\n{inner}    </format>\n")
    }
}

fn content_type_for(path: &str) -> &'static str {
    if path.ends_with(".rpm") {
        "application/x-rpm"
    } else if path.ends_with(".gz") {
        "application/gzip"
    } else if path.ends_with(".xml") {
        "application/xml"
    } else {
        "application/octet-stream"
    }
}

impl RepositoryBackendTrait for RpmRepoZnippy {
    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> {
        use traits::znippy_base::{not_found_bytes, ok_bytes};
        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!("rpm 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
        //    repodata packed into the archive (real createrepo output 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 rpm-md index from the .rpm files, 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::Rpm
    }

    fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<traits::Bytes>> {
        // Match on the PARSED NEVRA, exact name + version — never a string
        // prefix (which made `zlib 1.3.1` collide with `zlib-1.3.10`). When the
        // id carries an arch in `namespace`, require it too. `rpm_files()` is
        // sorted, so the pick is deterministic across runs.
        for f in self.rpm_files() {
            let Some(fname) = f.rsplit('/').next() else {
                continue;
            };
            let Some((name, version, _release, arch)) = Self::parse_rpm_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 `.rpm` 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>> {
        let mut out = Vec::new();
        // 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();
        for f in self.rpm_files() {
            let Some(fname) = f.rsplit('/').next() else {
                continue;
            };
            let Some((name, version, _release, arch)) = Self::parse_rpm_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/x-rpm".into(),
                    // No mtime and no content digest in a znippy file table;
                    // the CHANGE TOKEN below is what the index CAN answer, and
                    // it reads no artifact byte.
                    ..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 traits::test_support::MemReader;

    #[test]
    fn caps_and_methods_agree() {
        traits::backend::conformance::check(&RpmRepoZnippy::new("e".into())).unwrap();
        let loaded = RpmRepoZnippy::with_reader(
            "l".into(),
            MemReader::new([("a-1-1.x86_64.rpm", b"x".to_vec())]),
        );
        assert!(loaded.caps().archive);
        traits::backend::conformance::check(&loaded).unwrap();
    }

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

    /// RED-when-broken guard on the shared `nornir_escape::xml_attr` consumed here
    /// (the deleted local `xml_escape` twin). repomd/primary attributes are
    /// single-quoted, so the escaper MUST keep the apos-inclusive 5-entity variant:
    /// `&`→`&amp;`, `<`→`&lt;`, `>`→`&gt;`, `"`→`&quot;`, `'`→`&apos;`. This asserts
    /// the emitted `<format>` block is BYTE-IDENTICAL to the old twin's output —
    /// including `&apos;`. Swap to plain `nornir_escape::xml` (drops `&apos;`) and
    /// the license row here fails, catching a repomd regression.
    #[test]
    fn format_block_xml_attr_escapes_all_five_entities_incl_apos() {
        let m = RpmMetaRow {
            name: "pkg".into(),
            version: "1.0".into(),
            release: "1".into(),
            arch: "x86_64".into(),
            epoch: None,
            summary: None,
            // Every sensitive char, apostrophe included.
            license: Some("GPL & \"MIT\" <v1> it's".into()),
            url: None,
            vendor: None,
            sourcerpm: None,
            provides: vec!["lib'a & <b>".into()],
            requires: vec![],
        };
        let block = format_block(&m);
        // Exact, byte-for-byte — `'` renders as `&apos;`, NOT `&#39;` nor a raw `'`.
        assert_eq!(
            block,
            "    <format>\n\
             \x20     <rpm:license>GPL &amp; &quot;MIT&quot; &lt;v1&gt; it&apos;s</rpm:license>\n\
             \x20     <rpm:provides>\n\
             \x20       <rpm:entry name=\"lib&apos;a &amp; &lt;b&gt;\"/>\n\
             \x20     </rpm:provides>\n\
             \x20   </format>\n",
            "format block must be byte-identical incl &apos;:\n{block}"
        );
    }

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

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

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

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

    /// **Capability row** (CLAUDE.md prod-vs-prototype: RPM header parse). The
    /// `repomd.xml`/`primary.xml` synthesis carries real SHA256, and the epoch +
    /// `<format>` fields now come from a real binary RPM header parse of the served
    /// bytes (the `header` module) when the typed rpm view is absent — filename
    /// NEVRA is only the last-resort fallback. Proven by
    /// [`native_header_parse_enriches_primary`]; emitted `ok=true`.
    #[test]
    fn rpm_header_parse_capability() {
        let rpm =
            super::header::tests_support::make_rpm(&[super::header::tests_support::Tag::Str(
                1014, "GPLv3+",
            )]);
        let parsed = super::header::parse_rpm_header(&rpm, "hello", "2.10", "1", "x86_64")
            .map(|m| m.license.as_deref() == Some("GPLv3+"))
            .unwrap_or(false);
        assert!(parsed, "native RPM header parse must read the license tag");
        #[cfg(feature = "testmatrix")]
        fstatus(
            "znippy-rpm",
            "binary_header_parse",
            parsed,
            "primary.xml epoch/<format> parsed from the real RPM header tag table; real SHA256",
        );
    }

    /// The native RPM header parse ENRICHES the synthesized `primary.xml`: served a
    /// bare `.rpm` (no typed rpm view, so `header_meta()` is empty), holger still
    /// surfaces the real epoch, `<summary>`/`<url>`, and the `<format>` block
    /// (license / provides / requires) parsed from the package's own header — not
    /// just the filename NEVRA with `epoch=0`. The capability the maturity row asserts.
    /// **`rpmlib(...)` requires are never listed** (lane HOL-MVP-FORMATS, measured with a real
    /// `rpmbuild` package and `dnf install` through holger 2026-10-30: "nothing provides
    /// rpmlib(CompressedFileNames) needed by hello-holger"). They are rpm's own feature
    /// markers, satisfied by rpm itself and provided by no package, so createrepo leaves them
    /// out of `primary.xml`; listing them makes every real package uninstallable.
    #[test]
    fn rpmlib_requires_are_never_listed_in_primary() {
        use super::header::tests_support::{make_rpm, Tag};
        let rpm = make_rpm(&[Tag::StrArray(
            1049,
            &[
                "rpmlib(CompressedFileNames)",
                "/bin/sh",
                "rpmlib(PayloadIsZstd)",
            ],
        )]);
        let reader = MemReader::new([("hello-holger-1.0.0-1.noarch.rpm", rpm)]);
        let repo = RpmRepoZnippy::with_reader("rpm-test".into(), reader);
        let (st, _h, body) = repo
            .handle_http2_request("GET", "/rpm-test/repodata/primary.xml", &[])
            .expect("ok");
        let primary = String::from_utf8(body).expect("utf8");
        assert_eq!(st, 200);
        assert!(
            primary.contains("<rpm:entry name=\"/bin/sh\"/>"),
            "a real requirement stays:\n{primary}"
        );
        assert!(
            !primary.contains("rpmlib("),
            "rpmlib() requires are never listed:\n{primary}"
        );
    }

    #[test]
    fn native_header_parse_enriches_primary() {
        use super::header::tests_support::{make_rpm, Tag};
        let rpm = make_rpm(&[
            Tag::Int32(1003, 7),                         // epoch
            Tag::Str(1004, "zlib compression library"),  // summary
            Tag::Str(1014, "Zlib"),                      // license
            Tag::Str(1020, "https://zlib.net"),          // url
            Tag::StrArray(1047, &["zlib", "libz.so.1"]), // provides
            Tag::StrArray(1049, &["libc.so.6"]),         // requires
        ]);
        let key = "Packages/zlib-1.3.1-2.fc40.x86_64.rpm";
        let reader = MemReader::new([(key, rpm)]);
        // `with_reader` ⇒ no typed archive view ⇒ header_meta() empty, so these
        // fields can ONLY come from the native header parse.
        let repo = RpmRepoZnippy::with_reader("rpm-test".into(), reader);
        assert!(
            repo.header_meta().is_empty(),
            "no typed view in with_reader mode"
        );

        let (st, _h, body) = repo
            .handle_http2_request("GET", "/rpm-test/repodata/primary.xml", &[])
            .expect("ok");
        let primary = String::from_utf8(body).expect("utf8");
        assert_eq!(st, 200);
        // Filename NEVRA still present…
        assert!(primary.contains("<name>zlib</name>"), "primary:\n{primary}");
        // …enriched with the natively-parsed epoch + rpm-md fields.
        assert!(primary.contains("epoch=\"7\""), "primary:\n{primary}");
        assert!(
            primary.contains("<summary>zlib compression library</summary>"),
            "primary:\n{primary}"
        );
        assert!(
            primary.contains("<url>https://zlib.net</url>"),
            "primary:\n{primary}"
        );
        assert!(
            primary.contains("<rpm:license>Zlib</rpm:license>"),
            "primary:\n{primary}"
        );
        assert!(
            primary.contains("<rpm:entry name=\"libz.so.1\"/>"),
            "primary:\n{primary}"
        );
        assert!(
            primary.contains("<rpm:entry name=\"libc.so.6\"/>"),
            "primary:\n{primary}"
        );

        #[cfg(feature = "testmatrix")]
        fstatus(
            "znippy-rpm",
            "native_header_parse_enriches_primary",
            primary.contains("epoch=\"7\"") && primary.contains("<rpm:license>Zlib</rpm:license>"),
            "RPM header parse surfaced epoch + <format> with no typed view",
        );
    }

    #[test]
    fn parse_nevra_filename() {
        let parsed = RpmRepoZnippy::parse_rpm_filename("zlib-1.3.1-2.fc40.x86_64.rpm");
        assert_eq!(
            parsed,
            Some((
                "zlib".into(),
                "1.3.1".into(),
                "2.fc40".into(),
                "x86_64".into()
            ))
        );
    }

    #[test]
    fn serves_injected_archive() {
        // Archive key is the dnf-style path-passthrough layout:
        //   Packages/{pkg}-{ver}-{rel}.{arch}.rpm
        let key = "Packages/zlib-1.3.1-2.fc40.x86_64.rpm";
        let expected = b"\xed\xab\xee\xdb-fake-rpm-payload".to_vec();
        let reader = MemReader::new([(key, expected.clone())]);
        let repo = RpmRepoZnippy::with_reader("rpm-test".to_string(), reader);

        // fetch() matches on "{name}-{version}" against the .rpm filename.
        let id = ArtifactId {
            namespace: None,
            name: "zlib".into(),
            version: "1.3.1".into(),
        };
        let fetched = repo.fetch(&id).expect("fetch ok");
        assert_eq!(fetched.as_deref(), Some(expected.as_slice()));

        // HTTP/2 hit: GET /{repo}/{key} -> 200, application/x-rpm, exact body.
        let url = format!("/{}/{}", "rpm-test", key);
        let (status, headers, body) = repo
            .handle_http2_request("GET", &url, &[])
            .expect("handle ok");
        assert_eq!(status, 200);
        assert_eq!(body, expected);
        let ctype = headers
            .iter()
            .find(|(k, _)| k == "Content-Type")
            .map(|(_, v)| v.as_str());
        assert_eq!(ctype, Some("application/x-rpm"));

        // HTTP/2 miss: unknown package -> 404.
        let miss_url = "/rpm-test/Packages/nope-9.9.9-1.fc40.x86_64.rpm";
        let (miss_status, _miss_headers, _miss_body) = repo
            .handle_http2_request("GET", miss_url, &[])
            .expect("handle ok");
        assert_eq!(miss_status, 404);

        // fetch() miss returns None.
        let miss_id = ArtifactId {
            namespace: None,
            name: "nope".into(),
            version: "9.9.9".into(),
        };
        let miss_fetch = repo.fetch(&miss_id).expect("fetch ok");
        assert!(miss_fetch.is_none());

        #[cfg(feature = "testmatrix")]
        fstatus(
            "znippy-rpm",
            "serves_injected_archive",
            fetched.as_deref() == Some(expected.as_slice())
                && status == 200
                && body == expected
                && ctype == Some("application/x-rpm")
                && miss_status == 404
                && miss_fetch.is_none(),
            &format!(
                "key={key} status={status} ctype={ctype:?} bytes={} miss_status={miss_status}",
                body.len()
            ),
        );
    }

    /// Native protocol: `repomd.xml` + `primary.xml` are synthesized from the
    /// `.rpm` files (real size + SHA256), and `repomd.xml` references the
    /// `primary.xml` it would serve by hash — the rpm-md index dnf resolves
    /// before downloading.
    #[test]
    fn synthesizes_rpm_md_index_from_packages() {
        let key = "Packages/zlib-1.3.1-2.fc40.x86_64.rpm";
        let rpm_bytes = b"\xed\xab\xee\xdb-fake-rpm-payload".to_vec();
        let reader = MemReader::new([(key, rpm_bytes.clone())]);
        let repo = RpmRepoZnippy::with_reader("rpm-test".to_string(), reader);

        // primary.xml: 200 + application/xml, one <package> with the NEVRA fields,
        // real size + SHA256, and a <location> pointing at the served rpm path.
        let (pst, ph, pbody) = repo
            .handle_http2_request("GET", "/rpm-test/repodata/primary.xml", &[])
            .expect("ok");
        let primary = String::from_utf8(pbody.clone()).expect("utf8");
        let pctype = ph
            .iter()
            .find(|(k, _)| k == "Content-Type")
            .map(|(_, v)| v.as_str());
        let expected_sha = traits::sha256_hex(&rpm_bytes);
        let primary_ok = pst == 200
            && pctype == Some("application/xml")
            && primary.contains("packages=\"1\"")
            && primary.contains("<name>zlib</name>")
            && primary.contains("<arch>x86_64</arch>")
            && primary.contains("ver=\"1.3.1\" rel=\"2.fc40\"")
            && primary.contains(&format!("pkgid=\"YES\">{expected_sha}</checksum>"))
            && primary.contains(&format!("<size package=\"{}\"/>", rpm_bytes.len()))
            && primary.contains(&format!("<location href=\"{key}\"/>"));
        assert!(primary_ok, "primary.xml malformed:\n{primary}");

        // repomd.xml: 200, names primary.xml.gz (lane HOL-PERF-2) and opens to the
        // SHA256 of the bytes the server would (and just did) serve for primary.xml.
        let (rst, _rh, rbody) = repo
            .handle_http2_request("GET", "/rpm-test/repodata/repomd.xml", &[])
            .expect("ok");
        let repomd = String::from_utf8(rbody).expect("utf8");
        let primary_sha = traits::sha256_hex(&pbody);
        let repomd_ok = rst == 200
            && repomd.contains("<location href=\"repodata/primary.xml.gz\"/>")
            && repomd.contains(&format!(
                "<open-checksum type=\"sha256\">{primary_sha}</open-checksum>"
            ))
            && repomd.contains(&format!("<open-size>{}</open-size>", pbody.len()));
        assert!(repomd_ok, "repomd.xml malformed:\n{repomd}");

        #[cfg(feature = "testmatrix")]
        {
            fstatus(
                "znippy-rpm",
                "synthesizes_primary_xml",
                primary_ok,
                &format!("status={pst} sha={expected_sha}"),
            );
            fstatus(
                "znippy-rpm",
                "synthesizes_repomd_xml",
                repomd_ok,
                &format!("status={rst} primary_sha={primary_sha}"),
            );
        }
    }

    /// T21: fetch must not collide a version prefix (`1.3.1` vs `1.3.10`), must
    /// honour the arch hint, and `primary.xml` must carry one `<package>` per rpm
    /// with the real per-body size + sha256 and no two sharing location/sha.
    #[test]
    fn fetch_no_version_collision_and_multi_package_primary() {
        let b131 = b"zlib-1.3.1-body".to_vec();
        let b1310 = b"zlib-1.3.10-body-different".to_vec();
        let bi686 = b"zlib-1.3.1-i686-body".to_vec();
        let reader = MemReader::new([
            ("Packages/zlib-1.3.1-2.fc40.x86_64.rpm", b131.clone()),
            ("Packages/zlib-1.3.10-1.fc40.x86_64.rpm", b1310.clone()),
            ("Packages/zlib-1.3.1-2.fc40.i686.rpm", bi686.clone()),
        ]);
        let repo = RpmRepoZnippy::with_reader("rpm-test".into(), reader);

        // EXACT version: 1.3.1 must never return the 1.3.10 bytes.
        let id = ArtifactId {
            namespace: None,
            name: "zlib".into(),
            version: "1.3.1".into(),
        };
        let got = repo.fetch(&id).expect("ok");
        // Without an arch hint either 1.3.1 build is acceptable, but never 1.3.10.
        let got = got.expect("a 1.3.1 build is found");
        assert_ne!(
            got, b1310,
            "fetch(zlib,1.3.1) must NOT return the 1.3.10 body"
        );
        assert!(
            got == b131 || got == bi686,
            "fetch returned a real 1.3.1 build"
        );

        // Arch hint pins the exact build.
        let id_i686 = ArtifactId {
            namespace: Some("i686".into()),
            name: "zlib".into(),
            version: "1.3.1".into(),
        };
        assert_eq!(
            repo.fetch(&id_i686).expect("ok").as_deref(),
            Some(bi686.as_slice())
        );
        let id_x86 = ArtifactId {
            namespace: Some("x86_64".into()),
            name: "zlib".into(),
            version: "1.3.1".into(),
        };
        assert_eq!(
            repo.fetch(&id_x86).expect("ok").as_deref(),
            Some(b131.as_slice())
        );

        // primary.xml: one <package> per rpm, sizes + sha256 match the bodies,
        // no two packages share a location or a checksum.
        let (pst, _ph, pbody) = repo
            .handle_http2_request("GET", "/rpm-test/repodata/primary.xml", &[])
            .expect("ok");
        let primary = String::from_utf8(pbody).expect("utf8");
        let pkg_count = primary.matches("<package type=\"rpm\">").count();
        let collision_ok = pst == 200
            && primary.contains("packages=\"3\"")
            && pkg_count == 3
            && primary.contains(&format!("<size package=\"{}\"/>", b131.len()))
            && primary.contains(&format!("<size package=\"{}\"/>", b1310.len()))
            && primary.contains(&traits::sha256_hex(&b131))
            && primary.contains(&traits::sha256_hex(&b1310))
            && primary.contains(&traits::sha256_hex(&bi686));
        assert!(
            collision_ok,
            "primary.xml missing a per-rpm row:\n{primary}"
        );

        #[cfg(feature = "testmatrix")]
        fstatus(
            "znippy-rpm",
            "fetch_no_version_collision",
            collision_ok && got != b1310,
            &format!("3 packages, exact-version fetch, arch-pinned (count={pkg_count})"),
        );
    }

    /// **A chain view's `primary.xml` is rendered from the rows** (lane HOL-PERF-2): byte
    /// for byte the index the in-process header walk gives, and not one `.rpm` is read.
    #[test]
    fn with_rows_the_primary_index_reads_no_rpm_and_is_the_parsed_one() {
        use super::header::tests_support::{make_rpm, Tag};
        use traits::test_support::{CountingReader, MemReader};
        let rpm = make_rpm(&[
            Tag::Int32(1003, 1),
            Tag::Str(1004, "A friendly greeter"),
            Tag::Str(1014, "GPLv3+"),
            Tag::StrArray(1047, &["hello"]),
            Tag::StrArray(1049, &["libc.so.6"]),
        ]);
        let path = "Packages/hello-2.10-1.x86_64.rpm";
        let parsed = RpmRepoZnippy::with_reader("r".into(), MemReader::new([(path, rpm.clone())]));
        let counting = CountingReader::new(MemReader::new([(path, rpm.clone())]));
        let viewed = RpmRepoZnippy::with_reader_and_rows(
            "r".into(),
            counting.clone(),
            holger_index_rows::MapRows::of("rpm", &[(path, &rpm)]),
        );
        for url in ["/r/repodata/primary.xml", "/r/repodata/repomd.xml"] {
            let want = parsed.handle_http2_request("GET", url, &[]).unwrap();
            assert_eq!(want.0, 200, "{url}");
            let got = viewed.handle_http2_request("GET", url, &[]).unwrap();
            assert_eq!(
                String::from_utf8_lossy(&got.2),
                String::from_utf8_lossy(&want.2),
                "{url}, byte for byte"
            );
        }
        assert!(String::from_utf8_lossy(
            &viewed
                .handle_http2_request("GET", "/r/repodata/primary.xml", &[])
                .unwrap()
                .2
        )
        .contains("A friendly greeter"));
        assert_eq!(counting.hits(), 0, "no .rpm is read to index it");
    }

    /// **Rendered once, handed out as the same bytes** (lane HOL-PERF-2, X5).
    #[test]
    fn the_rpm_md_index_is_one_allocation_handed_out_again() {
        use super::header::tests_support::{make_rpm, Tag};
        use traits::test_support::MemReader;
        let rpm = make_rpm(&[Tag::Str(1004, "a greeter")]);
        let repo = RpmRepoZnippy::with_reader(
            "r".into(),
            MemReader::new([("Packages/hello-2.10-1.x86_64.rpm", rpm)]),
        );
        for url in ["/r/repodata/primary.xml", "/r/repodata/repomd.xml"] {
            let a = repo.handle_http2_request_bytes("GET", url, &[]).unwrap();
            let b = repo.handle_http2_request_bytes("GET", url, &[]).unwrap();
            assert_eq!(a.0, 200, "{url}");
            assert_eq!(
                a.2.as_ptr(),
                b.2.as_ptr(),
                "{url}: the same bytes, not a copy"
            );
        }
    }

    /// **`primary.xml.gz` is written once and named by `repomd.xml`** (lane HOL-PERF-2,
    /// step 6): dnf moves about a tenth of the bytes; `primary.xml` stays served.
    #[test]
    fn primary_xml_gz_is_written_once_and_named_by_repomd() {
        use super::header::tests_support::{make_rpm, Tag};
        use std::io::Read as _;
        use traits::test_support::MemReader;
        let repo = RpmRepoZnippy::with_reader(
            "r".into(),
            MemReader::new([(
                "Packages/hello-2.10-1.x86_64.rpm",
                make_rpm(&[Tag::Str(1004, "a greeter")]),
            )]),
        );
        let plain = repo
            .handle_http2_request_bytes("GET", "/r/repodata/primary.xml", &[])
            .unwrap()
            .2;
        let gz = repo
            .handle_http2_request_bytes("GET", "/r/repodata/primary.xml.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[..], "primary.xml.gz is primary.xml");
        assert_eq!(
            gz.2.as_ptr(),
            repo.handle_http2_request_bytes("GET", "/r/repodata/primary.xml.gz", &[])
                .unwrap()
                .2
                .as_ptr(),
            "written once"
        );
        let repomd = String::from_utf8(
            repo.handle_http2_request("GET", "/r/repodata/repomd.xml", &[])
                .unwrap()
                .2,
        )
        .unwrap();
        assert!(
            repomd.contains("<location href=\"repodata/primary.xml.gz\"/>"),
            "{repomd}"
        );
        assert!(
            repomd.contains(&format!(
                "<checksum type=\"sha256\">{}</checksum>",
                traits::sha256_hex(&gz.2)
            )),
            "{repomd}"
        );
        assert!(
            repomd.contains(&format!(
                "<open-checksum type=\"sha256\">{}</open-checksum>",
                traits::sha256_hex(&plain)
            )),
            "{repomd}"
        );
    }
}