Public release:

  - ZipWriter now rejects a destination that does not support...
  - Fix asset_write signing of ZIP-based files (ZIP, DOCX, PP...

GitOrigin-RevId: 4f4f9976d4e9721657fd140cccc733963f77dceb
Change-Id: I97317a69dc3e76b66ec26cbcbdd1b4403d75af7c
diff --git a/formats/zip/BUILD b/formats/zip/BUILD
index 1a0dbf8..b9679fb 100644
--- a/formats/zip/BUILD
+++ b/formats/zip/BUILD
@@ -378,7 +378,10 @@
         "@abseil-cpp//absl/status",
         "@abseil-cpp//absl/status:status_matchers",
         "@abseil-cpp//absl/status:statusor",
+        "@abseil-cpp//absl/strings",
         "@googletest//:gtest_main",
+        "@riegeli//riegeli/bytes:cfile_reader",
+        "@riegeli//riegeli/bytes:cfile_writer",
         "@riegeli//riegeli/bytes:string_reader",
         "@riegeli//riegeli/bytes:string_writer",
     ],
diff --git a/formats/zip/writer.cc b/formats/zip/writer.cc
index c349aad..c8741d4 100644
--- a/formats/zip/writer.cc
+++ b/formats/zip/writer.cc
@@ -43,6 +43,17 @@
         "request must not include `hard_binding` (binding is computed by "
         "ZipWriter)");
   }
+  // Phase 2 below reads back and patches `destination`. Check this before
+  // writing anything, so that an unsuitable `destination` is left untouched.
+  if (!destination->SupportsReadMode()) {
+    return absl::InvalidArgumentError(
+        "ZipWriter::Write requires a riegeli::Writer that supports read mode.");
+  }
+  if (!destination->SupportsRandomAccess()) {
+    return absl::InvalidArgumentError(
+        "ZipWriter::Write requires a riegeli::Writer that supports random "
+        "access.");
+  }
 
   ABSL_ASSIGN_OR_RETURN(auto reader, ZipReader::Create(source));
 
@@ -83,10 +94,6 @@
 
   // Phase 2: Compute final hard binding based on the updated content, finalize
   // the manifest store, and embed the final manifest store in the asset.
-  if (!destination->SupportsReadMode()) {
-    return absl::InvalidArgumentError(
-        "ZipWriter::Write requires a riegeli::Writer that supports read mode.");
-  }
   riegeli::Reader* phase2_reader = destination->ReadMode(0);
   if (phase2_reader == nullptr) {
     return absl::InternalError("failed to switch to read mode");
diff --git a/formats/zip/writer.h b/formats/zip/writer.h
index 78b7a76..5583c56 100644
--- a/formats/zip/writer.h
+++ b/formats/zip/writer.h
@@ -41,6 +41,12 @@
   // asset. The riegeli::Reader and riegeli::Writer cannot be pointed to the
   // same underlying data. The request must not include `hard_binding`.
   // Fails if C2PA metadata already exists.
+  //
+  // `destination` must support read mode and random access (e.g.
+  // `riegeli::StringWriter`, or `riegeli::CFileWriter` opened with
+  // `set_read(true)`), because the embedded manifest store is finalized by
+  // reading back and patching the written asset. Otherwise, returns
+  // `InvalidArgumentError` without writing to `destination`.
   absl::Status Write(GenerateManifestStoreRequest& request,
                      riegeli::Reader* absl_nonnull source,
                      riegeli::Writer* absl_nonnull destination);
diff --git a/formats/zip/writer_test.cc b/formats/zip/writer_test.cc
index 9761ef3..a6ea71f 100644
--- a/formats/zip/writer_test.cc
+++ b/formats/zip/writer_test.cc
@@ -23,6 +23,7 @@
 #include "absl/status/status.h"
 #include "absl/status/status_matchers.h"
 #include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
 #include "formats/byte_range.h"
 #include "formats/zip/extractor.h"
 #include "generator/generator.h"
@@ -31,6 +32,8 @@
 #include "proto/generator.pb.h"
 #include "proto/manifest.pb.h"
 #include "proto/validation_status.pb.h"
+#include "riegeli/bytes/cfile_reader.h"
+#include "riegeli/bytes/cfile_writer.h"
 #include "riegeli/bytes/string_reader.h"
 #include "riegeli/bytes/string_writer.h"
 #include "testing/nonprod_generator.h"
@@ -127,5 +130,35 @@
                        HasSubstr("must not include `hard_binding`")));
 }
 
