wolfpack 0.3.1

A package manager and a build tool that supports major package formats (deb, RPM, ipk, pkg, MSIX).
Documentation
use fs_err::File;
use std::io::Error;
use std::io::Write;
use std::path::Path;

use flate2::write::ZlibEncoder;
use flate2::Compression;
use stuckliste::receipt::ReceiptBuilder;
use tempfile::TempDir;
pub use zar::rsa::RsaPrivateKey as SigningKey;
pub use zar::rsa::RsaPublicKey as VerifyingKey;
pub use zar::ChecksumAlgo;
pub use zar::RsaSigner as PackageSigner;

use crate::macos::xml;

#[cfg_attr(test, derive(PartialEq, Eq, Clone, Debug))]
pub struct Package {
    pub identifier: String,
    pub version: String,
}

impl Package {
    pub fn write<W: Write, P: AsRef<Path>>(
        &self,
        writer: W,
        directory: P,
        signer: &PackageSigner,
    ) -> Result<(), Error> {
        let info = xml::PackageInfo {
            format_version: 2,
            install_location: Some("/".into()),
            identifier: self.identifier.clone(),
            version: self.version.clone(),
            generator_version: Some(GENERATOR_VERSION.into()),
            auth: xml::Auth::Root,
            payload: xml::Payload {
                number_of_files: 0,
                install_kb: 0,
            },
            relocatable: Default::default(),
            bundles: Default::default(),
            bundle_version: Default::default(),
            upgrade_bundle: Default::default(),
            update_bundle: Default::default(),
            atomic_update_bundle: Default::default(),
            strict_identifier: Default::default(),
            relocate: Default::default(),
            scripts: Default::default(),
        };
        let workdir = TempDir::new()?;
        let package_info_file = workdir.path().join("PackageInfo");
        info.write(File::create(&package_info_file)?)?;
        let directory = directory.as_ref();
        let bom = ReceiptBuilder::new().create(directory)?;
        let bom_file = workdir.path().join("Bom");
        bom.write(File::create(&bom_file)?)?;
        let payload_file = workdir.path().join("Payload");
        {
            let writer = ZlibEncoder::new(File::create(&payload_file)?, Compression::best());
            let mut archive = cpio::Builder::new(writer);
            archive.set_format(cpio::Format::Odc);
            archive.append_dir_all(directory)?;
            archive.finish()?.finish()?;
        }
        let mut xar = zar::Builder::new(writer, Some(signer));
        xar.append_dir_all(
            workdir.path(),
            zar::Compression::Gzip,
            zar::no_extra_contents,
        )?;
        xar.finish()?;
        Ok(())
    }

    pub fn file_name(&self) -> String {
        format!("{}-{}.pkg", self.identifier, self.version)
    }
}

const GENERATOR_VERSION: &str = concat!("Wolfpack/", env!("CARGO_PKG_VERSION"));

#[cfg(test)]
mod tests {
    use fs_err::File;
    use std::process::Command;

    use arbitrary::Arbitrary;
    use arbitrary::Unstructured;
    use arbtest::arbtest;
    use command_error::CommandExt;
    use rand::rngs::OsRng;
    use rand::Rng;
    use rand_mt::Mt64;
    use tempfile::TempDir;
    use zar::rsa::RsaPrivateKey;
    use zar::ChecksumAlgo;

    use super::*;
    use crate::test::prevent_concurrency;
    use crate::test::Chars;
    use crate::test::DirectoryOfFiles;
    use crate::test::CONTROL;
    use crate::test::UNICODE;

    #[ignore = "Needs `darling`"]
    #[test]
    fn darling_installer_installs_random_package() {
        assert!(Command::new("mount")
            .arg("-t")
            .arg("tmpfs")
            .arg("tmpfs")
            .arg("/root")
            .status_checked()
            .unwrap()
            .success());
        let _guard = prevent_concurrency("macos");
        let signing_key = RsaPrivateKey::new(&mut OsRng, 2048).unwrap();
        let signer = PackageSigner::new(ChecksumAlgo::Sha1, signing_key, Vec::new()).unwrap();
        let workdir = TempDir::new().unwrap();
        let package_file = workdir.path().join("test.pkg");
        arbtest(|u| {
            let package: Package = u.arbitrary()?;
            let directory: DirectoryOfFiles = u.arbitrary()?;
            package
                .clone()
                .write(
                    &mut File::create(package_file.as_path()).unwrap(),
                    directory.path(),
                    &signer,
                )
                .unwrap();
            assert!(
                Command::new("darling")
                    .arg("shell")
                    .arg("xar")
                    .arg("-tf")
                    .arg(format!("/Volumes/SystemRoot{}", package_file.display()))
                    .status_checked()
                    .unwrap()
                    .success(),
                "manifest:\n========{:?}========",
                package
            );
            assert!(
                Command::new("darling")
                    .arg("shell")
                    .arg("installer")
                    .arg("-verbose")
                    .arg("-target")
                    .arg("/")
                    .arg("-pkg")
                    .arg(format!("/Volumes/SystemRoot{}", package_file.display()))
                    .status_checked()
                    .unwrap()
                    .success(),
                "manifest:\n========{:?}========",
                package
            );
            Ok(())
        });
    }

    impl<'a> Arbitrary<'a> for Package {
        fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
            let seed: u64 = u.arbitrary()?;
            let mut rng = Mt64::new(seed);
            let valid_chars = Chars::from(UNICODE).difference(CONTROL);
            let len = rng.gen_range(1..=10);
            let identifier = valid_chars.random_string(&mut rng, len);
            let len = rng.gen_range(1..=10);
            let version = valid_chars.random_string(&mut rng, len);
            Ok(Self {
                identifier,
                version,
            })
        }
    }
}