repro-env 0.4.4

Dependency lockfiles for reproducible build environments 📦🔒
Documentation
use crate::errors::*;
use crate::lockfile::PackageLock;
use data_encoding::BASE64;
use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
use sequoia_openpgp::Packet;
use std::cmp;
use std::time;
use std::time::SystemTime;

pub fn parse_timestamp_from_sig(buf: &[u8]) -> Result<Option<time::SystemTime>> {
    let mut ppr = PacketParser::from_bytes(buf)?;

    while let PacketParserResult::Some(pp) = ppr {
        let (packet, next_ppr) = pp.recurse()?;
        ppr = next_ppr;
        debug!("Found packet in pgp data: {packet:?}");
        let Packet::Signature(sig) = &packet else {
            continue;
        };
        let Some(time) = sig.signature_creation_time() else {
            continue;
        };
        return Ok(Some(time));
    }

    Ok(None)
}

pub fn find_max_signature_time<'a, I: Iterator<Item = &'a PackageLock>>(
    pkgs: I,
) -> Result<Option<SystemTime>> {
    let mut current_max = None;

    for pkg in pkgs {
        let base64 = pkg
            .signature
            .as_ref()
            .context("Package in dependency lockfile is missing signature")?;
        let signature = BASE64
            .decode(base64.as_bytes())
            .with_context(|| anyhow!("Failed to decode signature as base64: {base64:?}"))?;

        match (parse_timestamp_from_sig(&signature), &mut current_max) {
            (Ok(Some(time)), Some(max)) => {
                *max = cmp::max(*max, time);
            }
            (Ok(Some(time)), max) => *max = Some(time),
            (Ok(None), _) => (),
            (Err(err), _) => {
                warn!("Failed to parse timestamp from signature {base64:?}: {err:#?}")
            }
        }
    }

    Ok(current_max)
}

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

    #[test]
    fn test_parse_sig() {
        let buf = BASE64.decode(b"iHUEABYKAB0WIQQEKYl95fO9rFN6MGltQr3RFuAGjwUCZcU7FAAKCRBtQr3RFuAGj4Y4AQCKsihdyJWyNGBwQ9Kd5AmenehuvR4xfFOCjIOndQCYhwD+NFzEjbwraHHVtEjQh4HtrnZPc0JplQvM5zRT3gDCawE=").unwrap();
        let time = parse_timestamp_from_sig(&buf).unwrap().unwrap();
        let expected = time::UNIX_EPOCH
            .checked_add(time::Duration::from_secs(1707424532))
            .unwrap();
        assert_eq!(time, expected);
    }

    #[test]
    fn test_max_signature_time() {
        let pkgs = [
            PackageLock {
                name: "archlinux-keyring".to_string(),
                version: "20230704-1".to_string(),
                system: "archlinux".to_string(),
                url: "https://archive.archlinux.org/packages/a/archlinux-keyring/archlinux-keyring-20230704-1-any.pkg.tar.zst".to_string(),
                provides: vec![],
                sha256: "6a3d2acaa396c4bd72fe3f61a3256d881e3fc2cf326113cf331f168e36dd9a3c".to_string(),
                signature: Some(
"iHUEABYIAB0WIQQEKYl95fO9rFN6MGltQr3RFuAGjwUCZKPPXgAKCRBtQr3RFuAGj9oXAP94RQ1sKD53/RxVYlVEEOjKHvOmrWvDkt1veMYygnlnIgD+MLg/TT6d71kE8F08+JH+EcnG7wQow5Xr/qBo1VPLdgQ=".to_string()),
                installed: false,
            },
            PackageLock {
                name: "binutils".to_string(),
                version: "2.40-6".to_string(),
                system: "archlinux".to_string(),
                url: "https://archive.archlinux.org/packages/b/binutils/binutils-2.40-6-x86_64.pkg.tar.zst".to_string(),
                provides: vec![],
                sha256: "b65fd16001578e10b602e577a8031cbfffc1164caf47ed9ba00c60d804519430".to_string(),
                signature: Some(
"iNUEABYKAH0WIQQFx3danouXdAf+COadTFqhVCbaCgUCZG6Rg18UgAAAAAAuAChpc3N1ZXItZnByQG5vdGF0aW9ucy5vcGVucGdwLmZpZnRoaG9yc2VtYW4ubmV0MDVDNzc3NUE5RThCOTc3NDA3RkUwOEU2OUQ0QzVBQTE1NDI2REEwQQAKCRCdTFqhVCbaCge2AQD/LGBeHRaeO8xh4E/bAYfqd1O/OFqk2DrQBJ73cdKl2gD9EC8p4U/cXQK8V774m6LSS50usH5pxcQWEq/H0SF+FgM=".to_string()),
                installed: false,
            }
        ];

        let time = find_max_signature_time(pkgs.iter()).unwrap();
        let expected = time::UNIX_EPOCH
            .checked_add(time::Duration::from_secs(1688457054))
            .unwrap();
        assert_eq!(time, Some(expected));
    }
}