+TEST_F(ZipWriterTest, WriteToDestinationWithoutReadModeFails) {
+  GenerateManifestStoreRequest request;
+  request.mutable_actions()
+      ->mutable_created()
+      ->mutable_action_metadata()
+      ->set_description("test only");
+
+  auto source = credentio_testing::GetFileReader(
+      "credentio/formats/zip/testing/"
+      "no_c2pa_032_asset.pptx");
+  ABSL_ASSERT_OK(source);
+
+  // By default, `riegeli::CFileWriter` opens the file write-only.
+  const std::string output_path =
+      absl::StrCat(testing::TempDir(), "/write_only_output.pptx");
+  riegeli::CFileWriter destination(output_path);
+  ABSL_ASSERT_OK(destination.status());
+  ASSERT_FALSE(destination.SupportsReadMode());
+
+  EXPECT_THAT(writer_->Write(request, source->get(), &destination),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("supports read mode")));
+  ASSERT_TRUE(destination.Close());
+
+  // Nothing is written to the rejected destination.
+  riegeli::CFileReader output(output_path);
+  ABSL_ASSERT_OK(output.status());
+  EXPECT_THAT(output.Size(), Optional(0));
+}
+
 }  // namespace
 }  // namespace credentio
diff --git a/tools/BUILD b/tools/BUILD
index cf83f83..c11089f 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -110,22 +110,13 @@
     deps = [
         ":asset_writer",
         "//crypto/default:default_crypto_read_handler",
-        "//formats/bmff:extractor",
         "//formats/bmff:test_utils",
         "//formats/gif:constants",
-        "//formats/gif:extractor",
         "//formats/gif:test_builder",
-        "//formats/id3:extractor",
-        "//formats/jpeg:extractor",
         "//formats/jpeg/testing:jpeg_creator",
-        "//formats/pdf:extractor",
-        "//formats/png:extractor",
         "//formats/png:test_utils",
         "//formats/riff:create_riff",
-        "//formats/riff:extractor",
         "//formats/tiff:constants",
-        "//formats/tiff:extractor",
-        "//formats/zip:extractor",
         "//proto:generator_cc_proto",
         "//proto:manifest_cc_proto",
         "//proto:validation_status_cc_proto",
@@ -139,9 +130,11 @@
         "@abseil-cpp//absl/status:status_matchers",
         "@abseil-cpp//absl/status:statusor",
         "@abseil-cpp//absl/strings",
+        "@abseil-cpp//absl/strings:string_view",
         "@boringssl//:crypto",
         "@googletest//:gtest_main",
         "@riegeli//riegeli/bytes:cfile_reader",
+        "@riegeli//riegeli/bytes:reader",
         "@riegeli//riegeli/bytes:string_reader",
         "@riegeli//riegeli/bytes:string_writer",
         "@riegeli//riegeli/endian:endian_writing",
diff --git a/tools/asset_writer.cc b/tools/asset_writer.cc
index a177688..55f3268 100644
--- a/tools/asset_writer.cc
+++ b/tools/asset_writer.cc
@@ -162,7 +162,10 @@
     return source.status();
   }
 
-  riegeli::CFileWriter<> destination(output_path);
+  // Some writers (e.g. `ZipWriter`) read back the destination, so it must be
+  // opened for reading as well.
+  riegeli::CFileWriter<> destination(
+      output_path, riegeli::CFileWriterBase::Options().set_read(true));
   if (!destination.ok()) {
     return destination.status();
   }
diff --git a/tools/asset_writer_test.cc b/tools/asset_writer_test.cc
index 75c3654..81edcae 100644
--- a/tools/asset_writer_test.cc
+++ b/tools/asset_writer_test.cc
@@ -17,6 +17,7 @@
 
 #include <cstdint>
 #include <fstream>
+#include <functional>
 #include <ios>
 #include <iterator>
 #include <memory>
@@ -29,29 +30,22 @@
 #include "absl/status/status_matchers.h"
 #include "absl/status/statusor.h"
 #include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
 #include "crypto/default/default_crypto_read_handler.h"
-#include "formats/bmff/extractor.h"
 #include "formats/bmff/test_utils.h"
 #include "formats/gif/constants.h"
