| // 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::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()) { | |
| ABSL_ASSERT_OK(result); | |
| 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()) { | |
| ABSL_ASSERT_OK(result); | |
| 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()) { | |
| ABSL_ASSERT_OK(result); | |
| 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 |