blob: 3b94c247ae5f9f5a2a0c385bfc1bda6820972d4e [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 "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