summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHermes Agent <hermes@localhost>2026-08-12 06:39:37 +0000
committerHermes Agent <hermes@localhost>2026-08-12 06:39:37 +0000
commit227ee2af958eba340d5d7b269387455b4513277d (patch)
tree615c3605f45edb4c39abae9937ea236acc634390
parent200df1f46cc5a5a5659c90ab37c2ba624cb17b5a (diff)
Reject ambiguous archive members in comparison
-rw-r--r--src/manifest.rs7
-rw-r--r--tests/compare.rs24
2 files changed, 31 insertions, 0 deletions
diff --git a/src/manifest.rs b/src/manifest.rs
index 4cd256c..067dc9a 100644
--- a/src/manifest.rs
+++ b/src/manifest.rs
@@ -110,6 +110,7 @@ impl ArtifactManifest {
let mut services = Vec::new();
let mut archive_members = Vec::new();
let mut initrd = None;
+ let mut member_paths = HashSet::new();
for entry in archive
.entries()
@@ -121,6 +122,12 @@ impl ArtifactManifest {
if is_archive_root_path(&path) || is_internal_metadata_path(&path) {
continue;
}
+ if !member_paths.insert(path.clone()) {
+ bail!(
+ "duplicate archive member record: {path} in {}",
+ archive_path.display()
+ );
+ }
let entry_type = entry.header().entry_type();
let kind = match entry_type {
EntryType::Regular => "file",
diff --git a/tests/compare.rs b/tests/compare.rs
index 4f48721..ab63f9a 100644
--- a/tests/compare.rs
+++ b/tests/compare.rs
@@ -371,6 +371,30 @@ fn normalizes_legacy_dot_prefixed_archive_paths_before_comparison() {
}
#[test]
+fn rejects_duplicate_archive_member_paths_before_semantic_comparison() {
+ let fixture = tempdir().expect("temporary directory");
+ let artifact = fixture.path().join("ambiguous-legacy.tar");
+ let file = fs::File::create(&artifact).expect("create legacy archive");
+ let mut archive = tar::Builder::new(file);
+
+ for contents in [b"first\n".as_slice(), b"second\n".as_slice()] {
+ let mut header = tar::Header::new_gnu();
+ header.set_size(contents.len() as u64);
+ header.set_mode(0o644);
+ header.set_cksum();
+ archive
+ .append_data(&mut header, "./etc/controller.conf", Cursor::new(contents))
+ .expect("append duplicate legacy member");
+ }
+ archive.finish().expect("finish legacy archive");
+
+ let error = ArtifactManifest::collect_archive(&artifact)
+ .expect_err("ambiguous archive paths cannot be compared semantically");
+
+ assert!(error.to_string().contains("duplicate archive member record: etc/controller.conf"));
+}
+
+#[test]
fn collects_hardlink_targets_from_an_archive_for_semantic_comparison() {
let fixture = tempdir().expect("temporary directory");
let artifact = fixture.path().join("legacy.tar");