use std::collections::BTreeMap; use std::fmt::Write; use crate::manifest::ArtifactManifest; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Change { Added, Removed, Changed, } impl Change { fn verb(self) -> &'static str { match self { Self::Added => "added", Self::Removed => "removed", Self::Changed => "changed", } } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SemanticDifference { Package { name: String, change: Change }, File { path: String, change: Change }, Initrd { change: Change }, Service { name: String, change: Change }, ArchiveMember { path: String, change: Change }, } #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct ComparisonReport { differences: Vec, } impl ComparisonReport { pub fn differences(&self) -> &[SemanticDifference] { &self.differences } pub fn is_equivalent(&self) -> bool { self.differences.is_empty() } pub fn render(&self) -> String { if self.is_equivalent() { return "artifacts are semantically equivalent\n".into(); } let mut output = String::new(); for difference in &self.differences { match difference { SemanticDifference::Package { name, change } => writeln!(output, "{} package {name}", change.verb()), SemanticDifference::File { path, change } => writeln!(output, "{} file {path}", change.verb()), SemanticDifference::Initrd { change } => writeln!(output, "{} initrd", change.verb()), SemanticDifference::Service { name, change } => writeln!(output, "{} service {name}", change.verb()), SemanticDifference::ArchiveMember { path, change } => writeln!(output, "{} archive member {path}", change.verb()), } .expect("writing to String cannot fail"); } output } } pub fn compare(left: &ArtifactManifest, right: &ArtifactManifest) -> ComparisonReport { let mut differences = Vec::new(); compare_records(&left.packages, &right.packages, |record| &record.name, |name, change| { SemanticDifference::Package { name: name.to_owned(), change } }, &mut differences); compare_records(&left.files, &right.files, |record| &record.path, |path, change| { SemanticDifference::File { path: path.to_owned(), change } }, &mut differences); match (&left.initrd, &right.initrd) { (None, Some(_)) => differences.push(SemanticDifference::Initrd { change: Change::Added }), (Some(_), None) => differences.push(SemanticDifference::Initrd { change: Change::Removed }), (Some(left), Some(right)) if left != right => { differences.push(SemanticDifference::Initrd { change: Change::Changed }); } _ => {} } compare_records(&left.services, &right.services, |record| &record.name, |name, change| { SemanticDifference::Service { name: name.to_owned(), change } }, &mut differences); compare_records(&left.archive_members, &right.archive_members, |record| &record.path, |path, change| { SemanticDifference::ArchiveMember { path: path.to_owned(), change } }, &mut differences); differences.sort(); ComparisonReport { differences } } fn compare_records( left: &[T], right: &[T], key: impl Fn(&T) -> &str, difference: impl Fn(&str, Change) -> SemanticDifference, output: &mut Vec, ) { let left = left.iter().map(|record| (key(record), record)).collect::>(); let right = right.iter().map(|record| (key(record), record)).collect::>(); for name in left.keys().chain(right.keys()).copied().collect::>() { match (left.get(name), right.get(name)) { (None, Some(_)) => output.push(difference(name, Change::Added)), (Some(_), None) => output.push(difference(name, Change::Removed)), (Some(left), Some(right)) if left != right => output.push(difference(name, Change::Changed)), _ => {} } } }