blob: 75c365420e5f6c33396f24c600f9fb67625749b6 [file]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "tools/asset_writer.h"
#include <cstdint>
#include <fstream>
#include <ios>
#include <iterator>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/log/absl_check.h"
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.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/string_reader.h"
#include "riegeli/bytes/string_writer.h"
#include "riegeli/endian/endian_writing.h"
#include "testing/crypto_testing.h"
#include "testing/local_key_crypto_write_handler.h"
#include "testing/test_file_utils.h"
#include "validator/asset_validator_impl.h"
#include "validator/validator_options.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::StatusIs;
using ::testing::IsEmpty;
using ::testing::Not;
LocalKeyCryptoWriteHandlerOptions CreateWriteOptions(
const TestPki& pki, bool disable_timestamping) {
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;
}
return options;
}
// Returns a validator that enforces claim signer and TSA trust against the
// root CA of `pki`.
std::unique_ptr<AssetValidatorImpl> CreateTestValidator(const TestPki& pki) {
DefaultCryptoReadHandlerOptions crypto_options;
crypto_options.claim_signer_trust_anchors_pem = pki.root_ca.cert_pem;
crypto_options.tsa_trust_anchors_pem = pki.root_ca.cert_pem;
auto crypto_read_handler = CreateDefaultCryptoReadHandler(crypto_options);
ABSL_CHECK_OK(crypto_read_handler);
return std::make_unique<AssetValidatorImpl>(ValidatorOptions{
.crypto_read_handler = *std::move(crypto_read_handler),
});
}
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 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());
}
TEST(AssetWriterTest, WritesJpegManifestStore) {
std::string jpeg_bytes = CreateJpeg({});
riegeli::StringReader<> source(jpeg_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/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());
}
TEST(AssetWriterTest, WritesGifManifestStore) {
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());
}
TEST(AssetWriterTest, WritesBmffManifestStore) {
std::string bmff_bytes = 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) {
std::string tiff_bytes;
riegeli::StringWriter<> writer(&tiff_bytes);
riegeli::WriteBigEndian<uint32_t>(kTiffBigEndianHeader, writer);
riegeli::WriteBigEndian<uint32_t>(8, writer); // Offset to first IFD
riegeli::WriteBigEndian<uint16_t>(1, writer); // 1 entry
riegeli::WriteBigEndian<uint16_t>(0x0100, writer);
riegeli::WriteBigEndian<uint16_t>(kTiffTypeShort, writer);
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());
}
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());
}
TEST(AssetWriterTest, WritesId3ManifestStore) {
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());
}
TEST(AssetWriterTest, WritesPdfManifestStore) {
constexpr absl::string_view kPdfBytes = R"(%PDF-1.7
%%%%%
1 0 obj
/test_name
endobj
2 0 obj
123.456
endobj
3 0 obj
<</Type /Catalog>>
endobj
xref
0 4
0000000000 65535 f
0000000015 00000 n
0000000041 00000 n
0000000064 00000 n
trailer
<</Root 3 0 R /Size 4>>
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(
"credentio/formats/zip/testing/"
"no_c2pa_032_asset.pptx");
ABSL_ASSERT_OK(pptx_bytes);
riegeli::StringReader<> source(*pptx_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_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());
}
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);
const std::string input_path =
absl::StrCat(testing::TempDir(), "/test_input.png");
const std::string output_path =
absl::StrCat(testing::TempDir(), "/test_output.png");
{
std::ofstream out(input_path, std::ios::binary);
out << *png_or;
}
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());
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());
}
TEST(AssetWriterTest, RejectsInPlaceWrite) {
auto png_or = CreatePng({Chunk{.type = 0x49444154, .payload = "pixel_data"}});
ABSL_ASSERT_OK(png_or);
const std::string path =
absl::StrCat(testing::TempDir(), "/test_in_place.png");
{
std::ofstream out(path, std::ios::binary);
out << *png_or;
}
GenerateManifestStoreRequest request;
request.mutable_actions()->mutable_created();
TestPki pki = CreateTestPki();
EXPECT_THAT(
WriteAssetFile(CreateWriteOptions(pki, /*disable_timestamping=*/true),
std::move(request), 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);
}
TEST(AssetWriterTest, RejectsUnsupportedMediaType) {
riegeli::StringReader<> source("fake_unknown_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), "application/octet-stream", source,
destination),
StatusIs(absl::StatusCode::kUnimplemented));
}
} // namespace
} // namespace credentio