summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHermes Agent <hermes@localhost>2026-08-12 04:10:14 +0000
committerHermes Agent <hermes@localhost>2026-08-12 04:10:14 +0000
commitfc4e46c41bfb769193159143f6567341cd3e3a51 (patch)
tree5fac35118f11f71ae6f31e9f7fac4b80508fdf76
parent196f315321643a0ce2327f750c779a7d40e4b427 (diff)
Validate semantic file record invariants
-rw-r--r--src/manifest.rs26
-rw-r--r--tests/compare.rs33
2 files changed, 59 insertions, 0 deletions
diff --git a/src/manifest.rs b/src/manifest.rs
index 3a34af7..bc09073 100644
--- a/src/manifest.rs
+++ b/src/manifest.rs
@@ -31,6 +31,7 @@ impl ArtifactManifest {
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));
+ validate_file_records(&files)?;
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()))?;
@@ -355,6 +356,31 @@ fn link_target(path: &Path) -> Result<String> {
Ok(value.to_owned())
}
+fn validate_file_records(records: &[FileRecord]) -> Result<()> {
+ for record in records {
+ match record.kind.as_str() {
+ "file" => {
+ if record.digest.as_deref().is_none_or(str::is_empty) {
+ bail!("regular file record requires a digest: {}", record.path);
+ }
+ if record.target.is_some() {
+ bail!("regular file record cannot carry a target: {}", record.path);
+ }
+ }
+ "symlink" | "hardlink" => {
+ if record.digest.is_some() {
+ bail!("{} record cannot carry a digest: {}", record.kind, record.path);
+ }
+ if record.target.as_deref().is_none_or(str::is_empty) {
+ bail!("{} record requires a target: {}", record.kind, record.path);
+ }
+ }
+ _ => bail!("unsupported file record kind {}: {}", record.kind, record.path),
+ }
+ }
+ Ok(())
+}
+
fn ensure_unique<'a>(kind: &str, keys: impl IntoIterator<Item = &'a str>) -> Result<()> {
let mut seen = HashSet::new();
for key in keys {
diff --git a/tests/compare.rs b/tests/compare.rs
index 43c91e0..45522d1 100644
--- a/tests/compare.rs
+++ b/tests/compare.rs
@@ -173,6 +173,39 @@ fn rejects_duplicate_semantic_keys() {
}
#[test]
+fn rejects_malformed_file_records_before_comparison() {
+ let error = ArtifactManifest::new(
+ vec![],
+ vec![FileRecord {
+ path: "etc/controller.conf".into(),
+ kind: "file".into(),
+ digest: None,
+ target: None,
+ }],
+ None,
+ vec![],
+ vec![],
+ )
+ .expect_err("regular files require a digest");
+ assert!(error.to_string().contains("regular file record requires a digest"));
+
+ let error = ArtifactManifest::new(
+ vec![],
+ vec![FileRecord {
+ path: "etc/controller-link".into(),
+ kind: "symlink".into(),
+ digest: Some("not-applicable".into()),
+ target: Some("controller.conf".into()),
+ }],
+ None,
+ vec![],
+ vec![],
+ )
+ .expect_err("symlinks cannot carry a digest");
+ assert!(error.to_string().contains("symlink record cannot carry a digest"));
+}
+
+#[test]
fn collects_semantic_facts_from_native_rootfs_and_tar_artifact() {
let fixture = tempdir().expect("temporary directory");
let rootfs = fixture.path().join("rootfs");