blob: d3dc19a444a3d45a52b7c6350dd8fa317e153e62 [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/bmff/extractor.h"
#include <cstdint>
#include <limits>
#include <optional>
#include <string>
#include <vector>
#include "absl/log/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 "absl/strings/string_view.h"
#include "constants/labels.h"
#include "formats/asset_box.h"
#include "formats/asset_byte_info.h"
#include "formats/bmff/test_utils.h"
#include "formats/byte_range.h"
#include "formats/extractor_result.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "riegeli/bytes/string_reader.h"
#include "testing/jumbf_utils.h"
#include "testing/test_string_utils.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::StatusIs;
using ::credentio_testing::BigBox;
using ::credentio_testing::Box;
using ::credentio_testing::BoxFlags;
using ::credentio_testing::BoxVersion;
using ::credentio_testing::C2paBoxPayload;
using ::credentio_testing::ManifestBigBox;
using ::credentio_testing::ManifestBox;
using ::credentio_testing::Uint32Str;
using ::credentio_testing::Uint64Str;
using ::credentio_testing::UuidBoxPayload;
using ::testing::Eq;
using ::testing::HasSubstr;
constexpr int64_t kMaxChunkBytes = 10 * 1024 * 1024; // 2 MiB
struct TestCase {
std::string name;
std::string contents;
absl::StatusOr<ExtractorResult> result;
};
class ExtractorTest : public testing::TestWithParam<TestCase> {};
INSTANTIATE_TEST_SUITE_P(
ExtractorTests, ExtractorTest,
testing::ValuesIn({
TestCase{
.name = "EmptyContent",
.contents = "",
.result = absl::NotFoundError("No manifest store found"),
},
TestCase{
.name = "C2paBox",
.contents = absl::StrCat(Box("blah", "irrelevant content"),
ManifestBox(/*merkle_offset=*/0, "foo")),
.result = ExtractorResult{.manifest_store = "foo",
.asset_byte_info =
{.manifest_store_location =
ByteRange{.offset = 26,
.length = 48}}},
},
TestCase{
.name = "BigC2paBox",
.contents = absl::StrCat(Box("blah", "irrelevant content"),
ManifestBigBox(/*merkle_offset=*/0,
"foo")),
.result = ExtractorResult{.manifest_store = "foo",
.asset_byte_info =
{.manifest_store_location =
ByteRange{.offset = 26,
.length = 56}}},
},
TestCase{
.name = "C2paBoxTooLarge",
.contents = absl::StrCat(Box("blah", "irrelevant content"),
ManifestBox(/*merkle_offset=*/0,
std::string(kMaxChunkBytes + 1,
'Z'))),
.result = absl::NotFoundError("C2PA box is too large"),
},
TestCase{
.name = "NoC2paBox",
.contents = absl::StrCat(Box("blah", "irrelevant content"),
Box("asdf", "more irrelevant content"),
BigBox("zxcv", "whatever")),
.result = absl::NotFoundError("No manifest store found"),
},
TestCase{
.name = "C2paBoxAfterMoovIgnored",
.contents = absl::StrCat(Box("blah", "irrelevant content"),
Box("moov", "also irrelevant"),
Box("uuid", absl::StrCat(kC2paBmffBoxUuid,
"foo"))),
.result = absl::NotFoundError("No manifest store found"),
},
TestCase{
.name = "C2paBoxAfterMdatIgnored",
.contents = absl::StrCat(Box("blah", "irrelevant content"),
Box("mdat", "also irrelevant"),
Box("uuid", absl::StrCat(kC2paBmffBoxUuid,
"foo"))),
.result = absl::NotFoundError("No manifest store found"),
},
TestCase{
// Quoting
// https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_embedded_2:
// "If there are multiple C2PA Manifest Stores present in an asset,
// they shall all be considered as invalid and the validation should
// treat this as if no manifests were located."
.name = "MultipleC2paBoxesIgnored",
.contents = absl::StrCat(ManifestBox(/*merkle_offset=*/0, "foo"),
ManifestBox(/*merkle_offset=*/0, "bar")),
.result = absl::NotFoundError("Multiple manifest stores found"),
},
TestCase{
.name = "NonC2paUuidBoxIgnored",
.contents = absl::StrCat(Box("uuid",
"abcdefghijklmnop not a C2PA box"),
ManifestBox(/*merkle_offset=*/0, "foo")),
.result =
ExtractorResult{
.manifest_store = "foo",
.asset_byte_info = {.manifest_store_location =
ByteRange{
.offset = 39, .length = 48}}},
},
TestCase{
.name = "TruncatedBoxHeader",
.contents = "abcdefg",
.result = absl::NotFoundError("No manifest store found"),
},
TestCase{
.name = "TruncatedBoxPayload",
.contents =
[]() {
std::string contents = Box("asdf", "payload");
contents.resize(contents.size() - 1);
return contents;
}(),
.result = absl::NotFoundError("truncated BMFF box"),
},
TestCase{
.name = "TruncatedBoxPayloadHugeSize",
.contents = absl::StrCat(
Uint32Str(1), "asdf",
Uint64Str(std::numeric_limits<int64_t>::max()), "payload"),
.result = absl::NotFoundError("truncated BMFF box"),
},
TestCase{
.name = "ShortUuidBox",
.contents = Box("uuid", "foo"),
.result = absl::NotFoundError("kUnexpectedEof"),
},
TestCase{
.name = "InvalidBoxSize",
.contents = absl::StrCat(Uint32Str(2), "asdf", "payload"),
.result = absl::NotFoundError("kInvalidData"),
},
TestCase{
.name = "InvalidBigBoxSize",
.contents = absl::StrCat(Uint32Str(1), "asdf", Uint64Str(15),
"payload"),
.result = absl::NotFoundError("No manifest store found"),
},
TestCase{
.name = "InvalidVersion",
.contents = Box("uuid",
UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(1),
BoxFlags(0), "")),
.result = absl::NotFoundError("unsupported C2PA box version"),
},
TestCase{
.name = "InvalidFlags",
.contents = Box("uuid",
UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0),
BoxFlags(1), "")),
.result = absl::NotFoundError("unsupported C2PA box flags"),
},
TestCase{
.name = "NonManifestC2paBoxIgnored",
.contents = absl::StrCat(
Box("uuid", C2paBoxPayload("merkle", "ignored data")),
ManifestBox(/*merkle_offset=*/0, "foo")),
.result =
ExtractorResult{
.manifest_store = "foo",
.asset_byte_info = {.manifest_store_location =
ByteRange{
.offset = 47, .length = 48}}},
},
TestCase{
// Not enough bytes for the merkle offset.
.name = "TruncatedManifestBox",
.contents = Box("uuid", C2paBoxPayload("manifest", "asdfjkl")),
.result = absl::NotFoundError("kUnexpectedEof"),
},
}),
[](const testing::TestParamInfo<ExtractorTest::ParamType>& info) {
return info.param.name;
});
TEST_P(ExtractorTest, ExtractManifestStore) {
riegeli::StringReader<> input(GetParam().contents);
absl::StatusOr<std::string> result =
BmffExtractor().ExtractManifestStore(input);
absl::StatusOr<ExtractorResult> expected_result = GetParam().result;
if (expected_result.ok()) {
ASSERT_THAT(result, IsOk());
EXPECT_THAT(*result, Eq(expected_result->manifest_store));
} else {
EXPECT_THAT(result,
StatusIs(expected_result.status().code(),
HasSubstr(expected_result.status().message())));
}
}
TEST_P(ExtractorTest, ExtractWorksWhenPrefixPadded) {
std::string contents = "padding" + GetParam().contents;
riegeli::StringReader<> input(contents);
ASSERT_TRUE(input.Seek(7));
absl::StatusOr<std::string> result =
BmffExtractor().ExtractManifestStore(input);
absl::StatusOr<ExtractorResult> expected_result = GetParam().result;
if (expected_result.ok()) {
ASSERT_THAT(result, IsOk());
EXPECT_THAT(*result, Eq(expected_result->manifest_store));
} else {
EXPECT_THAT(result,
StatusIs(expected_result.status().code(),
HasSubstr(expected_result.status().message())));
}
}
TEST_P(ExtractorTest, ExtractManifestStoreLocation) {
riegeli::StringReader<> input(GetParam().contents);
absl::StatusOr<std::optional<ByteRange>> result =
BmffExtractor().ExtractManifestStoreLocation(input,
/*options=*/{});
absl::StatusOr<ExtractorResult> expected_result = GetParam().result;
if (expected_result.ok()) {
ASSERT_THAT(result, IsOk());
EXPECT_THAT(*result,
Eq(expected_result->asset_byte_info.manifest_store_location));
} else {
EXPECT_THAT(result,
StatusIs(expected_result.status().code(),
HasSubstr(expected_result.status().message())));
}
}
TEST_P(ExtractorTest, ExtractBoxes) {
riegeli::StringReader<> input(GetParam().contents);
absl::StatusOr<std::vector<AssetBox>> result =
BmffExtractor().ExtractBoxes(input,
/*options=*/{});
// BMFF does not have a concept of boxes, calling this method should always
// fail.
EXPECT_THAT(result, StatusIs(absl::StatusCode::kUnimplemented,
"BMFF does not have a concept of boxes"));
}
TEST(BmffExtractorTest, IsManifestStore) {
EXPECT_TRUE(BmffExtractor().MightBeC2paManifestStore(
CreateStartOfManifestStorePayload()));
}
} // namespace
} // namespace credentio