diff --git a/src/error.rs b/src/error.rs index 5de280fa..27a24062 100644 --- a/src/error.rs +++ b/src/error.rs @@ -191,6 +191,15 @@ pub enum Error { first: KeyIdentifier, second: KeyIdentifier, }, + #[snafu(display( + "embedded file in manifest `{path}` should have hash `{expected}` but has hash `{actual}`" + ))] + EmbeddedFileHashMismatch { + actual: Hash, + backtrace: Option, + expected: Hash, + path: DisplayPath, + }, #[snafu(display("{count} mismatched file{}", if *count == 1 { "" } else { "s" }))] EntryMismatch { backtrace: Option, @@ -593,6 +602,12 @@ pub enum Error { path: DisplayPath, source: ArchiveError, }, + #[snafu(display("manifest `{path}` contains unreferenced embedded files: {hashes}"))] + UnreferencedEmbeddedFiles { + backtrace: Option, + hashes: Ticked, + path: DisplayPath, + }, #[snafu(display("invalid video `{path}`"))] Video { backtrace: Option, diff --git a/src/manifest.rs b/src/manifest.rs index 60b31943..44533a08 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -49,6 +49,38 @@ impl Manifest { Archive::pack(self).unwrap().fingerprint().unwrap() } + pub(crate) fn from_json(json: &str, path: &Utf8Path) -> Result { + let manifest = + serde_json::from_str::(json).context(error::DeserializeManifest { path })?; + + let hashes = manifest + .files() + .values() + .map(|file| file.hash) + .collect::>(); + + let mut unreferenced = BTreeSet::new(); + for (&expected, content) in &manifest.embedded { + let actual = Hash::bytes(content); + + ensure! { + actual == expected, + error::EmbeddedFileHashMismatch { actual, expected, path }, + } + + if !hashes.contains(&expected) { + unreferenced.insert(expected); + } + } + + ensure! { + unreferenced.is_empty(), + error::UnreferencedEmbeddedFiles { hashes: unreferenced, path }, + } + + Ok(manifest) + } + pub fn load(path: Option<&Utf8Path>) -> Result { Ok(Self::load_with_opt_path(path)?.1) } diff --git a/src/subcommand/archive.rs b/src/subcommand/archive.rs index 911c7614..51956faa 100644 --- a/src/subcommand/archive.rs +++ b/src/subcommand/archive.rs @@ -12,8 +12,7 @@ impl Archive { pub(crate) fn run(self) -> Result { let json = filesystem::read_to_string(&self.input)?; - let manifest = serde_json::from_str::(&json) - .context(error::DeserializeManifest { path: &self.input })?; + let manifest = Manifest::from_json(&json, &self.input)?; manifest.save(&self.output)?; diff --git a/tests/archive.rs b/tests/archive.rs index 6c49f51d..099a244b 100644 --- a/tests/archive.rs +++ b/tests/archive.rs @@ -65,6 +65,35 @@ fn embedded_preserved() { .success(); } +#[test] +fn rejects_embedded_hash_mismatch() { + let expected = Hash::bytes(b"foo").to_string(); + let actual = Hash::bytes(b"bar").to_string(); + let content = hex::encode(b"bar"); + + Test::new() + .write( + "manifest.json", + json! { + embedded: { + *expected: content + }, + package: { + foo: { + hash: expected, + size: 3 + } + }, + signatures: [], + }, + ) + .args(["archive", "manifest.json", "manifest.filepack"]) + .stderr_regex_path(&format!( + "error: embedded file in manifest `.*manifest.json` should have hash `{expected}` but has hash `{actual}`\n", + )) + .failure(); +} + #[test] fn rejects_totals_overflow() { Test::new() @@ -95,6 +124,29 @@ fn rejects_totals_overflow() { .failure(); } +#[test] +fn rejects_unreferenced_embedded_files() { + let hash = Hash::bytes(b"foo").to_string(); + let content = hex::encode(b"foo"); + + Test::new() + .write( + "manifest.json", + json! { + embedded: { + *hash: content + }, + package: {}, + signatures: [], + }, + ) + .args(["archive", "manifest.json", "manifest.filepack"]) + .stderr_regex_path(&format!( + "error: manifest `.*manifest.json` contains unreferenced embedded files: `{hash}`\n", + )) + .failure(); +} + #[test] fn round_trip() { Test::new()