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/manifest.rs | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/manifest.rs (limited to 'src/manifest.rs') diff --git a/src/manifest.rs b/src/manifest.rs new file mode 100644 index 0000000..1a5829b --- /dev/null +++ b/src/manifest.rs @@ -0,0 +1,127 @@ +use std::collections::HashSet; +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ArtifactManifest { + pub packages: Vec, + pub files: Vec, + pub initrd: Option, + pub services: Vec, + pub archive_members: Vec, +} + +impl ArtifactManifest { + pub fn new( + mut packages: Vec, + mut files: Vec, + initrd: Option, + mut services: Vec, + mut archive_members: Vec, + ) -> Result { + packages.sort_by(|left, right| left.name.cmp(&right.name)); + files.sort_by(|left, right| left.path.cmp(&right.path)); + services.sort_by(|left, right| left.name.cmp(&right.name)); + archive_members.sort_by(|left, right| left.path.cmp(&right.path)); + ensure_unique("package", packages.iter().map(|record| record.name.as_str()))?; + ensure_unique("file", files.iter().map(|record| record.path.as_str()))?; + ensure_unique("service", services.iter().map(|record| record.name.as_str()))?; + ensure_unique("archive member", archive_members.iter().map(|record| record.path.as_str()))?; + Ok(Self { packages, files, initrd, services, archive_members }) + } + + pub fn load(path: impl AsRef) -> Result { + let path = path.as_ref(); + let text = fs::read_to_string(path).with_context(|| format!("read artifact manifest {}", path.display()))?; + let manifest: Self = toml::from_str(&text).with_context(|| format!("parse artifact manifest {}", path.display()))?; + Self::new(manifest.packages, manifest.files, manifest.initrd, manifest.services, manifest.archive_members) + } + + pub fn write_beside(&self, artifact: impl AsRef) -> Result { + let artifact = artifact.as_ref(); + let directory = artifact.parent().unwrap_or_else(|| Path::new(".")); + let path = directory.join("artifact.manifest.toml"); + let text = toml::to_string_pretty(self).context("serialize artifact manifest")?; + fs::write(&path, text).with_context(|| format!("write artifact manifest {}", path.display()))?; + Ok(path) + } +} + +fn ensure_unique<'a>(kind: &str, keys: impl IntoIterator) -> Result<()> { + let mut seen = HashSet::new(); + for key in keys { + if key.is_empty() || !seen.insert(key) { + bail!("duplicate {kind} record: {key}"); + } + } + Ok(()) +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PackageRecord { + pub name: String, + pub version: String, +} + +impl PackageRecord { + pub fn new(name: impl Into, version: impl Into) -> Self { + Self { name: name.into(), version: version.into() } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FileRecord { + pub path: String, + pub kind: String, + pub digest: Option, + pub target: Option, +} + +impl FileRecord { + pub fn file(path: impl Into, digest: impl Into) -> Self { + Self { path: path.into(), kind: "file".into(), digest: Some(digest.into()), target: None } + } + + pub fn symlink(path: impl Into, target: impl Into) -> Self { + Self { path: path.into(), kind: "symlink".into(), digest: None, target: Some(target.into()) } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InitrdRecord { + pub path: String, + pub sha256: String, +} + +impl InitrdRecord { + pub fn new(path: impl Into, sha256: impl Into) -> Self { + Self { path: path.into(), sha256: sha256.into() } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ServiceRecord { + pub name: String, + pub enabled: bool, +} + +impl ServiceRecord { + pub fn new(name: impl Into, enabled: bool) -> Self { + Self { name: name.into(), enabled } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ArchiveMemberRecord { + pub path: String, + pub kind: String, +} + +impl ArchiveMemberRecord { + pub fn new(path: impl Into, kind: impl Into) -> Self { + Self { path: path.into(), kind: kind.into() } + } +} -- cgit v1.2.3