repro-env 0.3.3

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

pub enum Compression {
    Xz,
    Zstd,
    None,
}

pub fn detect_compression<R: Read>(mut reader: R) -> Result<Compression> {
    let mut buf = [0u8; 6];

    reader
        .read_exact(&mut buf)
        .context("Failed to read magic bytes from archive")?;

    if buf.starts_with(&[0x28, 0xB5, 0x2F, 0xFD]) {
        Ok(Compression::Zstd)
    } else if buf.starts_with(&[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]) {
        Ok(Compression::Xz)
    } else {
        Ok(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<R: Read>(reader: R) -> Result<Pkg> {
    let mut reader = BufPeekReader::new(reader);
    match detect_compression(reader.peek())? {
        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[..]).context("Failed to parse package")?;
        assert_eq!(
            pkg,
            Pkg {
                name: "gcc".to_string(),
                version: "13.1.1-1".to_string(),
            }
        );

        Ok(())
    }
}