repro-env 0.2.0

Dependency lockfiles for a reproducible build environment 📦🔒
Documentation
use crate::errors::*;
use crate::pkgs::Pkg;
use std::io::{BufRead, BufReader, Read};

pub enum Compression {
    Xz,
    Zstd,
    None,
}

pub fn detect_compression(bytes: &[u8]) -> Compression {
    let mime = tree_magic_mini::from_u8(bytes);
    debug!("Detected mimetype for possibly compressed data: {:?}", mime);

    match mime {
        "application/x-xz" => Compression::Xz,
        "application/zstd" => Compression::Zstd,
        _ => Compression::None,
    }
}

pub fn parse_pkginfo<R: Read>(reader: R) -> Result<Pkg> {
    let reader = BufReader::new(reader);

    let mut name = None;
    let mut version = None;

    for line in reader.lines() {
        let line = line?;

        if let Some(value) = line.strip_prefix("pkgname = ") {
            name = Some(value.to_string());
        } else if let Some(value) = line.strip_prefix("pkgver = ") {
            version = Some(value.to_string());
        }
    }

    Ok(Pkg {
        name: name.context("Could not find pkgname in .PKGINFO")?,
        version: version.context("Could not find pkgver in .PKGINFO")?,
    })
}

pub fn parse_tar<R: Read>(reader: R) -> Result<Pkg> {
    let mut tar = tar::Archive::new(reader);
    for entry in tar.entries()? {
        let entry = entry?;
        let path = entry.path()?;
        if path.to_str() == Some(".PKGINFO") {
            return parse_pkginfo(entry);
        }
    }
    bail!("Failed to find .PKGINFO in package file")
}

pub fn parse(reader: &[u8]) -> Result<Pkg> {
    match detect_compression(reader) {
        Compression::Xz => {
            let mut buf = Vec::new();
            lzma_rs::xz_decompress(&mut &reader[..], &mut buf)?;
            parse_tar(&buf[..])
        }
        Compression::Zstd => {
            let decoder = ruzstd::StreamingDecoder::new(reader)?;
            parse_tar(decoder)
        }
        Compression::None => parse_tar(reader),
    }
}

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

    #[test]
    fn test_parse_pkg() -> Result<()> {
        let archive = {
            let data = br#"# Generated by makepkg 6.0.2
# using fakeroot version 1.31
pkgname = gcc
pkgbase = gcc
pkgver = 13.1.1-1
pkgdesc = The GNU Compiler Collection - C and C++ frontends
url = https://gcc.gnu.org
builddate = 1682849478
packager = Frederik Schwan <freswa@archlinux.org>
size = 190564290
arch = x86_64
license = GPL3
license = LGPL
license = FDL
license = custom
replaces = gcc-multilib
provides = gcc-multilib
depend = gcc-libs=13.1.1-1
depend = binutils>=2.28
depend = libmpc
depend = zstd
depend = libisl.so=23-64
optdepend = lib32-gcc-libs: for generating code for 32-bit ABI
makedepend = binutils
makedepend = doxygen
makedepend = gcc-ada
makedepend = gcc-d
makedepend = git
makedepend = lib32-glibc
makedepend = lib32-gcc-libs
makedepend = libisl
makedepend = libmpc
makedepend = python
makedepend = zstd
checkdepend = dejagnu
checkdepend = expect
checkdepend = inetutils
checkdepend = python-pytest
checkdepend = tcl
"#;

            let mut tar = tar::Builder::new(Vec::new());
            let mut header = tar::Header::new_gnu();
            header.set_path(".PKGINFO")?;
            header.set_size(data.len() as u64);
            header.set_cksum();
            tar.append(&header, &data[..])?;
            tar.into_inner()?
        };

        let mut buf = Vec::new();
        lzma_rs::xz_compress(&mut &archive[..], &mut buf)?;

        let pkg = parse(&buf)?;
        assert_eq!(
            pkg,
            Pkg {
                name: "gcc".to_string(),
                version: "13.1.1-1".to_string(),
            }
        );

        Ok(())
    }
}