blob: 1f0b29a29fb9f85cb6ed2e9c5ee8b49cd1765e36 [file] [edit]
// 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 "formats/jpeg/writer.h"
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/log/absl_check.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/status_matchers.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/notification.h"
#include "async/async_context.h"
#include "formats/byte_range.h"
#include "formats/jpeg/extractor.h"
#include "formats/jpeg/reader.h"
#include "formats/jpeg/testing/jpeg_creator.h"
#include "formats/jpeg/testing/jumbf_creator.h"
#include "generator/generator.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/reader.h"
#include "riegeli/bytes/string_reader.h"
#include "riegeli/bytes/string_writer.h"
#include "testing/fake_manifest_store_generator.h"
#include "testing/nonprod_generator.h"
#include "testing/nonprod_validator.h"
#include "testing/proto_test_utils.h"
#include "validator/asset_validator.h"
#include "validator/result.h"
namespace credentio {
namespace {
using ::absl_testing::IsOkAndHolds;
using ::absl_testing::StatusIs;
using ::credentio_testing::ParseTextProtoOrDie;
using ::credentio_testing::PartiallyEqualsProto;
using ::testing::AllOf;
using ::testing::Contains;
using ::testing::Field;
using ::testing::Gt;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::Not;
using ::testing::Optional;
using ::testing::SizeIs;
using ::testing::StartsWith;
constexpr char kExifData[] = "Exif\0\0fake exif data";
constexpr absl::string_view kExifPayload(kExifData, sizeof(kExifData) - 1);
// Returns a JPEG segment with the given marker and payload.
std::string Segment(uint16_t marker, absl::string_view payload) {
const uint16_t length = static_cast<uint16_t>(payload.size() + 2);
return absl::StrCat(
std::string({static_cast<char>(marker >> 8), static_cast<char>(marker),
static_cast<char>(length >> 8), static_cast<char>(length)}),
payload);
}
// Returns a minimal JPEG image: SOI, APP0 (JFIF), APP1 (Exif), DQT, EOI.
std::string CreateJpegImage() {
return CreateJpeg(JpegParams{
.payload = {
Segment(0xffe0,
absl::string_view("JFIF\0\x01\x02\0\0\x01\0\x01\0\0", 14)),
Segment(0xffe1, kExifPayload),
Segment(0xffdb, std::string(65, '\x01')),
}});
}
GenerateManifestStoreRequest CreateRequest() {
GenerateManifestStoreRequest request;
request.mutable_actions()
->mutable_created()
->mutable_action_metadata()
->set_description("test only");
return request;
}
// Returns a fake manifest store: a JUMBF box with a payload of the given size.
std::string CreateFakeManifestStore(size_t payload_size = 32) {
return CreateJumbf({.payload = std::string(payload_size, 'm')});
}
// Returns the byte offset of the Exif payload in `jpeg`.
size_t ExifOffset(absl::string_view jpeg) {
size_t offset = jpeg.find(kExifPayload);
ABSL_CHECK_NE(offset, absl::string_view::npos);
return offset;
}
class JpegWriterTest : public ::testing::Test {
protected:
void SetUp() override {
auto generator = CreateNonProdManifestStoreGenerator();
ABSL_CHECK_OK(generator);
generator_ = *std::move(generator);
writer_ = std::make_unique<JpegWriter>(generator_.get());
auto validator = CreateNonProdValidator();
ABSL_CHECK_OK(validator);
validator_ = *std::move(validator);
}
// Validates `asset` and returns the active manifest validation statuses.
absl::StatusOr<ValidationStatusSet> Validate(absl::string_view asset) {
riegeli::StringReader reader(asset);
ABSL_ASSIGN_OR_RETURN(std::unique_ptr<ValidationResult> result,
validator_->Validate(reader, "image/jpeg"));
return result->proto().active_manifest().validation();
}
// Expects `asset` to contain a manifest store and to be valid.
void ExpectValid(absl::string_view asset) {
riegeli::StringReader reader(asset);
EXPECT_THAT(
JpegExtractor().ExtractManifestStoreLocation(reader, {}),
IsOkAndHolds(Optional(AllOf(Field(&ByteRange::offset, Gt(0)),
Field(&ByteRange::length, Gt(0))))));
absl::StatusOr<ValidationStatusSet> validation = Validate(asset);
ABSL_ASSERT_OK(validation);
EXPECT_THAT(validation->failures(), IsEmpty());
EXPECT_THAT(validation->successes(),
Contains(PartiallyEqualsProto(
ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "assertion.dataHash.match"
)pb"))));
}
// Writes a manifest store into a copy of `image` using `writer`.
static absl::StatusOr<std::string> WriteCopy(
const JpegWriter& writer, GenerateManifestStoreRequest request,
absl::string_view image,
JpegWriter::OutputMetadata* output_metadata = nullptr) {
riegeli::StringReader source(image);
std::string result;
riegeli::StringWriter destination(&result);
absl::Status status = writer.Write(std::move(request), &source,
&destination, output_metadata);
if (!status.ok()) return status;
if (!destination.Close()) return destination.status();
return result;
}
std::unique_ptr<ManifestStoreGenerator> generator_;
std::unique_ptr<JpegWriter> writer_;
std::unique_ptr<AssetValidator> validator_;
};
TEST_F(JpegWriterTest, Write) {
std::string image = CreateJpegImage();
JpegWriter::OutputMetadata output_metadata;
absl::StatusOr<std::string> result =
WriteCopy(*writer_, CreateRequest(), image, &output_metadata);
ABSL_ASSERT_OK(result);
EXPECT_TRUE(output_metadata.timestamp_included);
EXPECT_GT(result->size(), image.size());
ExpectValid(*result);
absl::StatusOr<ValidationStatusSet> validation = Validate(*result);
ABSL_ASSERT_OK(validation);
EXPECT_THAT(validation->informationals(),
Not(Contains(PartiallyEqualsProto(
ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "assertion.dataHash.additionalExclusionsPresent"
)pb")))));
}
TEST_F(JpegWriterTest, WriteInsertsManifestStoreBeforeFirstNonAppSegment) {
FakeManifestStoreGenerator generator(CreateFakeManifestStore());
JpegWriter writer(&generator);
std::string image = CreateJpegImage();
size_t dqt_offset = image.find("\xff\xdb");
ASSERT_NE(dqt_offset, std::string::npos);
absl::StatusOr<std::string> result =
WriteCopy(writer, CreateRequest(), image);
ABSL_ASSERT_OK(result);
// Bytes before the insertion point are unchanged.
EXPECT_THAT(*result, StartsWith(image.substr(0, dqt_offset)));
// An APP11 segment containing the manifest store is inserted.
EXPECT_EQ(result->substr(dqt_offset, 2), "\xff\xeb");
EXPECT_THAT(*result, HasSubstr(generator.manifest_store()));
// Bytes after the insertion point are unchanged.
EXPECT_EQ(result->substr(result->size() - (image.size() - dqt_offset)),
image.substr(dqt_offset));
}
TEST_F(JpegWriterTest, WriteWithoutOutputMetadata) {
absl::StatusOr<std::string> result =
WriteCopy(*writer_, CreateRequest(), CreateJpegImage());
ABSL_ASSERT_OK(result);
ExpectValid(*result);
}
TEST_F(JpegWriterTest, WriteWithoutTimestamp) {
auto generator =
CreateNonProdManifestStoreGenerator({.disable_timestamping = true});
ABSL_ASSERT_OK(generator);
JpegWriter writer(generator->get());
JpegWriter::OutputMetadata output_metadata;
absl::StatusOr<std::string> result =
WriteCopy(writer, CreateRequest(), CreateJpegImage(), &output_metadata);
ABSL_ASSERT_OK(result);
EXPECT_FALSE(output_metadata.timestamp_included);
ExpectValid(*result);
}
TEST_F(JpegWriterTest, WriteInPlace) {
std::string image = CreateJpegImage();
const size_t original_size = image.size();
riegeli::StringWriter<> asset(
&image, riegeli::StringWriterBase::Options().set_append(true));
ABSL_ASSERT_OK(asset.status());
JpegWriter::OutputMetadata output_metadata;
ABSL_ASSERT_OK(writer_->Write(CreateRequest(), &asset, &output_metadata));
ASSERT_TRUE(asset.Close());
EXPECT_TRUE(output_metadata.timestamp_included);
EXPECT_GT(image.size(), original_size);
ExpectValid(image);
}
TEST_F(JpegWriterTest, WriteInPlaceWithoutOutputMetadata) {
std::string image = CreateJpegImage();
riegeli::StringWriter<> asset(
&image, riegeli::StringWriterBase::Options().set_append(true));
ABSL_ASSERT_OK(asset.status());
ABSL_ASSERT_OK(writer_->Write(CreateRequest(), &asset));
ASSERT_TRUE(asset.Close());
ExpectValid(image);
}
TEST_F(JpegWriterTest, WriteInPlaceFailsWhenRandomAccessNotSupported) {
class NonRandomAccessWriter : public riegeli::StringWriter<> {
public:
using StringWriter::StringWriter;
bool SupportsRandomAccess() override { return false; }
};
std::string image = CreateJpegImage();
NonRandomAccessWriter asset(&image);
EXPECT_THAT(writer_->Write(CreateRequest(), &asset),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("asset does not support random access")));
}
TEST_F(JpegWriterTest, WriteInPlaceFailsWhenReadModeNotSupported) {
class NonReadModeWriter : public riegeli::StringWriter<> {
public:
using StringWriter::StringWriter;
bool SupportsReadMode() override { return false; }
};
std::string image = CreateJpegImage();
NonReadModeWriter asset(
&image, riegeli::StringWriterBase::Options().set_append(true));
EXPECT_THAT(writer_->Write(CreateRequest(), &asset),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("asset does not support read mode")));
}
TEST_F(JpegWriterTest, WriteWithHardBindingFails) {
GenerateManifestStoreRequest request = CreateRequest();
request.mutable_hard_binding();
EXPECT_THAT(WriteCopy(*writer_, request, CreateJpegImage()),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("must not include `hard_binding`")));
}
TEST_F(JpegWriterTest, WriteInPlaceWithHardBindingFails) {
GenerateManifestStoreRequest request = CreateRequest();
request.mutable_hard_binding();
std::string image = CreateJpegImage();
const std::string original = image;
riegeli::StringWriter<> asset(
&image, riegeli::StringWriterBase::Options().set_append(true));
EXPECT_THAT(writer_->Write(request, &asset),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("must not include `hard_binding`")));
ASSERT_TRUE(asset.Close());
EXPECT_EQ(image, original);
}
TEST_F(JpegWriterTest, WriteWithMultiAssetHash) {
GenerateManifestStoreRequest request = CreateRequest();
request.mutable_options()->set_generate_multi_asset_hash(true);
absl::StatusOr<std::string> result =
WriteCopy(*writer_, request, CreateJpegImage());
ABSL_ASSERT_OK(result);
ExpectValid(*result);
}
TEST_F(JpegWriterTest, WriteFailsWhenNoInsertionPointFound) {
EXPECT_THAT(
WriteCopy(*writer_, CreateRequest(),
absl::string_view("\xff\xd8\xff\xe0\x00\x02", 6)),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("No valid C2PA insertion point found in JPEG")));
}
TEST_F(JpegWriterTest, WriteFailsWhenManifestStoreAlreadyExists) {
absl::StatusOr<std::string> result =
WriteCopy(*writer_, CreateRequest(), CreateJpegImage());
ABSL_ASSERT_OK(result);
EXPECT_THAT(WriteCopy(*writer_, CreateRequest(), *result),
StatusIs(absl::StatusCode::kAlreadyExists,
HasSubstr("C2PA chunk already exists")));
}
TEST_F(JpegWriterTest, ModifyingHashedBytesInvalidatesAsset) {
absl::StatusOr<std::string> result =
WriteCopy(*writer_, CreateRequest(), CreateJpegImage());
ABSL_ASSERT_OK(result);
(*result)[ExifOffset(*result) + kExifPayload.size() - 1] ^= 0xff;
absl::StatusOr<ValidationStatusSet> validation = Validate(*result);
ABSL_ASSERT_OK(validation);
EXPECT_THAT(
validation->failures(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "assertion.dataHash.mismatch"
)pb"))));
}
TEST_F(JpegWriterTest, WriteAsync) {
std::string image = CreateJpegImage();
riegeli::StringReader source(image);
std::string result;
riegeli::StringWriter destination(&result);
absl::Notification done;
absl::Status write_status;
JpegWriter::OutputMetadata output_metadata;
writer_->Write(CreateRequest(), &source, &destination, &output_metadata,
AsyncContext(), [&](absl::Status status) {
write_status = std::move(status);
done.Notify();
});
done.WaitForNotification();
ABSL_ASSERT_OK(write_status);
ASSERT_TRUE(destination.Close());
EXPECT_TRUE(output_metadata.timestamp_included);
ExpectValid(result);
}
TEST_F(JpegWriterTest, WriteInPlaceAsync) {
std::string image = CreateJpegImage();
riegeli::StringWriter<> asset(
&image, riegeli::StringWriterBase::Options().set_append(true));
absl::Notification done;
absl::Status write_status;
writer_->Write(CreateRequest(), &asset, /*output_metadata=*/nullptr,
AsyncContext(), [&](absl::Status status) {
write_status = std::move(status);
done.Notify();
});
done.WaitForNotification();
ABSL_ASSERT_OK(write_status);
ASSERT_TRUE(asset.Close());
ExpectValid(image);
}
TEST_F(JpegWriterTest, WriteAsyncReportsErrorThroughCallback) {
GenerateManifestStoreRequest request = CreateRequest();
request.mutable_hard_binding();
std::string image = CreateJpegImage();
riegeli::StringReader source(image);
std::string result;
riegeli::StringWriter destination(&result);
absl::Notification done;
absl::Status write_status;
writer_->Write(request, &source, &destination, /*output_metadata=*/nullptr,
AsyncContext(), [&](absl::Status status) {
write_status = std::move(status);
done.Notify();
});
done.WaitForNotification();
EXPECT_THAT(write_status, StatusIs(absl::StatusCode::kInvalidArgument));
}
TEST_F(JpegWriterTest, ExclusionProcessorExcludesBytesFromHardBinding) {
std::vector<std::string> visited_segments;
JpegWriter writer(
generator_.get(),
{.exclusion_processor = [&visited_segments](
riegeli::Reader&, const JpegSegment& segment,
const GenerateManifestStoreRequest& request)
-> absl::StatusOr<std::vector<ByteRange>> {
EXPECT_EQ(request.actions().created().action_metadata().description(),
"test only");
visited_segments.push_back(segment.marker_label);
if (segment.marker_label != "APP1") return std::vector<ByteRange>();
return std::vector<ByteRange>{{.offset = segment.payload_offset,
.length = segment.payload_length}};
}});
absl::StatusOr<std::string> result =
WriteCopy(writer, CreateRequest(), CreateJpegImage());
ABSL_ASSERT_OK(result);
EXPECT_THAT(visited_segments, Contains("APP0"));
EXPECT_THAT(visited_segments, Contains("APP1"));
EXPECT_THAT(visited_segments, Contains("DQT"));
ExpectValid(*result);
absl::StatusOr<ValidationStatusSet> validation = Validate(*result);
ABSL_ASSERT_OK(validation);
EXPECT_THAT(
validation->informationals(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "assertion.dataHash.additionalExclusionsPresent"
)pb"))));
// Modifying excluded bytes does not invalidate the asset.
(*result)[ExifOffset(*result) + kExifPayload.size() - 1] ^= 0xff;
ExpectValid(*result);
}
TEST_F(JpegWriterTest, ExclusionProcessorErrorIsPropagated) {
JpegWriter writer(
generator_.get(),
{.exclusion_processor = [](riegeli::Reader&, const JpegSegment&,
const GenerateManifestStoreRequest&)
-> absl::StatusOr<std::vector<ByteRange>> {
return absl::DataLossError("exclusion failure");
}});
EXPECT_THAT(WriteCopy(writer, CreateRequest(), CreateJpegImage()),
StatusIs(absl::StatusCode::kDataLoss, "exclusion failure"));
}
TEST_F(JpegWriterTest, ExclusionProcessorRangeOutsideSegmentFails) {
JpegWriter writer(
generator_.get(),
{.exclusion_processor = [](riegeli::Reader&, const JpegSegment& segment,
const GenerateManifestStoreRequest&)
-> absl::StatusOr<std::vector<ByteRange>> {
return std::vector<ByteRange>{
{.offset = segment.offset, .length = segment.length + 1}};
}});
EXPECT_THAT(WriteCopy(writer, CreateRequest(), CreateJpegImage()),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Invalid or overlapping exclusion range")));
}
TEST_F(JpegWriterTest, GeneratorErrorIsPropagated) {
FakeManifestStoreGenerator generator;
generator.set_status(absl::UnavailableError("generator failure"));
JpegWriter writer(&generator);
EXPECT_THAT(WriteCopy(writer, CreateRequest(), CreateJpegImage()),
StatusIs(absl::StatusCode::kUnavailable, "generator failure"));
}
TEST_F(JpegWriterTest, InPlaceGeneratorErrorLeavesAssetUnchanged) {
FakeManifestStoreGenerator generator;
generator.set_status(absl::UnavailableError("generator failure"));
JpegWriter writer(&generator);
std::string image = CreateJpegImage();
const std::string original = image;
riegeli::StringWriter<> asset(
&image, riegeli::StringWriterBase::Options().set_append(true));
EXPECT_THAT(writer.Write(CreateRequest(), &asset),
StatusIs(absl::StatusCode::kUnavailable, "generator failure"));
ASSERT_TRUE(asset.Close());
EXPECT_EQ(image, original);
}
TEST_F(JpegWriterTest, WriteFailsWithEmptyManifestStoreTemplate) {
FakeManifestStoreGenerator generator;
generator.mutable_manifest_store_template()->set_manifest_store_size(0);
JpegWriter writer(&generator);
EXPECT_THAT(WriteCopy(writer, CreateRequest(), CreateJpegImage()),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("invalid manifest store template size: 0")));
}
TEST_F(JpegWriterTest, WriteFailsWithTooLargeManifestStoreTemplate) {
FakeManifestStoreGenerator generator;
generator.mutable_manifest_store_template()->set_manifest_store_size(
100 * 1024 * 1024 + 1);
JpegWriter writer(&generator);
EXPECT_THAT(WriteCopy(writer, CreateRequest(), CreateJpegImage()),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("invalid manifest store template size")));
}
TEST_F(JpegWriterTest, OutputMetadataReflectsFinalizeResponse) {
FakeManifestStoreGenerator generator(CreateFakeManifestStore());
JpegWriter writer(&generator);
JpegWriter::OutputMetadata output_metadata;
generator.mutable_finalize_manifest_store_response()->set_is_timestamped(
true);
ABSL_ASSERT_OK(
WriteCopy(writer, CreateRequest(), CreateJpegImage(), &output_metadata));
EXPECT_TRUE(output_metadata.timestamp_included);
output_metadata.timestamp_included = true;
generator.mutable_finalize_manifest_store_response()->set_is_timestamped(
false);
ABSL_ASSERT_OK(
WriteCopy(writer, CreateRequest(), CreateJpegImage(), &output_metadata));
EXPECT_FALSE(output_metadata.timestamp_included);
}
TEST_F(JpegWriterTest, LargeManifestStoreIsSplitAcrossApp11Segments) {
// A manifest store larger than a single APP11 segment (64 KiB).
FakeManifestStoreGenerator generator(CreateFakeManifestStore(200 * 1024));
JpegWriter writer(&generator);
absl::StatusOr<std::string> result =
WriteCopy(writer, CreateRequest(), CreateJpegImage());
ABSL_ASSERT_OK(result);
std::vector<size_t> app11_offsets;
for (size_t pos = result->find("\xff\xeb"); pos != std::string::npos;
pos = result->find("\xff\xeb", pos + 1)) {
app11_offsets.push_back(pos);
}
EXPECT_THAT(app11_offsets, SizeIs(Gt(3)));
}
} // namespace
} // namespace credentio