| // 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 |