| // 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/riff/writer.h" | |
| #include <cstdint> | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include "absl/log/absl_check.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "formats/byte_range.h" | |
| #include "formats/riff/create_riff.h" | |
| #include "formats/riff/extractor.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/string_reader.h" | |
| #include "riegeli/bytes/string_writer.h" | |
| #include "testing/nonprod_generator.h" | |
| #include "testing/nonprod_validator.h" | |
| #include "testing/proto_test_utils.h" | |
| #include "validator/asset_validator.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; | |
| std::string CreateWebPImage() { | |
| return credentio_riff::RiffChunk("WEBP", | |
| credentio_riff::Chunk("VP8 ", "image_data")); | |
| } | |
| std::string CreateAviVideo() { | |
| return credentio_riff::RiffChunk( | |
| "AVI ", | |
| absl::StrCat( | |
| credentio_riff::ListChunk( | |
| "hdrl", credentio_riff::Chunk("avih", std::string(56, '\0'))), | |
| credentio_riff::ListChunk( | |
| "movi", credentio_riff::Chunk("00dc", "fake video frame data")))); | |
| } | |
| class RiffWriterTest : public ::testing::Test { | |
| protected: | |
| void SetUp() override { | |
| auto generator = CreateNonProdManifestStoreGenerator({}); | |
| ABSL_CHECK_OK(generator); | |
| generator_ = *std::move(generator); | |
| writer_ = std::make_unique<RiffWriter>(generator_.get()); | |
| auto validator = CreateNonProdValidator({}); | |
| ABSL_CHECK_OK(validator); | |
| validator_ = *std::move(validator); | |
| } | |
| std::unique_ptr<ManifestStoreGenerator> generator_; | |
| std::unique_ptr<RiffWriter> writer_; | |
| std::unique_ptr<AssetValidator> validator_; | |
| }; | |
| TEST_F(RiffWriterTest, Write) { | |
| GenerateManifestStoreRequest request; | |
| request.mutable_actions() | |
| ->mutable_created() | |
| ->mutable_action_metadata() | |
| ->set_description("test only"); | |
| std::string image = CreateWebPImage(); | |
| riegeli::StringReader source(image); | |
| ABSL_ASSERT_OK(source.status()); | |
| std::string result; | |
| riegeli::StringWriter destination(&result); | |
| ABSL_ASSERT_OK(destination.status()); | |
| RiffWriter::OutputMetadata output_metadata; | |
| ABSL_ASSERT_OK( | |
| writer_->Write(request, &source, &destination, &output_metadata)); | |
| ASSERT_TRUE(source.Close()); | |
| ASSERT_TRUE(destination.Close()); | |
| EXPECT_TRUE(output_metadata.timestamp_included); | |
| riegeli::StringReader result_reader(result); | |
| ABSL_ASSERT_OK(result_reader.status()); | |
| EXPECT_THAT(RiffExtractor().ExtractManifestStoreLocation(result_reader, {}), | |
| IsOkAndHolds(Optional(AllOf(Field(&ByteRange::offset, Gt(0)), | |
| Field(&ByteRange::length, Gt(0)))))); | |
| ASSERT_TRUE(result_reader.Seek(0)); | |
| auto validation_result = validator_->Validate(result_reader, "image/webp"); | |
| ABSL_ASSERT_OK(validation_result); | |
| EXPECT_THAT( | |
| (*validation_result)->proto().active_manifest().validation().failures(), | |
| IsEmpty()); | |
| EXPECT_THAT( | |
| (*validation_result) | |
| ->proto() | |
| .active_manifest() | |
| .validation() | |
| .informationals(), | |
| Not(Contains(PartiallyEqualsProto(ParseTextProtoOrDie<ValidationStatus>( | |
| R"pb( | |
| code: "assertion.dataHash.additionalExclusionsPresent" | |
| )pb"))))); | |
| } | |
| TEST_F(RiffWriterTest, WriteInPlace) { | |
| GenerateManifestStoreRequest request; | |
| request.mutable_actions() | |
| ->mutable_created() | |
| ->mutable_action_metadata() | |
| ->set_description("test only"); | |
| std::string image = CreateWebPImage(); | |
| riegeli::StringWriter<> asset( | |
| &image, riegeli::StringWriterBase::Options().set_append(true)); | |
| ABSL_ASSERT_OK(asset.status()); | |
| RiffWriter::OutputMetadata output_metadata; | |
| ABSL_ASSERT_OK(writer_->Write(request, &asset, &output_metadata)); | |
| ASSERT_TRUE(asset.Close()); | |
| EXPECT_TRUE(output_metadata.timestamp_included); | |
| riegeli::StringReader<> result_reader(image); | |
| ABSL_ASSERT_OK(result_reader.status()); | |
| EXPECT_THAT(RiffExtractor().ExtractManifestStoreLocation(result_reader, {}), | |
| IsOkAndHolds(Optional(AllOf(Field(&ByteRange::offset, Gt(0)), | |
| Field(&ByteRange::length, Gt(0)))))); | |
| ASSERT_TRUE(result_reader.Seek(0)); | |
| auto validation_result = validator_->Validate(result_reader, "image/webp"); | |
| ABSL_ASSERT_OK(validation_result); | |
| EXPECT_THAT( | |
| (*validation_result)->proto().active_manifest().validation().failures(), | |
| IsEmpty()); | |
| EXPECT_THAT( | |
| (*validation_result) | |
| ->proto() | |
| .active_manifest() | |
| .validation() | |
| .informationals(), | |
| Not(Contains(PartiallyEqualsProto(ParseTextProtoOrDie<ValidationStatus>( | |
| R"pb( | |
| code: "assertion.dataHash.additionalExclusionsPresent" | |
| )pb"))))); | |
| } | |
| TEST_F(RiffWriterTest, WriteAvi) { | |
| GenerateManifestStoreRequest request; | |
| request.mutable_actions() | |
| ->mutable_created() | |
| ->mutable_action_metadata() | |
| ->set_description("test avi"); | |
| std::string video = CreateAviVideo(); | |
| riegeli::StringReader source(video); | |
| ABSL_ASSERT_OK(source.status()); | |
| std::string result; | |
| riegeli::StringWriter destination(&result); | |
| ABSL_ASSERT_OK(destination.status()); | |
| RiffWriter::OutputMetadata output_metadata; | |
| ABSL_ASSERT_OK( | |
| writer_->Write(request, &source, &destination, &output_metadata)); | |
| ASSERT_TRUE(source.Close()); | |
| ASSERT_TRUE(destination.Close()); | |
| EXPECT_TRUE(output_metadata.timestamp_included); | |
| riegeli::StringReader result_reader(result); | |
| ABSL_ASSERT_OK(result_reader.status()); | |
| EXPECT_THAT(RiffExtractor().ExtractManifestStoreLocation(result_reader, {}), | |
| IsOkAndHolds(Optional(AllOf(Field(&ByteRange::offset, Gt(0)), | |
| Field(&ByteRange::length, Gt(0)))))); | |
| ASSERT_TRUE(result_reader.Seek(0)); | |
| auto validation_result = | |
| validator_->Validate(result_reader, "video/x-msvideo"); | |
| ABSL_ASSERT_OK(validation_result); | |
| EXPECT_THAT( | |
| (*validation_result)->proto().active_manifest().validation().failures(), | |
| IsEmpty()); | |
| EXPECT_THAT( | |
| (*validation_result) | |
| ->proto() | |
| .active_manifest() | |
| .validation() | |
| .informationals(), | |
| Not(Contains(PartiallyEqualsProto(ParseTextProtoOrDie<ValidationStatus>( | |
| R"pb( | |
| code: "assertion.dataHash.additionalExclusionsPresent" | |
| )pb"))))); | |
| ASSERT_TRUE(result_reader.Seek(0)); | |
| auto validation_result_no_hint = | |
| validator_->Validate(result_reader, /*media_type=*/{}); | |
| ABSL_ASSERT_OK(validation_result_no_hint); | |
| EXPECT_THAT((*validation_result_no_hint) | |
| ->proto() | |
| .active_manifest() | |
| .validation() | |
| .failures(), | |
| IsEmpty()); | |
| } | |
| TEST_F(RiffWriterTest, WriteWithOutputMetadataNoTimestamp) { | |
| auto generator = | |
| CreateNonProdManifestStoreGenerator({.disable_timestamping = true}); | |
| ABSL_ASSERT_OK(generator); | |
| auto writer = std::make_unique<RiffWriter>(generator->get()); | |
| GenerateManifestStoreRequest request; | |
| request.mutable_actions() | |
| ->mutable_created() | |
| ->mutable_action_metadata() | |
| ->set_description("test only"); | |
| std::string image = CreateWebPImage(); | |
| riegeli::StringReader source(image); | |
| ABSL_ASSERT_OK(source.status()); | |
| std::string result; | |
| riegeli::StringWriter destination(&result); | |
| ABSL_ASSERT_OK(destination.status()); | |
| RiffWriter::OutputMetadata output_metadata; | |
| ABSL_ASSERT_OK( | |
| writer->Write(request, &source, &destination, &output_metadata)); | |
| ASSERT_TRUE(source.Close()); | |
| ASSERT_TRUE(destination.Close()); | |
| EXPECT_FALSE(output_metadata.timestamp_included); | |
| } | |
| TEST_F(RiffWriterTest, WriteTwiceFails) { | |
| GenerateManifestStoreRequest request; | |
| request.mutable_actions() | |
| ->mutable_created() | |
| ->mutable_action_metadata() | |
| ->set_description("test only"); | |
| std::string image = CreateWebPImage(); | |
| riegeli::StringReader source1(image); | |
| ABSL_ASSERT_OK(source1.status()); | |
| std::string result1; | |
| riegeli::StringWriter destination1(&result1); | |
| ABSL_ASSERT_OK(destination1.status()); | |
| ABSL_ASSERT_OK(writer_->Write(request, &source1, &destination1)); | |
| ASSERT_TRUE(source1.Close()); | |
| ASSERT_TRUE(destination1.Close()); | |
| riegeli::StringReader source2(result1); | |
| ABSL_ASSERT_OK(source2.status()); | |
| std::string result2; | |
| riegeli::StringWriter destination2(&result2); | |
| ABSL_ASSERT_OK(destination2.status()); | |
| EXPECT_THAT( | |
| writer_->Write(request, &source2, &destination2), | |
| StatusIs(absl::StatusCode::kAlreadyExists, HasSubstr("already exists"))); | |
| } | |
| TEST_F(RiffWriterTest, WriteWithHardBindingFails) { | |
| GenerateManifestStoreRequest request; | |
| request.mutable_actions() | |
| ->mutable_created() | |
| ->mutable_action_metadata() | |
| ->set_description("test only"); | |
| request.mutable_hard_binding(); | |
| std::string image = CreateWebPImage(); | |
| riegeli::StringReader source(image); | |
| ABSL_ASSERT_OK(source.status()); | |
| std::string result; | |
| riegeli::StringWriter destination(&result); | |
| ABSL_ASSERT_OK(destination.status()); | |
| EXPECT_THAT(writer_->Write(request, &source, &destination), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("must not include `hard_binding`"))); | |
| } | |
| TEST_F(RiffWriterTest, DemonstratesUnboundedMemoryBufferingDoS) { | |
| GenerateManifestStoreRequest request; | |
| request.mutable_actions() | |
| ->mutable_created() | |
| ->mutable_action_metadata() | |
| ->set_description("demonstrate unbounded buffering DoS"); | |
| constexpr uint32_t kJunkPayloadSize = 10 * 1024 * 1024; // 10 MiB | |
| constexpr uint32_t kTotalRiffSize = 4 + 8 + kJunkPayloadSize; | |
| std::string large_riff_data; | |
| large_riff_data.reserve(8 + kTotalRiffSize); | |
| // RIFF Header: "RIFF" <total_riff_size> "WEBP" | |
| large_riff_data.append("RIFF", 4); | |
| large_riff_data.push_back(static_cast<char>(kTotalRiffSize & 0xFF)); | |
| large_riff_data.push_back(static_cast<char>((kTotalRiffSize >> 8) & 0xFF)); | |
| large_riff_data.push_back(static_cast<char>((kTotalRiffSize >> 16) & 0xFF)); | |
| large_riff_data.push_back(static_cast<char>((kTotalRiffSize >> 24) & 0xFF)); | |
| large_riff_data.append("WEBP", 4); | |
| // JUNK Subchunk: "JUNK" <junk_payload_size> <junk_payload> | |
| large_riff_data.append("JUNK", 4); | |
| large_riff_data.push_back(static_cast<char>(kJunkPayloadSize & 0xFF)); | |
| large_riff_data.push_back(static_cast<char>((kJunkPayloadSize >> 8) & 0xFF)); | |
| large_riff_data.push_back(static_cast<char>((kJunkPayloadSize >> 16) & 0xFF)); | |
| large_riff_data.push_back(static_cast<char>((kJunkPayloadSize >> 24) & 0xFF)); | |
| large_riff_data.append(kJunkPayloadSize, 'A'); | |
| riegeli::StringReader source(large_riff_data); | |
| ABSL_ASSERT_OK(source.status()); | |
| std::string result; | |
| riegeli::StringWriter destination(&result); | |
| ABSL_ASSERT_OK(destination.status()); | |
| ABSL_ASSERT_OK(writer_->Write(request, &source, &destination)); | |
| ASSERT_TRUE(source.Close()); | |
| ASSERT_TRUE(destination.Close()); | |
| // Verify that the output was successfully generated and contains the 10MB | |
| // media payload | |
| EXPECT_GT(result.size(), kJunkPayloadSize); | |
| } | |
| } // namespace | |
| } // namespace credentio |