buildchain 0.5.3

Software for creating and managing a distributed and reproducible chain of builds
Documentation
// SPDX-License-Identifier: GPL-3.0-only

use std::fs::File;
use std::io::{stdout, Read, Write};

use crate::block::PackedBlock;
use crate::store::b32dec;
use crate::{err_str, Block, Manifest, Sha384};

pub struct DownloadArguments<'a> {
    pub project: &'a str,
    pub branch: &'a str,
    pub cert_opt: Option<&'a str>,
    pub cache_opt: Option<&'a str>,
    pub key: &'a str,
    pub url: &'a str,
    pub file_opt: Option<&'a str>,
}

pub struct Downloader {
    key: Vec<u8>,
    url: reqwest::Url,
    project: String,
    branch: String,
    client: reqwest::blocking::Client,
}

impl Downloader {
    pub fn new(
        key: &str,
        url: &str,
        project: &str,
        branch: &str,
        cert_opt: Option<&[u8]>,
    ) -> Result<Downloader, String> {
        let key = b32dec(key).ok_or_else(|| "key not in base32 format".to_string())?;

        let url = reqwest::Url::parse(url).map_err(err_str)?;

        let client = {
            let mut builder = reqwest::blocking::Client::builder();

            if let Some(cert) = cert_opt {
                builder = builder
                    .add_root_certificate(reqwest::Certificate::from_pem(cert).map_err(err_str)?);
            }

            builder.build().map_err(err_str)?
        };

        Ok(Downloader {
            key,
            url,
            project: project.to_string(),
            branch: branch.to_string(),
            client,
        })
    }

    fn download(&self, path: &str) -> Result<Vec<u8>, String> {
        let url = self.url.join(path).map_err(err_str)?;
        let mut response = self.client.get(url).send().map_err(err_str)?;
        if !response.status().is_success() {
            return Err(format!(
                "failed to download {}: {:?}",
                path,
                response.status()
            ));
        }

        let mut data = Vec::new();
        response.read_to_end(&mut data).map_err(err_str)?;
        Ok(data)
    }

    pub fn object(&self, digest: &str) -> Result<Vec<u8>, String> {
        let path = format!("object/{}", digest);
        let data = self.download(&path)?;

        let sha = Sha384::new(data.as_slice()).map_err(err_str)?;
        if sha.to_base32() != digest {
            return Err("sha384 mismatch".to_string());
        }

        Ok(data)
    }

    pub fn tail(&self) -> Result<Block, String> {
        let path = format!("tail/{}/{}", self.project, self.branch);
        let data = self.download(&path)?;

        let b: &PackedBlock =
            plain::from_bytes(&data).map_err(|_| "response too small".to_string())?;
        b.verify(&self.key)
    }
}

pub fn download(args: DownloadArguments) -> Result<(), String> {
    let mut cert = Vec::new();
    let cert_opt = if let Some(cert_path) = args.cert_opt {
        {
            let mut file = File::open(cert_path).map_err(err_str)?;
            file.read_to_end(&mut cert).map_err(err_str)?;
        }
        Some(cert.as_slice())
    } else {
        None
    };

    let dl = Downloader::new(args.key, args.url, args.project, args.branch, cert_opt)?;

    let tail = dl.tail()?;

    let manifest_json = dl.object(&tail.digest)?;
    let manifest = serde_json::from_slice::<Manifest>(&manifest_json).map_err(err_str)?;

    if let Some(file) = args.file_opt {
        if let Some(digest) = manifest.files.get(file) {
            let data = dl.object(digest)?;
            stdout().write(&data).map_err(err_str)?;
        } else {
            return Err(format!("{} not found", file));
        }
    } else {
        for (file, _digest) in manifest.files.iter() {
            println!("{}", file);
        }
    }

    Ok(())
}