-#include "formats/gif/extractor.h"
 #include "formats/gif/test_builder.h"
-#include "formats/id3/extractor.h"
-#include "formats/jpeg/extractor.h"
 #include "formats/jpeg/testing/jpeg_creator.h"
-#include "formats/pdf/extractor.h"
-#include "formats/png/extractor.h"
 #include "formats/png/test_utils.h"
 #include "formats/riff/create_riff.h"
-#include "formats/riff/extractor.h"
 #include "formats/tiff/constants.h"
-#include "formats/tiff/extractor.h"
-#include "formats/zip/extractor.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "proto/generator.pb.h"
 #include "proto/manifest.pb.h"
 #include "proto/validation_status.pb.h"
 #include "riegeli/bytes/cfile_reader.h"
+#include "riegeli/bytes/reader.h"
 #include "riegeli/bytes/string_reader.h"
 #include "riegeli/bytes/string_writer.h"
 #include "riegeli/endian/endian_writing.h"
@@ -69,16 +63,17 @@
 using ::testing::IsEmpty;
 using ::testing::Not;
 
-LocalKeyCryptoWriteHandlerOptions CreateWriteOptions(
-    const TestPki& pki, bool disable_timestamping) {
+constexpr absl::string_view kPptxMediaType =
+    "application/"
+    "vnd.openxmlformats-officedocument.presentationml.presentation";
+
+// Signs with the claim signer and timestamps with the local TSA of `pki`.
+LocalKeyCryptoWriteHandlerOptions CreateWriteOptions(const TestPki& pki) {
   LocalKeyCryptoWriteHandlerOptions options;
   options.claim_signer.key_pem = pki.claim_signer.key_pem;
   options.claim_signer.certificate_chain = {pki.claim_signer.cert_der};
-  options.disable_timestamping = disable_timestamping;
-  if (!disable_timestamping) {
-    options.tsa.key_pem = pki.tsa.key_pem;
-    options.tsa.cert_pem = pki.tsa.cert_pem;
-  }
+  options.tsa.key_pem = pki.tsa.key_pem;
+  options.tsa.cert_pem = pki.tsa.cert_pem;
   return options;
 }
 
@@ -95,140 +90,38 @@
   });
 }
 
