summaryrefslogtreecommitdiff
path: root/src/compare.rs
diff options
context:
space:
mode:
authorHermes Agent <hermes@localhost>2026-08-12 01:34:51 +0000
committerHermes Agent <hermes@localhost>2026-08-12 01:34:51 +0000
commit9acf451473989fa868e21d2d6ff59cf3fb445562 (patch)
treec58417401e1ecb7304c21c2ebf6f2eaa032ad907 /src/compare.rs
parent020fbf6a034b3c90de1bdd3ee5e3e5e15c783059 (diff)
Add semantic artifact manifest comparison
Diffstat (limited to 'src/compare.rs')
-rw-r--r--src/compare.rs103
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)),
+ _ => {}
+ }
+ }
+}