| // 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/base/types.h" | |
| #include "riegeli/bytes/string_reader.h" | |
| #include "testing/jumbf_utils.h" | |
| #include "testing/test_string_utils.h" | |
| namespace credentio { | |
| namespace { | |
| using ::absl_testing::IsOkAndHolds; | |
| using ::absl_testing::StatusIs; | |
| using ::credentio_testing::BadBox; | |
| using ::credentio_testing::BigBox; | |
| using ::credentio_testing::Box; | |
| using ::credentio_testing::BoxFlags; | |
| using ::credentio_testing::BoxVersion; | |
| using ::credentio_testing::C2paBoxPayload; | |
| using ::credentio_testing::C2paUuidBox; | |
| 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; | |
| using ::testing::Optional; | |
| 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"), | |
| }, | |
| // --- Manifest box placement. --- | |
| TestCase{ | |
| .name = "ManifestBetweenFtypAndMoov", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("foo"), | |
| Box("moov", "movie metadata"), Box("mdat", "media data")), | |
| .result = ExtractorResult{.manifest_store = "foo", | |
| .asset_byte_info = | |
| {.manifest_store_location = | |
| ByteRange{.offset = 16, | |
| .length = 48}}}, | |
| }, | |
| TestCase{ | |
| .name = "ManifestBetweenFtypAndMdat", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("foo"), | |
| Box("mdat", "media data"), Box("moov", "movie metadata")), | |
| .result = | |
| ExtractorResult{ | |
| .manifest_store = "foo", | |
| .asset_byte_info = {.manifest_store_location = | |
| ByteRange{ | |
| .offset = 16, .length = 48}}}, | |
| }, | |
| TestCase{ | |
| .name = "ManifestAfterFreeBeforeMoov", | |
| .contents = absl::StrCat(Box("ftyp", "isomisom"), | |
| Box("free", "padding"), C2paUuidBox("foo"), | |
| Box("moov", "movie metadata"), | |
| Box("mdat", "media data")), | |
| .result = | |
| ExtractorResult{ | |
| .manifest_store = "foo", | |
| .asset_byte_info = {.manifest_store_location = | |
| ByteRange{ | |
| .offset = 31, .length = 48}}}, | |
| }, | |
| TestCase{ | |
| .name = "ManifestWithoutFtyp", | |
| .contents = absl::StrCat(C2paUuidBox("foo"), | |
| Box("moov", "movie metadata"), | |
| Box("mdat", "media data")), | |
| .result = | |
| ExtractorResult{.manifest_store = "foo", | |
| .asset_byte_info = | |
| {.manifest_store_location = | |
| ByteRange{.offset = 0, .length = 48}}}, | |
| }, | |
| TestCase{ | |
| .name = "MerkleBoxAfterMdatIgnored", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("foo"), | |
| Box("moov", "movie metadata"), Box("mdat", "media data"), | |
| Box("uuid", C2paBoxPayload("merkle", "merkle data"))), | |
| .result = | |
| ExtractorResult{ | |
| .manifest_store = "foo", | |
| .asset_byte_info = {.manifest_store_location = | |
| ByteRange{ | |
| .offset = 16, .length = 48}}}, | |
| }, | |
| TestCase{ | |
| .name = "UnknownC2paPurposeAfterMdatIgnored", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("foo"), | |
| Box("mdat", "media data"), C2paUuidBox("bar", "unknown")), | |
| .result = | |
| ExtractorResult{ | |
| .manifest_store = "foo", | |
| .asset_byte_info = {.manifest_store_location = | |
| ByteRange{ | |
| .offset = 16, .length = 48}}}, | |
| }, | |
| TestCase{ | |
| .name = "NonC2paUuidBoxAfterMdatIgnored", | |
| .contents = absl::StrCat(Box("ftyp", "isomisom"), | |
| C2paUuidBox("foo"), | |
| Box("mdat", "media data"), | |
| Box("uuid", "abcdefghijklmnop data")), | |
| .result = | |
| ExtractorResult{ | |
| .manifest_store = "foo", | |
| .asset_byte_info = {.manifest_store_location = | |
| ByteRange{ | |
| .offset = 16, .length = 48}}}, | |
| }, | |
| TestCase{ | |
| .name = "ManifestBeforeFtyp", | |
| .contents = absl::StrCat( | |
| C2paUuidBox("foo"), Box("ftyp", "isomisom"), | |
| Box("moov", "movie metadata"), Box("mdat", "media data")), | |
| .result = absl::NotFoundError("manifest resides before ftyp"), | |
| }, | |
| TestCase{ | |
| .name = "ManifestAfterMoov", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), Box("moov", "movie metadata"), | |
| C2paUuidBox("foo"), Box("mdat", "media data")), | |
| .result = absl::NotFoundError("manifest resides after moov"), | |
| }, | |
| TestCase{ | |
| .name = "ManifestAfterMdat", | |
| .contents = absl::StrCat(Box("ftyp", "isomisom"), | |
| Box("mdat", "media data"), | |
| C2paUuidBox("foo")), | |
| .result = absl::NotFoundError("manifest resides after mdat"), | |
| }, | |
| TestCase{ | |
| .name = "ManifestBetweenMdatAndMoov", | |
| .contents = | |
| absl::StrCat(Box("ftyp", "isomisom"), Box("mdat", "media data"), | |
| C2paUuidBox("foo"), Box("moov", "movie metadata")), | |
| .result = absl::NotFoundError("manifest resides after mdat"), | |
| }, | |
| TestCase{ | |
| .name = "ManifestAfterMoovWithoutFtyp", | |
| .contents = absl::StrCat(Box("moov", "movie metadata"), | |
| C2paUuidBox("foo")), | |
| .result = absl::NotFoundError("manifest resides after moov"), | |
| }, | |
| // --- Whole-file scanning. --- | |
| TestCase{ | |
| .name = "SecondManifestAfterMdat", | |
| .contents = | |
| absl::StrCat(Box("ftyp", "isomisom"), C2paUuidBox("foo"), | |
| Box("mdat", "media data"), C2paUuidBox("bar")), | |
| .result = absl::NotFoundError("multiple manifest boxes found"), | |
| }, | |
| TestCase{ | |
| .name = "TruncatedBoxAfterMdat", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("foo"), | |
| Box("mdat", "media data"), BadBox("free", "padding", 16)), | |
| .result = absl::NotFoundError("truncated BMFF box"), | |
| }, | |
| TestCase{ | |
| .name = "MalformedC2paBoxAfterMoov", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("foo"), | |
| Box("moov", "movie metadata"), | |
| Box("uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(1), | |
| BoxFlags(0), ""))), | |
| .result = absl::NotFoundError("unsupported C2PA box version"), | |
| }, | |
| // --- Manifest payload bounds. --- | |
| TestCase{ | |
| .name = "EmptyManifestPayload", | |
| .contents = absl::StrCat(Box("ftyp", "isomisom"), C2paUuidBox(""), | |
| Box("mdat", "media data")), | |
| .result = absl::NotFoundError("manifest store is empty"), | |
| }, | |
| // Regression: a manifest box too small for its merkle offset followed | |
| // by another box used to underflow the payload size. | |
| TestCase{ | |
| .name = "ManifestTooSmallForMerkleOffsetFollowedByBox", | |
| .contents = absl::StrCat(Box("uuid", | |
| C2paBoxPayload("manifest", "abc")), | |
| Box("mdat", "media data")), | |
| .result = absl::NotFoundError("kUnexpectedEof"), | |
| }, | |
| // --- Manifest vs. original/update coexistence. --- | |
| TestCase{ | |
| .name = "ManifestAndOriginalCoexist", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("foo"), | |
| C2paUuidBox("bar", "original"), Box("mdat", "media data")), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (original)"), | |
| }, | |
| TestCase{ | |
| .name = "ManifestAndUpdateCoexist", | |
| .contents = | |
| absl::StrCat(Box("ftyp", "isomisom"), C2paUuidBox("foo"), | |
| Box("mdat", "media data"), | |
| Box("uuid", C2paBoxPayload("update", "bar"))), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (update)"), | |
| }, | |
| TestCase{ | |
| .name = "ManifestOriginalAndUpdateCoexist", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("foo", "original"), | |
| C2paUuidBox("bar"), Box("mdat", "media data"), | |
| Box("uuid", C2paBoxPayload("update", "baz"))), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (original)"), | |
| }, | |
| // --- Original + update boxes. --- | |
| TestCase{ | |
| .name = "OriginalAndUpdateUnsupported", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("orig", "original"), | |
| Box("moov", "movie metadata"), Box("mdat", "media data"), | |
| Box("uuid", C2paBoxPayload("update", "upd"))), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (original)"), | |
| }, | |
| TestCase{ | |
| .name = "OriginalDirectlyFollowedByUpdateUnsupported", | |
| .contents = absl::StrCat(C2paUuidBox("orig", "original"), | |
| Box("uuid", | |
| C2paBoxPayload("update", "upd"))), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (original)"), | |
| }, | |
| TestCase{ | |
| .name = "OriginalAndBigBoxUpdateUnsupported", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("orig", "original"), | |
| Box("mdat", "media data"), | |
| BigBox("uuid", C2paBoxPayload("update", "upd"))), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (original)"), | |
| }, | |
| TestCase{ | |
| .name = "OriginalWithoutUpdate", | |
| .contents = absl::StrCat(Box("ftyp", "isomisom"), | |
| C2paUuidBox("orig", "original"), | |
| Box("mdat", "media data")), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (original)"), | |
| }, | |
| TestCase{ | |
| .name = "UpdateWithoutOriginal", | |
| .contents = | |
| absl::StrCat(Box("ftyp", "isomisom"), Box("mdat", "media data"), | |
| Box("uuid", C2paBoxPayload("update", "upd"))), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (update)"), | |
| }, | |
| TestCase{ | |
| .name = "UpdateNotLastBox", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("orig", "original"), | |
| Box("mdat", "media data"), | |
| Box("uuid", C2paBoxPayload("update", "upd")), | |
| Box("free", "padding")), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (original)"), | |
| }, | |
| TestCase{ | |
| .name = "OriginalBeforeFtyp", | |
| .contents = | |
| absl::StrCat(C2paUuidBox("orig", "original"), | |
| Box("ftyp", "isomisom"), Box("mdat", "media data"), | |
| Box("uuid", C2paBoxPayload("update", "upd"))), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (original)"), | |
| }, | |
| TestCase{ | |
| .name = "OriginalAfterMoov", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), Box("moov", "movie metadata"), | |
| Box("free", "padding"), C2paUuidBox("orig", "original"), | |
| Box("uuid", C2paBoxPayload("update", "upd"))), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (original)"), | |
| }, | |
| TestCase{ | |
| .name = "OriginalAfterMdat", | |
| .contents = | |
| absl::StrCat(Box("ftyp", "isomisom"), Box("mdat", "media data"), | |
| C2paUuidBox("orig", "original"), | |
| Box("uuid", C2paBoxPayload("update", "upd"))), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (original)"), | |
| }, | |
| TestCase{ | |
| .name = "MultipleOriginalBoxes", | |
| .contents = absl::StrCat( | |
| C2paUuidBox("a", "original"), C2paUuidBox("b", "original"), | |
| Box("uuid", C2paBoxPayload("update", "upd"))), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (original)"), | |
| }, | |
| TestCase{ | |
| .name = "MultipleUpdateBoxes", | |
| .contents = absl::StrCat(C2paUuidBox("orig", "original"), | |
| Box("uuid", C2paBoxPayload("update", "a")), | |
| Box("uuid", | |
| C2paBoxPayload("update", "b"))), | |
| .result = absl::NotFoundError( | |
| "unsupported C2PA metadata purpose (original)"), | |
| }, | |
| 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 boxes 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())); | |
| } | |
| TEST(BmffExtractorTest, EndOffsetStopsScanningBeforeTrailingData) { | |
| const std::string media = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("foo"), Box("mdat", "media data")); | |
| // Data past `end_offset` would otherwise fail extraction. | |
| const std::string contents = | |
| absl::StrCat(media, BadBox("free", "padding", 16), C2paUuidBox("bar")); | |
| riegeli::StringReader<> input(contents); | |
| EXPECT_THAT( | |
| BmffExtractor().ExtractManifestStoreLocation( | |
| input, {.end_offset = static_cast<int64_t>(media.size())}), | |
| IsOkAndHolds(Optional(Eq(ByteRange{.offset = 16, .length = 48})))); | |
| } | |
| TEST(BmffExtractorTest, EndOffsetWithoutLimitScansWholeFile) { | |
| const std::string contents = | |
| absl::StrCat(Box("ftyp", "isomisom"), C2paUuidBox("foo"), | |
| Box("mdat", "media data"), BadBox("free", "padding", 16)); | |
| riegeli::StringReader<> input(contents); | |
| EXPECT_THAT( | |
| BmffExtractor().ExtractManifestStoreLocation(input, {}), | |
| StatusIs(absl::StatusCode::kNotFound, HasSubstr("truncated BMFF box"))); | |
| } | |
| TEST(BmffExtractorTest, EndOffsetInsideBoxFails) { | |
| const std::string contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("foo"), Box("mdat", "media data")); | |
| riegeli::StringReader<> input(contents); | |
| EXPECT_THAT( | |
| BmffExtractor().ExtractManifestStoreLocation( | |
| input, {.end_offset = static_cast<int64_t>(contents.size() - 1)}), | |
| StatusIs(absl::StatusCode::kNotFound, HasSubstr("truncated BMFF box"))); | |
| } | |
| TEST(BmffExtractorTest, UpdateBoxEndingAtEndOffsetIsLast) { | |
| const std::string media = | |
| absl::StrCat(C2paUuidBox("orig", "original"), Box("mdat", "media data"), | |
| Box("uuid", C2paBoxPayload("update", "upd"))); | |
| const std::string contents = absl::StrCat(media, Box("free", "padding")); | |
| riegeli::StringReader<> input(contents); | |
| EXPECT_THAT(BmffExtractor().ExtractManifestStoreLocation( | |
| input, {.end_offset = static_cast<int64_t>(media.size())}), | |
| StatusIs(absl::StatusCode::kNotFound, | |
| HasSubstr("No manifest store found"))); | |
| } | |
| TEST(BmffExtractorTest, LocationReturnsNulloptWhenC2paNotRequired) { | |
| const std::string contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), Box("mdat", "media data"), C2paUuidBox("foo")); | |
| riegeli::StringReader<> input(contents); | |
| EXPECT_THAT(BmffExtractor().ExtractManifestStoreLocation( | |
| input, {.requires_c2pa = false}), | |
| IsOkAndHolds(Eq(std::nullopt))); | |
| } | |
| class NoSizeStringReader : public riegeli::StringReader<> { | |
| public: | |
| using riegeli::StringReader<>::StringReader; | |
| bool SupportsSize() override { return false; } | |
| }; | |
| class SizeNotValuedStringReader : public riegeli::StringReader<> { | |
| public: | |
| using riegeli::StringReader<>::StringReader; | |
| bool SupportsSize() override { return true; } | |
| protected: | |
| std::optional<riegeli::Position> SizeImpl() override { return std::nullopt; } | |
| }; | |
| TEST(BmffExtractorTest, ReaderWithoutSizeFails) { | |
| const std::string contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("foo"), Box("mdat", "media data")); | |
| NoSizeStringReader input(contents); | |
| EXPECT_THAT(BmffExtractor().ExtractManifestStore(input), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("reader size cannot be determined"))); | |
| } | |
| TEST(BmffExtractorTest, ReaderWithUnknownSizeFails) { | |
| const std::string contents = absl::StrCat( | |
| Box("ftyp", "isomisom"), C2paUuidBox("foo"), Box("mdat", "media data")); | |
| SizeNotValuedStringReader input(contents); | |
| EXPECT_THAT(BmffExtractor().ExtractManifestStore(input), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("reader size cannot be determined"))); | |
| } | |
| } // namespace | |
| } // namespace credentio |