| // 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/png/writer.h" | |
| #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/string_view.h" | |
| #include "absl/synchronization/notification.h" | |
| #include "async/async_context.h" | |
| #include "formats/byte_range.h" | |
| #include "formats/png/extractor.h" | |
| #include "formats/png/reader.h" | |
| #include "formats/png/test_utils.h" | |
| #include "generator/generator.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "proto/generator.pb.h" | |
| #include "proto/ingredient_assertion.pb.h" | |
| #include "proto/ingredient_validation_result.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/nonprod_generator.h" | |
| #include "testing/nonprod_validator.h" | |
| #include "validator/asset_validator.h" | |
| #include "validator/ingredient_result.h" | |
| #include "validator/ingredient_result_converter.h" | |
| #include "validator/result.h" | |
| namespace credentio { | |
| namespace { | |
| using ::absl_testing::IsOkAndHolds; | |
| using ::absl_testing::StatusIs; | |
| using ::testing::AllOf; | |
| using ::testing::Field; | |
| using ::testing::Gt; | |
| using ::testing::HasSubstr; | |
| using ::testing::IsEmpty; | |
| using ::testing::Optional; | |
| // Returns a minimal 13x17 RGB PNG image: signature, IHDR, IDAT, IEND. | |
| // | |
| // The IDAT payload is not a valid zlib stream; C2PA processing never decodes | |
| // the pixel data. | |
| absl::StatusOr<std::string> CreatePngImage() { | |
| return CreatePng({ | |
| {.type = 'IHDR', | |
| .payload = std::string("\x00\x00\x00\x0d" // Width: 13. | |
| "\x00\x00\x00\x11" // Height: 17. | |
| "\x08\x02\x00\x00\x00", // 8-bit RGB. | |
| 13)}, | |
| {.type = 'IDAT', .payload = std::string(64, '\x40')}, | |
| }); | |
| } | |
| GenerateManifestStoreRequest CreateRequest() { | |
| GenerateManifestStoreRequest request; | |
| request.mutable_actions() | |
| ->mutable_created() | |
| ->mutable_action_metadata() | |
| ->set_description("test only"); | |
| return request; | |
| } | |
| class PngWriterTest : public ::testing::Test { | |
| protected: | |
| void SetUp() override { | |
| auto generator = CreateNonProdManifestStoreGenerator(); | |
| ABSL_CHECK_OK(generator); | |
| generator_ = *std::move(generator); | |
| writer_ = std::make_unique<PngWriter>(generator_.get()); | |
| auto validator = CreateNonProdValidator(); | |
| ABSL_CHECK_OK(validator); | |
| validator_ = *std::move(validator); | |
| } | |
| // Validates `asset` and returns the ingredient validation result describing | |
| // it, as needed to reference it as an ingredient. | |
| absl::StatusOr<std::unique_ptr<IngredientResult>> ValidateIngredient( | |
| absl::string_view asset) { | |
| riegeli::StringReader reader(asset); | |
| ABSL_ASSIGN_OR_RETURN(std::unique_ptr<ValidationResult> result, | |
| validator_->Validate(reader, "image/png")); | |
| return IngredientResultConverter().Convert(std::move(*result)); | |
| } | |
| // Writes a manifest store into a copy of `image` using `writer`. | |
| static absl::StatusOr<std::string> WriteCopy( | |
| PngWriter& writer, GenerateManifestStoreRequest request, | |
| absl::string_view image, | |
| PngWriter::OutputMetadata* output_metadata = nullptr) { | |
| riegeli::StringReader source(image); | |
| std::string result; | |
| riegeli::StringWriter destination(&result); | |
| ABSL_RETURN_IF_ERROR(writer.Write(std::move(request), &source, &destination, | |
| output_metadata)); | |
| if (!destination.Close()) return destination.status(); | |
| return result; | |
| } | |
| std::unique_ptr<ManifestStoreGenerator> generator_; | |
| std::unique_ptr<PngWriter> writer_; | |
| std::unique_ptr<AssetValidator> validator_; | |
| }; | |
| TEST_F(PngWriterTest, Write) { | |
| absl::StatusOr<std::string> image = CreatePngImage(); | |
| ABSL_ASSERT_OK(image); | |
| PngWriter::OutputMetadata output_metadata; | |
| absl::StatusOr<std::string> result = | |
| WriteCopy(*writer_, CreateRequest(), *image, &output_metadata); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_TRUE(output_metadata.timestamp_included); | |
| riegeli::StringReader result_reader(*result); | |
| EXPECT_THAT(PngExtractor().ExtractManifestStoreLocation(result_reader, {}), | |
| IsOkAndHolds(Optional(AllOf(Field(&ByteRange::offset, Gt(0)), | |
| Field(&ByteRange::length, Gt(0)))))); | |
| } | |
| TEST_F(PngWriterTest, WriteWithoutTimestamp) { | |
| auto generator = | |
| CreateNonProdManifestStoreGenerator({.disable_timestamping = true}); | |
| ABSL_ASSERT_OK(generator); | |
| PngWriter writer(generator->get()); | |
| absl::StatusOr<std::string> image = CreatePngImage(); | |
| ABSL_ASSERT_OK(image); | |
| PngWriter::OutputMetadata output_metadata; | |
| ABSL_ASSERT_OK(WriteCopy(writer, CreateRequest(), *image, &output_metadata)); | |
| EXPECT_FALSE(output_metadata.timestamp_included); | |
| } | |
| TEST_F(PngWriterTest, ExistingC2paWithoutOpenedActionFails) { | |
| absl::StatusOr<std::string> image = CreatePngImage(); | |
| ABSL_ASSERT_OK(image); | |
| absl::StatusOr<std::string> png_with_c2pa = | |
| WriteCopy(*writer_, CreateRequest(), *image); | |
| ABSL_ASSERT_OK(png_with_c2pa); | |
| EXPECT_THAT( | |
| WriteCopy(*writer_, CreateRequest(), *png_with_c2pa), | |
| StatusIs(absl::StatusCode::kAlreadyExists, HasSubstr("already exists"))); | |
| } | |
| TEST_F(PngWriterTest, ExistingC2paWithOpenedActionSucceeds) { | |
| absl::StatusOr<std::string> image = CreatePngImage(); | |
| ABSL_ASSERT_OK(image); | |
| absl::StatusOr<std::string> png_with_c2pa = | |
| WriteCopy(*writer_, CreateRequest(), *image); | |
| ABSL_ASSERT_OK(png_with_c2pa); | |
| absl::StatusOr<std::unique_ptr<IngredientResult>> | |
| png_with_c2pa_validation_results = ValidateIngredient(*png_with_c2pa); | |
| ABSL_ASSERT_OK(png_with_c2pa_validation_results); | |
| EXPECT_THAT((*png_with_c2pa_validation_results) | |
| ->proto() | |
| .ingredient_validation_results() | |
| .active_manifest() | |
| .failures(), | |
| IsEmpty()); | |
| GenerateManifestStoreRequest request; | |
| request.mutable_actions()->mutable_opened()->set_ingredient_index(0); | |
| *request.add_ingredients()->mutable_ingredient_validation_results() = | |
| (*png_with_c2pa_validation_results)->proto(); | |
| absl::StatusOr<std::string> final_png = | |
| WriteCopy(*writer_, std::move(request), *png_with_c2pa); | |
| ABSL_ASSERT_OK(final_png); | |
| riegeli::StringReader source(*final_png); | |
| absl::StatusOr<std::unique_ptr<ValidationResult>> result = | |
| validator_->Validate(source, "image/png"); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT((*result)->proto().active_manifest().validation().failures(), | |
| IsEmpty()); | |
| } | |
| TEST_F(PngWriterTest, WriteWithHardBindingFails) { | |
| GenerateManifestStoreRequest request = CreateRequest(); | |
| request.mutable_hard_binding(); | |
| absl::StatusOr<std::string> image = CreatePngImage(); | |
| ABSL_ASSERT_OK(image); | |
| EXPECT_THAT(WriteCopy(*writer_, std::move(request), *image), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("must not include `hard_binding`"))); | |
| } | |
| TEST_F(PngWriterTest, WriteAsync) { | |
| absl::StatusOr<std::string> image = CreatePngImage(); | |
| ABSL_ASSERT_OK(image); | |
| riegeli::StringReader source(*image); | |
| std::string result; | |
| riegeli::StringWriter destination(&result); | |
| absl::Notification done; | |
| absl::Status write_status; | |
| PngWriter::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(source.Close()); | |
| ASSERT_TRUE(destination.Close()); | |
| EXPECT_TRUE(output_metadata.timestamp_included); | |
| riegeli::StringReader result_reader(result); | |
| EXPECT_THAT(PngExtractor().ExtractManifestStoreLocation(result_reader, {}), | |
| IsOkAndHolds(Optional(AllOf(Field(&ByteRange::offset, Gt(0)), | |
| Field(&ByteRange::length, Gt(0)))))); | |
| } | |
| TEST_F(PngWriterTest, WriteWithCustomExclusionProcessor) { | |
| int custom_processor_call_count = 0; | |
| PngWriter writer( | |
| generator_.get(), | |
| {.exclusion_processor = [&custom_processor_call_count]( | |
| riegeli::Reader&, const PngChunk&, | |
| const GenerateManifestStoreRequest&) | |
| -> absl::StatusOr<std::vector<ByteRange>> { | |
| ++custom_processor_call_count; | |
| return std::vector<ByteRange>(); | |
| }}); | |
| absl::StatusOr<std::string> image = CreatePngImage(); | |
| ABSL_ASSERT_OK(image); | |
| ABSL_ASSERT_OK(WriteCopy(writer, CreateRequest(), *image)); | |
| EXPECT_GT(custom_processor_call_count, 0); | |
| } | |
| } // namespace | |
| } // namespace credentio |