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

nordisk/holger

server/index-rows/src/deb.rs

//! Native `.deb` control-archive parse — the `ar` + `control.tar.*` reader that
//! extracts the authoritative Debian `control` stanza (Depends / Maintainer /
//! Description) straight from the served package bytes.
//!
//! A `.deb` is an `ar(5)` archive with exactly three members, in order:
//! `debian-binary`, `control.tar.{,gz,xz,zst}`, `data.tar.{,gz,xz,zst}`. The
//! control tarball holds `./control` (the single-stanza control file). We only
//! need the control member, so we hand-parse the `ar` container (trivial 60-byte
//! headers — no external crate), pick the codec from the member's extension,
//! decompress it, and untar just `control`.
//!
//! This is the fallback holger uses when the archive carries no typed deb
//! sub-index (a mirrored repo of bare `.deb`s, or one built without the znippy
//! `deb_native` plugin): the fields come from the real control file rather than
//! being derived from the pool filename.

use std::io::Read;

/// The `ar` global magic that opens every `.deb`.
const AR_MAGIC: &[u8] = b"!<arch>\n";
/// Per-member `ar` header size.
const AR_HEADER: usize = 60;
/// Refuse a control tarball larger than this once decompressed — a `control`
/// file is a few KiB; anything vast is a decompression-bomb, not real metadata.
const MAX_CONTROL_TAR: u64 = 16 * 1024 * 1024;

/// Extract the raw `control` stanza from `.deb` bytes, or `None` if the bytes
/// aren't a well-formed `.deb`, carry no `control.tar*` member, or that member
/// has no `control` entry / an unsupported codec. Never panics on malformed
/// input — a hostile archive just yields `None` (the caller then falls back to
/// filename-derived fields).
pub fn control_from_deb(deb: &[u8]) -> Option<String> {
    crate::counted();
    let raw = control_tar_member(deb)?;
    let (name, body) = raw;
    let tar_bytes = decompress_control_tar(&name, &body)?;
    control_from_tar(&tar_bytes)
}

/// Walk the `ar` members and return the `(member_name, raw_member_bytes)` of the
/// first whose name starts `control.tar`. Bounds-checked throughout.
fn control_tar_member(deb: &[u8]) -> Option<(String, Vec<u8>)> {
    if deb.len() < AR_MAGIC.len() || &deb[..AR_MAGIC.len()] != AR_MAGIC {
        return None;
    }
    let mut pos = AR_MAGIC.len();
    while pos + AR_HEADER <= deb.len() {
        let header = &deb[pos..pos + AR_HEADER];
        // Name is the first 16 bytes, space-padded; Debian/GNU may append a `/`.
        let name = std::str::from_utf8(&header[0..16])
            .ok()?
            .trim_end()
            .trim_end_matches('/')
            .to_string();
        // Size is a decimal ASCII string at bytes 48..58.
        let size: usize = std::str::from_utf8(&header[48..58])
            .ok()?
            .trim()
            .parse()
            .ok()?;
        let data_start = pos + AR_HEADER;
        let data_end = data_start.checked_add(size)?;
        if data_end > deb.len() {
            return None;
        }
        if name.starts_with("control.tar") {
            return Some((name, deb[data_start..data_end].to_vec()));
        }
        // Members are padded to an even boundary with a trailing `\n`.
        pos = data_end + (size & 1);
    }
    None
}

/// Decompress a `control.tar*` member per its extension. `control.tar` (no codec)
/// is returned verbatim; `.gz`/`.xz`/`.zst` are inflated. Unknown ⇒ `None`.
fn decompress_control_tar(member_name: &str, body: &[u8]) -> Option<Vec<u8>> {
    let capped = |mut r: Box<dyn Read>| -> Option<Vec<u8>> {
        let mut out = Vec::new();
        r.by_ref()
            .take(MAX_CONTROL_TAR)
            .read_to_end(&mut out)
            .ok()?;
        Some(out)
    };
    match member_name {
        "control.tar" => Some(body.to_vec()),
        "control.tar.gz" => capped(Box::new(flate2::read::GzDecoder::new(body))),
        "control.tar.xz" => capped(Box::new(liblzma::read::XzDecoder::new(body))),
        "control.tar.zst" => capped(Box::new(zstd::stream::read::Decoder::new(body).ok()?)),
        _ => None,
    }
}

