From 9acf451473989fa868e21d2d6ff59cf3fb445562 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 12 Aug 2026 01:34:51 +0000 Subject: Add semantic artifact manifest comparison --- src/compare.rs | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/compare.rs (limited to 'src/compare.rs') diff --git a/src/compare.rs b/src/compare.rs new file mode 100644 index 0000000..7088886 --- /dev/null +++ b/src/compare.rs @@ -0,0 +1,103 @@ +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); + if left.initrd != right.initrd { + 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)), + _ => {} + } + } +} -- cgit v1.2.3