-TEST(AssetWriterTest, WritesPngManifestStore) {
-  auto png_or = CreatePng({Chunk{.type = 0x49444154, .payload = "pixel_data"}});
-  ABSL_ASSERT_OK(png_or);
-
-  riegeli::StringReader<> source(*png_or);
-  std::string output_bytes;
-  riegeli::StringWriter<> destination(&output_bytes);
-
+GenerateManifestStoreRequest CreateRequest() {
   GenerateManifestStoreRequest request;
   request.mutable_actions()->mutable_created();
-
-  TestPki pki = CreateTestPki();
-  EXPECT_THAT(WriteAsset(CreateWriteOptions(pki, /*disable_timestamping=*/true),
-                         std::move(request), "image/png", source, destination),
-              IsOk());
-  ASSERT_TRUE(destination.Close());
-
-  riegeli::StringReader<> result_reader(output_bytes);
-  PngExtractor extractor;
-  auto extracted = extractor.ExtractManifestStore(result_reader);
-  ABSL_ASSERT_OK(extracted);
-  EXPECT_THAT(*extracted, Not(IsEmpty()));
-
-  ASSERT_TRUE(result_reader.Seek(0));
-  auto validator = CreateTestValidator(pki);
-  auto validation_result = validator->Validate(result_reader, "image/png");
-  ABSL_ASSERT_OK(validation_result);
-  EXPECT_THAT(
-      (*validation_result)->proto().active_manifest().validation().failures(),
-      IsEmpty());
+  return request;
 }
 
-TEST(AssetWriterTest, WritesJpegManifestStore) {
-  std::string jpeg_bytes = CreateJpeg({});
-  riegeli::StringReader<> source(jpeg_bytes);
-  std::string output_bytes;
-  riegeli::StringWriter<> destination(&output_bytes);
+// Minimal unsigned test assets, one per supported container format.
 
-  GenerateManifestStoreRequest request;
-  request.mutable_actions()->mutable_created();
-
-  TestPki pki = CreateTestPki();
-  EXPECT_THAT(WriteAsset(CreateWriteOptions(pki, /*disable_timestamping=*/true),
-                         std::move(request), "image/jpeg", source, destination),
-              IsOk());
-  ASSERT_TRUE(destination.Close());
-
-  riegeli::StringReader<> result_reader(output_bytes);
-  JpegExtractor extractor;
-  auto extracted = extractor.ExtractManifestStore(result_reader);
-  ABSL_ASSERT_OK(extracted);
-  EXPECT_THAT(*extracted, Not(IsEmpty()));
-
-  ASSERT_TRUE(result_reader.Seek(0));
-  auto validator = CreateTestValidator(pki);
-  auto validation_result = validator->Validate(result_reader, "image/jpeg");
-  ABSL_ASSERT_OK(validation_result);
-  EXPECT_THAT(
-      (*validation_result)->proto().active_manifest().validation().failures(),
-      IsEmpty());
+absl::StatusOr<std::string> CreatePngAsset() {
+  return CreatePng({Chunk{.type = 0x49444154, .payload = "pixel_data"}});
 }
 
-TEST(AssetWriterTest, WritesGifManifestStore) {
+absl::StatusOr<std::string> CreateJpegAsset() { return CreateJpeg({}); }
+
+absl::StatusOr<std::string> CreateGifAsset() {
   TestGifBuilder builder;
   builder.AddGifHeader();
   builder.AddLogicalDescriptor(/*has_color_table=*/false);
   builder.AddBlock(kGifExtensionIntroducer, kGifExtensionApplication,
                    "other_app_info", "misc_data");
   builder.AddBlock(kGifTrailerIntroducer);
-  std::string gif_bytes = builder.GetAsset();
-
-  riegeli::StringReader<> source(gif_bytes);
-  std::string output_bytes;
-  riegeli::StringWriter<> destination(&output_bytes);
-
-  GenerateManifestStoreRequest request;
-  request.mutable_actions()->mutable_created();
-
-  TestPki pki = CreateTestPki();
-  EXPECT_THAT(WriteAsset(CreateWriteOptions(pki, /*disable_timestamping=*/true),
-                         std::move(request), "image/gif", source, destination),
-              IsOk());
-  ASSERT_TRUE(destination.Close());
-
-  riegeli::StringReader<> result_reader(output_bytes);
-  GifExtractor extractor;
-  auto extracted = extractor.ExtractManifestStore(result_reader);
-  ABSL_ASSERT_OK(extracted);
-  EXPECT_THAT(*extracted, Not(IsEmpty()));
-
-  ASSERT_TRUE(result_reader.Seek(0));
-  auto validator = CreateTestValidator(pki);
-  auto validation_result = validator->Validate(result_reader, "image/gif");
-  ABSL_ASSERT_OK(validation_result);
-  EXPECT_THAT(
-      (*validation_result)->proto().active_manifest().validation().failures(),
-      IsEmpty());
+  return builder.GetAsset();
 }
 
-TEST(AssetWriterTest, WritesBmffManifestStore) {
-  std::string bmff_bytes = absl::StrCat(
+absl::StatusOr<std::string> CreateBmffAsset() {
+  return absl::StrCat(
       credentio_testing::Box("ftyp", {"isom\x00\x00\x02\x00isom", 12}),
       credentio_testing::Box("moov", ""),
       credentio_testing::Box("mdat", "video_payload"));
-
-  riegeli::StringReader<> source(bmff_bytes);
-  std::string output_bytes;
-  riegeli::StringWriter<> destination(&output_bytes);
-
-  GenerateManifestStoreRequest request;
-  request.mutable_actions()->mutable_created();
-
-  TestPki pki = CreateTestPki();
-  EXPECT_THAT(WriteAsset(CreateWriteOptions(pki, /*disable_timestamping=*/true),
-                         std::move(request), "video/mp4", source, destination),
-              IsOk());
-  ASSERT_TRUE(destination.Close());
-
-  riegeli::StringReader<> result_reader(output_bytes);
-  BmffExtractor extractor;
-  auto extracted = extractor.ExtractManifestStore(result_reader);
-  ABSL_ASSERT_OK(extracted);
-  EXPECT_THAT(*extracted, Not(IsEmpty()));
-
-  ASSERT_TRUE(result_reader.Seek(0));
-  auto validator = CreateTestValidator(pki);
-  auto validation_result = validator->Validate(result_reader, "video/mp4");
-  ABSL_ASSERT_OK(validation_result);
-  EXPECT_THAT(
-      (*validation_result)->proto().active_manifest().validation().failures(),
-      IsEmpty());
 }
 
-TEST(AssetWriterTest, WritesTiffManifestStore) {
+absl::StatusOr<std::string> CreateTiffAsset() {
   std::string tiff_bytes;
   riegeli::StringWriter<> writer(&tiff_bytes);
   riegeli::WriteBigEndian<uint32_t>(kTiffBigEndianHeader, writer);
@@ -239,103 +132,23 @@
   riegeli::WriteBigEndian<uint32_t>(1, writer);
   riegeli::WriteBigEndian<uint32_t>(10, writer);
   riegeli::WriteBigEndian<uint32_t>(0, writer);  // next IFD offset
-  ASSERT_TRUE(writer.Close());
-
-  riegeli::StringReader<> source(tiff_bytes);
-  std::string output_bytes;
-  riegeli::StringWriter<> destination(&output_bytes);
-
-  GenerateManifestStoreRequest request;
-  request.mutable_actions()->mutable_created();
-
-  TestPki pki = CreateTestPki();
-  EXPECT_THAT(WriteAsset(CreateWriteOptions(pki, /*disable_timestamping=*/true),
-                         std::move(request), "image/tiff", source, destination),
-              IsOk());
-  ASSERT_TRUE(destination.Close());
-
-  riegeli::StringReader<> result_reader(output_bytes);
-  TiffExtractor extractor;
-  auto extracted = extractor.ExtractManifestStore(result_reader);
-  ABSL_ASSERT_OK(extracted);
-  EXPECT_THAT(*extracted, Not(IsEmpty()));
-
-  ASSERT_TRUE(result_reader.Seek(0));
-  auto validator = CreateTestValidator(pki);
-  auto validation_result = validator->Validate(result_reader, "image/tiff");
-  ABSL_ASSERT_OK(validation_result);
-  EXPECT_THAT(
-      (*validation_result)->proto().active_manifest().validation().failures(),
-      IsEmpty());
+  if (!writer.Close()) return writer.status();
+  return tiff_bytes;
 }
 
-TEST(AssetWriterTest, WritesRiffManifestStore) {
-  std::string riff_bytes = credentio_riff::RiffChunk(
-      "WEBP", credentio_riff::Chunk("VP8 ", "image_data"));
-
-  riegeli::StringReader<> source(riff_bytes);
-  std::string output_bytes;
-  riegeli::StringWriter<> destination(&output_bytes);
-
-  GenerateManifestStoreRequest request;
-  request.mutable_actions()->mutable_created();
-
-  TestPki pki = CreateTestPki();
-  EXPECT_THAT(WriteAsset(CreateWriteOptions(pki, /*disable_timestamping=*/true),
-                         std::move(request), "image/webp", source, destination),
-              IsOk());
-  ASSERT_TRUE(destination.Close());
-
-  riegeli::StringReader<> result_reader(output_bytes);
-  RiffExtractor extractor;
-  auto extracted = extractor.ExtractManifestStore(result_reader);
-  ABSL_ASSERT_OK(extracted);
-  EXPECT_THAT(*extracted, Not(IsEmpty()));
-
-  ASSERT_TRUE(result_reader.Seek(0));
-  auto validator = CreateTestValidator(pki);
-  auto validation_result = validator->Validate(result_reader, "image/webp");
-  ABSL_ASSERT_OK(validation_result);
-  EXPECT_THAT(
-      (*validation_result)->proto().active_manifest().validation().failures(),
-      IsEmpty());
+absl::StatusOr<std::string> CreateWebpAsset() {
+  return credentio_riff::RiffChunk("WEBP",
+                                   credentio_riff::Chunk("VP8 ", "image_data"));
 }
 
-TEST(AssetWriterTest, WritesId3ManifestStore) {
+absl::StatusOr<std::string> CreateMp3Asset() {
   constexpr char kMp3Bytes[] = {'\xFF', '\xF3', '\x40', '\xC0',
                                 '\x01', '\x02', '\x03', '\x04'};
-  std::string id3_bytes(kMp3Bytes, sizeof(kMp3Bytes));
-
-  riegeli::StringReader<> source(id3_bytes);
-  std::string output_bytes;
-  riegeli::StringWriter<> destination(&output_bytes);
-
-  GenerateManifestStoreRequest request;
-  request.mutable_actions()->mutable_created();
-
-  TestPki pki = CreateTestPki();
-  EXPECT_THAT(WriteAsset(CreateWriteOptions(pki, /*disable_timestamping=*/true),
-                         std::move(request), "audio/mpeg", source, destination),
-              IsOk());
-  ASSERT_TRUE(destination.Close());
-
-  riegeli::StringReader<> result_reader(output_bytes);
-  Id3Extractor extractor;
-  auto extracted = extractor.ExtractManifestStore(result_reader);
-  ABSL_ASSERT_OK(extracted);
-  EXPECT_THAT(*extracted, Not(IsEmpty()));
-
-  ASSERT_TRUE(result_reader.Seek(0));
-  auto validator = CreateTestValidator(pki);
-  auto validation_result = validator->Validate(result_reader, "audio/mpeg");
-  ABSL_ASSERT_OK(validation_result);
-  EXPECT_THAT(
-      (*validation_result)->proto().active_manifest().validation().failures(),
-      IsEmpty());
+  return std::string(kMp3Bytes, sizeof(kMp3Bytes));
 }
 
-TEST(AssetWriterTest, WritesPdfManifestStore) {
-  constexpr absl::string_view kPdfBytes = R"(%PDF-1.7
+absl::StatusOr<std::string> CreatePdfAsset() {
+  return std::string(R"(%PDF-1.7
 %%%%%
 1 0 obj
 /test_name
@@ -357,161 +170,124 @@
 startxref
 98
 %%EOF
-)";
-
-  riegeli::StringReader<> source(kPdfBytes);
-  std::string output_bytes;
-  riegeli::StringWriter<> destination(&output_bytes);
-
-  GenerateManifestStoreRequest request;
-  request.mutable_actions()->mutable_created();
-
-  TestPki pki = CreateTestPki();
-  EXPECT_THAT(
-      WriteAsset(CreateWriteOptions(pki, /*disable_timestamping=*/true),
-                 std::move(request), "application/pdf", source, destination),
-      IsOk());
-  ASSERT_TRUE(destination.Close());
-
-  riegeli::StringReader<> result_reader(output_bytes);
-  PdfExtractor extractor;
-  auto extracted = extractor.ExtractManifestStore(result_reader);
-  ABSL_ASSERT_OK(extracted);
-  EXPECT_THAT(*extracted, Not(IsEmpty()));
-
-  ASSERT_TRUE(result_reader.Seek(0));
-  auto validator = CreateTestValidator(pki);
-  auto validation_result =
-      validator->Validate(result_reader, "application/pdf");
-  ABSL_ASSERT_OK(validation_result);
-  EXPECT_THAT(
-      (*validation_result)->proto().active_manifest().validation().failures(),
-      IsEmpty());
+)");
 }
 
-TEST(AssetWriterTest, WritesZipManifestStore) {
-  absl::StatusOr<std::string> pptx_bytes = credentio_testing::GetContents(
+absl::StatusOr<std::string> CreatePptxAsset() {
+  return credentio_testing::GetContents(
       "credentio/formats/zip/testing/"
       "no_c2pa_032_asset.pptx");
-  ABSL_ASSERT_OK(pptx_bytes);
-  riegeli::StringReader<> source(*pptx_bytes);
+}
+
+struct FormatTestCase {
+  std::string test_name;
+  // File extension used by `WriteAssetFile` to infer the media type.
+  std::string extension;
+  std::string media_type;
+  std::function<absl::StatusOr<std::string>()> create_asset;
+};
+
+class AssetWriterFormatTest : public testing::TestWithParam<FormatTestCase> {
+ protected:
+  // Checks that `signed_asset` has an active manifest that validates against
+  // `pki` without failures.
+  void ExpectSignedAndValid(riegeli::Reader& signed_asset, const TestPki& pki) {
+    auto validation_result =
+        CreateTestValidator(pki)->Validate(signed_asset, GetParam().media_type);
+    ABSL_ASSERT_OK(validation_result);
+    const ValidationStatusSet& validation =
+        (*validation_result)->proto().active_manifest().validation();
+    EXPECT_THAT(validation.failures(), IsEmpty());
+    EXPECT_THAT(validation.successes(), Not(IsEmpty()));
+  }
+};
+
+TEST_P(AssetWriterFormatTest, WriteAsset) {
+  const FormatTestCase& test_case = GetParam();
+  absl::StatusOr<std::string> asset_bytes = test_case.create_asset();
+  ABSL_ASSERT_OK(asset_bytes);
+
+  riegeli::StringReader<> source(*asset_bytes);
   std::string output_bytes;
   riegeli::StringWriter<> destination(&output_bytes);
 
-  GenerateManifestStoreRequest request;
-  request.mutable_actions()->mutable_created();
-
-  constexpr absl::string_view kPptxMediaType =
-      "application/"
-      "vnd.openxmlformats-officedocument.presentationml.presentation";
-
   TestPki pki = CreateTestPki();
-  EXPECT_THAT(
-      WriteAsset(CreateWriteOptions(pki, /*disable_timestamping=*/true),
-                 std::move(request), kPptxMediaType, source, destination),
-      IsOk());
+  ASSERT_THAT(WriteAsset(CreateWriteOptions(pki), CreateRequest(),
+                         test_case.media_type, source, destination),
+              IsOk());
   ASSERT_TRUE(destination.Close());
 
   riegeli::StringReader<> result_reader(output_bytes);
-  ZipExtractor extractor;
-  auto extracted = extractor.ExtractManifestStore(result_reader);
-  ABSL_ASSERT_OK(extracted);
-  EXPECT_THAT(*extracted, Not(IsEmpty()));
-
-  ASSERT_TRUE(result_reader.Seek(0));
-  auto validator = CreateTestValidator(pki);
-  auto validation_result = validator->Validate(result_reader, kPptxMediaType);
-  ABSL_ASSERT_OK(validation_result);
-  EXPECT_THAT(
-      (*validation_result)->proto().active_manifest().validation().failures(),
-      IsEmpty());
+  ExpectSignedAndValid(result_reader, pki);
 }
 
-TEST(AssetWriterTest, WritesPngWithTimestamp) {
-  auto png_or = CreatePng({Chunk{.type = 0x49444154, .payload = "pixel_data"}});
-  ABSL_ASSERT_OK(png_or);
-
-  riegeli::StringReader<> source(*png_or);
-  std::string output_bytes;
-  riegeli::StringWriter<> destination(&output_bytes);
-
-  GenerateManifestStoreRequest request;
-  request.mutable_actions()->mutable_created();
-
-  TestPki pki = CreateTestPki();
-  EXPECT_THAT(
-      WriteAsset(CreateWriteOptions(pki, /*disable_timestamping=*/false),
-                 std::move(request), "image/png", source, destination),
-      IsOk());
-  ASSERT_TRUE(destination.Close());
-
-  riegeli::StringReader<> result_reader(output_bytes);
-  auto validator = CreateTestValidator(pki);
-  auto validation_result = validator->Validate(result_reader, "image/png");
-  ABSL_ASSERT_OK(validation_result);
-  EXPECT_THAT(
-      (*validation_result)->proto().active_manifest().validation().failures(),
-      IsEmpty());
-}
-
-TEST(AssetWriterTest, WriteAssetFileWritesAndValidatesPng) {
-  auto png_or = CreatePng({Chunk{.type = 0x49444154, .payload = "pixel_data"}});
-  ABSL_ASSERT_OK(png_or);
+// Unlike `WriteAsset` above, this writes to a real file through
+// `riegeli::CFileWriter`, which e.g. `ZipWriter` must be able to read back.
+TEST_P(AssetWriterFormatTest, WriteAssetFile) {
+  const FormatTestCase& test_case = GetParam();
+  absl::StatusOr<std::string> asset_bytes = test_case.create_asset();
+  ABSL_ASSERT_OK(asset_bytes);
 
   const std::string input_path =
-      absl::StrCat(testing::TempDir(), "/test_input.png");
+      absl::StrCat(testing::TempDir(), "/", test_case.test_name, "_input.",
+                   test_case.extension);
   const std::string output_path =
-      absl::StrCat(testing::TempDir(), "/test_output.png");
-
+      absl::StrCat(testing::TempDir(), "/", test_case.test_name, "_output.",
+                   test_case.extension);
   {
     std::ofstream out(input_path, std::ios::binary);
-    out << *png_or;
+    out << *asset_bytes;
+    ASSERT_TRUE(out.good());
   }
 
-  GenerateManifestStoreRequest request;
-  request.mutable_actions()->mutable_created();
-
   TestPki pki = CreateTestPki();
-  EXPECT_THAT(
-      WriteAssetFile(CreateWriteOptions(pki, /*disable_timestamping=*/true),
-                     std::move(request), input_path, output_path),
-      IsOk());
+  ASSERT_THAT(WriteAssetFile(CreateWriteOptions(pki), CreateRequest(),
+                             input_path, output_path),
+              IsOk());
 
   riegeli::CFileReader<> result_reader(output_path);
   ASSERT_TRUE(result_reader.ok());
-  auto validator = CreateTestValidator(pki);
-  auto validation_result = validator->Validate(result_reader, "image/png");
-  ABSL_ASSERT_OK(validation_result);
-  EXPECT_THAT(
-      (*validation_result)->proto().active_manifest().validation().failures(),
-      IsEmpty());
+  ExpectSignedAndValid(result_reader, pki);
 }
 
+INSTANTIATE_TEST_SUITE_P(
+    AllFormats, AssetWriterFormatTest,
+    testing::ValuesIn(std::vector<FormatTestCase>{
+        {"Png", "png", "image/png", CreatePngAsset},
+        {"Jpeg", "jpg", "image/jpeg", CreateJpegAsset},
+        {"Gif", "gif", "image/gif", CreateGifAsset},
+        {"Bmff", "mp4", "video/mp4", CreateBmffAsset},
+        {"Tiff", "tiff", "image/tiff", CreateTiffAsset},
+        {"Webp", "webp", "image/webp", CreateWebpAsset},
+        {"Mp3", "mp3", "audio/mpeg", CreateMp3Asset},
+        {"Pdf", "pdf", "application/pdf", CreatePdfAsset},
+        {"Pptx", "pptx", std::string(kPptxMediaType), CreatePptxAsset},
+    }),
+    [](const testing::TestParamInfo<FormatTestCase>& info) {
+      return info.param.test_name;
+    });
+
 TEST(AssetWriterTest, RejectsInPlaceWrite) {
-  auto png_or = CreatePng({Chunk{.type = 0x49444154, .payload = "pixel_data"}});
-  ABSL_ASSERT_OK(png_or);
+  absl::StatusOr<std::string> png_bytes = CreatePngAsset();
+  ABSL_ASSERT_OK(png_bytes);
 
   const std::string path =
       absl::StrCat(testing::TempDir(), "/test_in_place.png");
   {
     std::ofstream out(path, std::ios::binary);
-    out << *png_or;
+    out << *png_bytes;
   }
 
-  GenerateManifestStoreRequest request;
-  request.mutable_actions()->mutable_created();
-
   TestPki pki = CreateTestPki();
   EXPECT_THAT(
-      WriteAssetFile(CreateWriteOptions(pki, /*disable_timestamping=*/true),
-                     std::move(request), path, path),
+      WriteAssetFile(CreateWriteOptions(pki), CreateRequest(), path, path),
       StatusIs(absl::StatusCode::kInvalidArgument));
 
   // The input asset must be left untouched.
   std::ifstream in(path, std::ios::binary);
   const std::string contents((std::istreambuf_iterator<char>(in)),
                              std::istreambuf_iterator<char>());
-  EXPECT_EQ(contents, *png_or);
+  EXPECT_EQ(contents, *png_bytes);
 }
 
 TEST(AssetWriterTest, RejectsUnsupportedMediaType) {
@@ -519,13 +295,9 @@
   std::string output_bytes;
   riegeli::StringWriter<> destination(&output_bytes);
 
-  GenerateManifestStoreRequest request;
-  request.mutable_actions()->mutable_created();
-
   TestPki pki = CreateTestPki();
-  EXPECT_THAT(WriteAsset(CreateWriteOptions(pki, /*disable_timestamping=*/true),
-                         std::move(request), "application/octet-stream", source,
-                         destination),
+  EXPECT_THAT(WriteAsset(CreateWriteOptions(pki), CreateRequest(),
+                         "application/octet-stream", source, destination),
               StatusIs(absl::StatusCode::kUnimplemented));
 }