/// Pull `./control` (or `control`) out of the decompressed control tarball and
/// return its UTF-8 body. `None` if absent or non-UTF-8.
fn control_from_tar(tar_bytes: &[u8]) -> Option<String> {
    let mut archive = tar::Archive::new(tar_bytes);
    let entries = archive.entries().ok()?;
    for entry in entries {
        let entry = entry.ok()?;
        let path = entry.path().ok()?;
        let is_control = path.file_name().map(|n| n == "control").unwrap_or(false)
            && matches!(path.to_string_lossy().as_ref(), "control" | "./control");
        if is_control {
            let mut buf = String::new();
            entry.take(MAX_CONTROL_TAR).read_to_string(&mut buf).ok()?;
            return Some(buf);
        }
    }
    None
}

/// Test-only fixtures shared with the crate's `lib.rs` integration tests (which
/// need a real `.deb` to prove the native parse enriches the synthesized index).
#[cfg(any(test, feature = "fixtures"))]
pub mod tests_support {
    use super::AR_MAGIC;
    use std::io::Write;

    /// A one-file `control.tar` carrying `./control` with `body`.
    pub fn control_tar(body: &str) -> Vec<u8> {
        let mut builder = tar::Builder::new(Vec::new());
        let mut header = tar::Header::new_gnu();
        header.set_path("./control").unwrap();
        header.set_size(body.len() as u64);
        header.set_mode(0o644);
        header.set_cksum();
        builder.append(&header, body.as_bytes()).unwrap();
        builder.into_inner().unwrap()
    }

    /// Assemble a minimal well-formed `.deb`: `ar` magic + `debian-binary` +
    /// `control.tar<ext>` (`control_bytes`) + `data.tar`.
    pub fn make_deb(control_ext: &str, control_bytes: &[u8]) -> Vec<u8> {
        fn member(out: &mut Vec<u8>, name: &str, data: &[u8]) {
            let mut header = format!(
                "{name:<16}0           0     0     100644  {:<10}",
                data.len()
            );
            header.push('`');
            header.push('\n');
            assert_eq!(header.len(), 60, "ar header must be 60 bytes: {header:?}");
            out.extend_from_slice(header.as_bytes());
            out.extend_from_slice(data);
            if data.len() % 2 == 1 {
                out.push(b'\n');
            }
        }
        let mut out = Vec::new();
        out.extend_from_slice(AR_MAGIC);
        member(&mut out, "debian-binary", b"2.0\n");
        member(
            &mut out,
            &format!("control.tar{control_ext}"),
            control_bytes,
        );
        member(&mut out, "data.tar", b"\0\0\0\0");
        out
    }

