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());
}
}