From cc4da72a9e4f2f293fec9355aba1128f572d3c4e Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 2 Nov 2026 23:35:38 +0700 Subject: [PATCH] The same files in another archive order are the same content tree An inventory's content_id hashed its (path, sha256) pairs in the order the archive listed them, so two ZIPs with the same files in another order had different ids, and core_archives_compare said they weren't identical. The pairs are sorted before hashing. Fixes #281. --- crates/analysis/src/js_inventory.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/analysis/src/js_inventory.rs b/crates/analysis/src/js_inventory.rs index 77c751b3..a1ca0a8e 100644 --- a/crates/analysis/src/js_inventory.rs +++ b/crates/analysis/src/js_inventory.rs @@ -74,9 +74,11 @@ impl Inventory { m } - /// `inv_` + sha256 over (path, sha256) pairs: equal content trees share an id. + /// `inv_` + sha256 over (path, sha256) pairs, sorted: equal content trees share an id whatever + /// order their archive lists them in (#281). pub fn content_id(&self) -> String { - let pairs: Vec<(&str, Option<&str>)> = self.entries.iter().map(|e| (e.path.as_str(), e.sha256.as_deref())).collect(); + let mut pairs: Vec<(&str, Option<&str>)> = self.entries.iter().map(|e| (e.path.as_str(), e.sha256.as_deref())).collect(); + pairs.sort_unstable(); format!("inv_{}", sha256_hex(craft_anything_evidence::canonical_json(&json!(pairs)).as_bytes())) } } @@ -481,6 +483,21 @@ mod tests { assert_eq!(d["counts"], json!({"added": 1, "removed": 0, "changed": 1, "moved": 1, "unchanged": 1})); } + /// #281: the same files listed in another order are the same content tree. + #[test] + fn member_order_does_not_change_the_content_id() { + let zip = |files: &[(&str, &[u8])]| { + let mut i = Inventorier::new("a.zip", "zip", InvLimits::default(), &|| false); + i.add_archive("", &fx::zip_stored(files), "zip", 0).unwrap(); + i.finish() + }; + let (x, y): (&[u8], &[u8]) = (b"one", b"two"); + let (ab, ba) = (zip(&[("a.txt", x), ("b.txt", y)]), zip(&[("b.txt", y), ("a.txt", x)])); + assert_eq!(ab.content_id(), ba.content_id()); + assert_eq!(compare_inventories(&ab, &ba, 100)["identical"], true); + assert_ne!(ab.content_id(), zip(&[("a.txt", y), ("b.txt", x)]).content_id(), "swapped contents differ"); + } + /// #283: two identical old files both "moved" to one new file, and nothing was removed. #[test] fn each_new_file_is_one_move() {