    /// A realistic gzip-compressed `.deb` for `hello 2.10-2 amd64` carrying a full
    /// control stanza (Depends / Maintainer / Description) — the fixture the lib
    /// integration test serves to prove the native parse enriches `Packages`.
    pub fn sample_deb() -> Vec<u8> {
        const STANZA: &str = "Package: hello\nVersion: 2.10-2\nArchitecture: amd64\nMaintainer: Santiago <santiago@debian.org>\nDepends: libc6 (>= 2.14)\nDescription: friendly GNU greeter\n";
        let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
        enc.write_all(&control_tar(STANZA)).unwrap();
        let gz = enc.finish().unwrap();
        make_deb(".gz", &gz)
    }
}

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

    /// Build a one-file `control.tar` carrying `./control` with `body`.
    fn control_tar(body: &str) -> Vec<u8> {
        let mut builder = tar::Builder::new(Vec::new());
        let mut header = tar::Header::new_gnu();
        header.set_path("./control").unwrap();
        header.set_size(body.len() as u64);
        header.set_mode(0o644);
        header.set_cksum();
        builder.append(&header, body.as_bytes()).unwrap();
        builder.into_inner().unwrap()
    }

    /// Assemble a minimal but well-formed `.deb`: `ar` magic + the three members
    /// (`debian-binary`, `control.tar<ext>`, `data.tar`). `control_bytes` is the
    /// already-encoded control member; `ext` names it.
    fn make_deb(control_ext: &str, control_bytes: &[u8]) -> Vec<u8> {
        fn member(out: &mut Vec<u8>, name: &str, data: &[u8]) {
            let mut header = format!(
                "{name:<16}0           0     0     100644  {:<10}",
                data.len()
            );
            header.push('`');
            header.push('\n');
            assert_eq!(
                header.len(),
                AR_HEADER,
                "ar header must be 60 bytes: {header:?}"
            );
            out.extend_from_slice(header.as_bytes());
            out.extend_from_slice(data);
            if data.len() % 2 == 1 {
                out.push(b'\n');
            }
        }
        let mut out = Vec::new();
        out.extend_from_slice(AR_MAGIC);
        member(&mut out, "debian-binary", b"2.0\n");
        member(
            &mut out,
            &format!("control.tar{control_ext}"),
            control_bytes,
        );
        member(&mut out, "data.tar", b"\0\0\0\0");
        out
    }

    const STANZA: &str = "Package: hello\nVersion: 2.10-2\nArchitecture: amd64\nMaintainer: Santiago <santiago@debian.org>\nDepends: libc6 (>= 2.14)\nDescription: example package\n";

    #[test]
    fn parses_uncompressed_control_tar() {
        let deb = make_deb("", &control_tar(STANZA));
        let got = control_from_deb(&deb).expect("control parsed");
        assert!(got.contains("Package: hello"));
        assert!(got.contains("Depends: libc6 (>= 2.14)"));
        assert!(got.contains("Maintainer: Santiago"));
    }

    #[test]
    fn parses_gzip_control_tar() {
        let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
        enc.write_all(&control_tar(STANZA)).unwrap();
        let gz = enc.finish().unwrap();
        let deb = make_deb(".gz", &gz);
        let got = control_from_deb(&deb).expect("gzip control parsed");
        assert!(got.contains("Package: hello"));
        assert!(got.contains("Description: example package"));
    }

    #[test]
    fn parses_xz_control_tar() {
        let tar = control_tar(STANZA);
        let mut enc = liblzma::write::XzEncoder::new(Vec::new(), 6);
        enc.write_all(&tar).unwrap();
        let xz = enc.finish().unwrap();
        let deb = make_deb(".xz", &xz);
        let got = control_from_deb(&deb).expect("xz control parsed");
        assert!(got.contains("Package: hello"));
    }

    #[test]
    fn parses_zstd_control_tar() {
        let zst = zstd::stream::encode_all(&control_tar(STANZA)[..], 3).unwrap();
        let deb = make_deb(".zst", &zst);
        let got = control_from_deb(&deb).expect("zstd control parsed");
        assert!(got.contains("Version: 2.10-2"));
    }

    #[test]
    fn non_deb_bytes_yield_none() {
        assert!(control_from_deb(b"not a deb at all").is_none());
        assert!(control_from_deb(b"").is_none());
        // Right magic, truncated header.
        assert!(control_from_deb(AR_MAGIC).is_none());
    }

    #[test]
    fn deb_without_control_member_yields_none() {
        // A `.deb` carrying only debian-binary + data.tar (no control.tar*).
        let mut out = Vec::new();
        out.extend_from_slice(AR_MAGIC);
        let name = "debian-binary   ";
        let data = b"2.0\n";
        let mut header = format!("{name}0           0     0     100644  {:<10}", data.len());
        header.push('`');
        header.push('\n');
        out.extend_from_slice(header.as_bytes());
        out.extend_from_slice(data);
        assert!(control_from_deb(&out).is_none());
    }

    #[test]
    fn unsupported_codec_yields_none() {
        let deb = make_deb(".lz4", &control_tar(STANZA));
        assert!(control_from_deb(&deb).is_none());
    }
}