diff options
| author | Hermes Agent <hermes@localhost> | 2026-08-12 01:34:51 +0000 |
|---|---|---|
| committer | Hermes Agent <hermes@localhost> | 2026-08-12 01:34:51 +0000 |
| commit | 9acf451473989fa868e21d2d6ff59cf3fb445562 (patch) | |
| tree | c58417401e1ecb7304c21c2ebf6f2eaa032ad907 /src/compare.rs | |
| parent | 020fbf6a034b3c90de1bdd3ee5e3e5e15c783059 (diff) | |
Add semantic artifact manifest comparison
Diffstat (limited to 'src/compare.rs')
| -rw-r--r-- | src/compare.rs | 103 |
1 files changed, 103 insertions, 0 deletions
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<SemanticDifference>, +} + +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<T: PartialEq>( + left: &[T], + right: &[T], + key: impl Fn(&T) -> &str, + difference: impl Fn(&str, Change) -> SemanticDifference, + output: &mut Vec<SemanticDifference>, +) { + let left = left.iter().map(|record| (key(record), record)).collect::<BTreeMap<_, _>>(); + let right = right.iter().map(|record| (key(record), record)).collect::<BTreeMap<_, _>>(); + for name in left.keys().chain(right.keys()).copied().collect::<std::collections::BTreeSet<_>>() { + 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)), + _ => {} + } + } +} |