use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest {
pub created: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
pub bundle_type: BundleType,
pub targets: Vec<String>,
pub toolchain: String,
pub crates: BTreeMap<String, CrateEntry>,
#[serde(default)]
pub rustup: Vec<RustupEntry>,
#[serde(default)]
pub dist: Vec<DistEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest_hash: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BundleType {
Full,
Delta,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateEntry {
pub name: String,
pub version: String,
pub sha256: String,
pub size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RustupEntry {
pub target: String,
pub filename: String,
pub sha256: String,
pub size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistEntry {
pub path: String,
pub sha256: String,
pub size: u64,
}
impl Manifest {
pub fn new(
bundle_type: BundleType,
parent: Option<String>,
targets: Vec<String>,
toolchain: String,
) -> Self {
Self {
created: chrono::Utc::now().to_rfc3339(),
parent,
bundle_type,
targets,
toolchain,
crates: BTreeMap::new(),
rustup: Vec::new(),
dist: Vec::new(),
manifest_hash: None,
}
}
pub fn add_crate(&mut self, name: String, version: String, sha256: String, size: u64) {
let key = format!("{}-{}", name, version);
self.crates.insert(
key,
CrateEntry {
name,
version,
sha256,
size,
},
);
}
pub fn add_rustup(&mut self, target: String, filename: String, sha256: String, size: u64) {
self.rustup.push(RustupEntry {
target,
filename,
sha256,
size,
});
}
pub fn add_dist(&mut self, path: String, sha256: String, size: u64) {
self.dist.push(DistEntry { path, sha256, size });
}
pub fn crate_set(&self) -> BTreeSet<(String, String)> {
self.crates
.values()
.map(|c| (c.name.clone(), c.version.clone()))
.collect()
}
pub fn compute_hash(&self) -> String {
let mut m = self.clone();
m.manifest_hash = None;
let json = serde_json::to_string(&m).expect("manifest serialization cannot fail");
let mut hasher = Sha256::new();
hasher.update(json.as_bytes());
hex::encode(hasher.finalize())
}
pub fn seal(&mut self) {
self.manifest_hash = Some(self.compute_hash());
}
pub fn verify_hash(&self) -> Result<()> {
let expected = self
.manifest_hash
.as_ref()
.context("manifest has no hash")?;
let computed = self.compute_hash();
if expected != &computed {
bail!(
"manifest hash mismatch: expected {}, computed {}",
expected,
computed
);
}
Ok(())
}
pub fn diff(&self, previous: &Manifest) -> BTreeSet<(String, String)> {
let prev_set = previous.crate_set();
let curr_set = self.crate_set();
curr_set.difference(&prev_set).cloned().collect()
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(self).context("failed to serialize manifest")
}
pub fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json).context("failed to parse manifest")
}
}