| // 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/box_matcher.h" | |
| #include <string> | |
| #include <vector> | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "constants/labels.h" | |
| #include "formats/bmff/box_header.h" | |
| #include "formats/bmff/test_utils.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "proto/bmff_based_hash_assertion.pb.h" | |
| #include "riegeli/bytes/string_reader.h" | |
| namespace credentio { | |
| namespace { | |
| using ::credentio_testing::BigBox; | |
| using ::credentio_testing::Box; | |
| using ::credentio_testing::BoxFlags; | |
| using ::credentio_testing::BoxVersion; | |
| using ::credentio_testing::UuidBoxPayload; | |
| using ::testing::ElementsAre; | |
| using ::testing::IsEmpty; | |
| constexpr absl::string_view kFlagsForZero("\x00\x00\x00", 3); | |
| constexpr absl::string_view kFlagsForOne("\x00\x00\x01", 3); | |
| constexpr absl::string_view kFlagsForTwo("\x00\x00\x02", 3); | |
| std::vector<std::string> MatchBmffBoxes( | |
| std::vector<credentio::BmffBoxHeader> boxes, | |
| const credentio::BMFFBoxMatcher* matcher) { | |
| std::vector<std::string> matched; | |
| for (const auto& box : boxes) { | |
| if (matcher->Matches(box)) { | |
| matched.push_back(box.xpath); | |
| } | |
| } | |
| return matched; | |
| } | |
| std::vector<credentio::BMFFBoxMatcher::Range> MatchBmffBoxRanges( | |
| std::vector<credentio::BmffBoxHeader> boxes, | |
| const credentio::BMFFBoxMatcher* matcher) { | |
| std::vector<credentio::BMFFBoxMatcher::Range> matched; | |
| for (const auto& box : boxes) { | |
| if (!matcher->Matches(box)) { | |
| continue; | |
| } | |
| auto ranges = matcher->MatchedRanges(box); | |
| matched.insert(matched.end(), ranges.begin(), ranges.end()); | |
| } | |
| return matched; | |
| } | |
| TEST(BoxMatcher, MatchXPath) { | |
| std::string uuid = Box( | |
| "uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), BoxFlags(0), "")); | |
| std::string moov = Box("moov", "moov"); | |
| std::string trak_trak1_trak1 = Box("trak", "abcd"); | |
| std::string trak_trak1 = Box("trak", trak_trak1_trak1); | |
| std::string trak_trak2 = BigBox("trak", "efgh"); | |
| std::string trak1 = | |
| BigBox("trak", absl::StrCat(trak_trak1, moov, trak_trak2)); | |
| std::string trak2 = Box("trak", "ijkl"); | |
| std::string contents = absl::StrCat(uuid, trak1, trak2); | |
| riegeli::StringReader<> input(contents); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak[1]/trak[1]/trak"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), | |
| ElementsAre("/trak[1]/trak[1]/trak[1]")); | |
| } | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak[1]/trak/trak"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), | |
| ElementsAre("/trak[1]/trak[1]/trak[1]")); | |
| } | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak/trak/trak"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), | |
| ElementsAre("/trak[1]/trak[1]/trak[1]")); | |
| } | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak/trak/trak[1]"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), | |
| ElementsAre("/trak[1]/trak[1]/trak[1]")); | |
| } | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak/trak/abcd"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), IsEmpty()); | |
| } | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), | |
| ElementsAre("/trak[1]", "/trak[2]")); | |
| } | |
| } | |
| TEST(BoxMatcher, MatchLength) { | |
| std::string uuid = Box( | |
| "uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), BoxFlags(0), "")); | |
| std::string moov = Box("moov", "moov"); | |
| std::string trak_trak1_trak1 = Box("trak", "abcd"); | |
| std::string trak_trak1 = Box("trak", trak_trak1_trak1); | |
| std::string trak_trak2 = BigBox("trak", "efgh"); | |
| std::string trak1 = | |
| BigBox("trak", absl::StrCat(trak_trak1, moov, trak_trak2)); | |
| std::string trak2 = Box("trak", "ijkl"); | |
| std::string contents = absl::StrCat(uuid, trak1, trak2); | |
| riegeli::StringReader<> input(contents); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak"); | |
| range.set_length(68); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), ElementsAre("/trak[1]")); | |
| } | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak"); | |
| range.set_length(12); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), ElementsAre("/trak[2]")); | |
| } | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), | |
| ElementsAre("/trak[1]", "/trak[2]")); | |
| } | |
| } | |
| TEST(BoxMatcher, MatchVersion) { | |
| std::string uuid = Box( | |
| "uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(2), BoxFlags(0), "")); | |
| riegeli::StringReader<> input(uuid); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/uuid"); | |
| range.set_version(2); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), ElementsAre("/uuid[1]")); | |
| } | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/uuid"); | |
| range.set_version(1); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), IsEmpty()); | |
| } | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/uuid"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), ElementsAre("/uuid[1]")); | |
| } | |
| } | |
| TEST(BoxMatcher, MatchFlagsDefaultExactMatch) { | |
| std::string uuid = Box("uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), | |
| kFlagsForOne, "")); | |
| riegeli::StringReader<> input(uuid); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| credentio::BmffRange range; | |
| range.set_xpath("/uuid"); | |
| range.set_flags(kFlagsForOne); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), ElementsAre("/uuid[1]")); | |
| }; | |
| TEST(BoxMatcher, MatchFlagsExplicitlyExactMatch) { | |
| std::string uuid = Box("uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), | |
| kFlagsForOne, "")); | |
| riegeli::StringReader<> input(uuid); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| credentio::BmffRange range; | |
| range.set_xpath("/uuid"); | |
| range.set_flags(kFlagsForOne); | |
| range.set_exact(true); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), ElementsAre("/uuid[1]")); | |
| }; | |
| TEST(BoxMatcher, MatchFlagsNotMatched) { | |
| std::string uuid = Box("uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), | |
| kFlagsForOne, "")); | |
| riegeli::StringReader<> input(uuid); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| credentio::BmffRange range; | |
| range.set_xpath("/uuid"); | |
| range.set_flags(kFlagsForZero); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), IsEmpty()); | |
| } | |
| TEST(BoxMatcher, MatchFlagsNotExactButMatched) { | |
| credentio::BmffRange range; | |
| range.set_xpath("/uuid"); | |
| range.set_flags(kFlagsForZero); | |
| range.set_exact(false); | |
| { | |
| std::string uuid = | |
| Box("uuid", | |
| UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), kFlagsForOne, "")); | |
| riegeli::StringReader<> input(uuid); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), ElementsAre("/uuid[1]")); | |
| } | |
| { | |
| std::string uuid = | |
| Box("uuid", | |
| UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), kFlagsForTwo, "")); | |
| riegeli::StringReader<> input(uuid); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), ElementsAre("/uuid[1]")); | |
| } | |
| }; | |
| TEST(BoxMatcher, MatchFlagsNotExactNotMatched) { | |
| std::string uuid = Box("uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), | |
| kFlagsForOne, "")); | |
| riegeli::StringReader<> input(uuid); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| credentio::BmffRange range; | |
| range.set_xpath("/uuid"); | |
| range.set_flags(kFlagsForTwo); | |
| range.set_exact(false); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), IsEmpty()); | |
| }; | |
| TEST(BoxMatcher, MatchFlagsNotFullBoxNotMatched) { | |
| std::string some_box = Box("some", "abcd"); | |
| riegeli::StringReader<> input(some_box); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| credentio::BmffRange range; | |
| range.set_xpath("/some"); | |
| range.set_flags(kFlagsForZero); | |
| range.set_exact(false); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), IsEmpty()); | |
| }; | |
| TEST(BoxMatcher, NoFlagsMatched) { | |
| { | |
| std::string uuid = | |
| Box("uuid", | |
| UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), kFlagsForOne, "")); | |
| riegeli::StringReader<> input(uuid); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| credentio::BmffRange range; | |
| range.set_xpath("/uuid"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), ElementsAre("/uuid[1]")); | |
| }; | |
| } | |
| TEST(BoxMatcher, MatchData) { | |
| std::string uuid = Box("uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), | |
| BoxFlags(0), "abcd")); | |
| riegeli::StringReader<> input(uuid); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/uuid"); | |
| auto data1 = range.add_data(); | |
| data1->set_offset(4); | |
| data1->set_value("uuid"); | |
| auto data2 = range.add_data(); | |
| data2->set_offset(28); | |
| data2->set_value("abcd"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), ElementsAre("/uuid[1]")); | |
| } | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/uuid"); | |
| auto data1 = range.add_data(); | |
| data1->set_offset(4); | |
| data1->set_value("aaaa"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), IsEmpty()); | |
| } | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/uuid"); | |
| auto data1 = range.add_data(); | |
| data1->set_offset(10000); | |
| data1->set_value("uuid"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), IsEmpty()); | |
| } | |
| } | |
| TEST(BoxMatcher, MismatchOneField) { | |
| std::string uuid = Box("uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(1), | |
| kFlagsForOne, "abcd")); | |
| riegeli::StringReader<> input(uuid); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| credentio::BmffRange matched_range; | |
| matched_range.set_xpath("/uuid"); | |
| matched_range.set_length(32); | |
| matched_range.set_version(1); | |
| matched_range.set_flags(kFlagsForOne); | |
| auto data1 = matched_range.add_data(); | |
| data1->set_offset(4); | |
| data1->set_value("uuid"); | |
| auto data2 = matched_range.add_data(); | |
| data2->set_offset(28); | |
| data2->set_value("abcd"); | |
| credentio::BMFFBoxMatcher matcher(matched_range, &input); | |
| ASSERT_THAT(MatchBmffBoxes(boxes, &matcher), ElementsAre("/uuid[1]")); | |
| { | |
| credentio::BmffRange range = matched_range; | |
| range.set_xpath("/uuid[1]"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), ElementsAre("/uuid[1]")); | |
| } | |
| { | |
| credentio::BmffRange range = matched_range; | |
| range.set_length(31); // length not matched. | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), IsEmpty()); | |
| } | |
| { | |
| credentio::BmffRange range = matched_range; | |
| range.set_version(2); // version not matched. | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), IsEmpty()); | |
| } | |
| { | |
| credentio::BmffRange range = matched_range; | |
| range.set_length(1); // length not matched. | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), IsEmpty()); | |
| } | |
| { | |
| credentio::BmffRange range = matched_range; | |
| range.set_flags(kFlagsForTwo); // flags not matched. | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), IsEmpty()); | |
| } | |
| { | |
| credentio::BmffRange range = matched_range; | |
| range.mutable_data(1)->set_value("abce"); // data not matched. | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxes(boxes, &matcher), IsEmpty()); | |
| } | |
| } | |
| TEST(BoxMatcher, MatchedRanges) { | |
| std::string uuid = Box( | |
| "uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), BoxFlags(0), "")); | |
| std::string moov = Box("moov", "moov"); // box_size = 12 | |
| std::string trak1 = Box("trak", moov); // box_size = 20 | |
| std::string trak2 = BigBox("trak", "ijkl"); // box_size = 20 | |
| std::string contents = absl::StrCat(uuid, trak1, trak2); | |
| riegeli::StringReader<> input(contents); | |
| auto boxes_or = ReadBmffBoxHeaders(input); | |
| ASSERT_TRUE(boxes_or.ok()); | |
| auto boxes = *boxes_or; | |
| // Single box matched. | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak[1]"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxRanges(boxes, &matcher), | |
| ElementsAre(credentio::BMFFBoxMatcher::Range{.offset = 28, | |
| .length = 20})); | |
| } | |
| // Multiple boxes matched. | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak"); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT( | |
| MatchBmffBoxRanges(boxes, &matcher), | |
| ElementsAre( | |
| credentio::BMFFBoxMatcher::Range{.offset = 28, .length = 20}, | |
| credentio::BMFFBoxMatcher::Range{.offset = 48, .length = 20})); | |
| } | |
| // Return subsets of the box. | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak[1]"); | |
| auto subset = range.add_subsets(); | |
| subset->set_offset(12); | |
| subset->set_length(1); | |
| subset = range.add_subsets(); | |
| subset->set_offset(16); | |
| subset->set_length(2); | |
| subset = range.add_subsets(); | |
| subset->set_offset(18); | |
| subset->set_length(0); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT( | |
| MatchBmffBoxRanges(boxes, &matcher), | |
| ElementsAre( | |
| credentio::BMFFBoxMatcher::Range{.offset = 40, .length = 1}, | |
| credentio::BMFFBoxMatcher::Range{.offset = 44, .length = 2}, | |
| credentio::BMFFBoxMatcher::Range{.offset = 46, .length = 2})); | |
| } | |
| // The last entry may have a length of zero; this indicates that the remainder | |
| // of the box from that relative byte offset onward is excluded. | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak[1]"); | |
| auto subset = range.add_subsets(); | |
| subset->set_offset(18); | |
| subset->set_length(0); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxRanges(boxes, &matcher), | |
| ElementsAre(credentio::BMFFBoxMatcher::Range{.offset = 46, | |
| .length = 2})); | |
| } | |
| // Relative byte offset plus length that exceeds the length of the box is | |
| // allowed; bytes beyond the end of the box are never hashed. | |
| { | |
| credentio::BmffRange range; | |
| range.set_xpath("/trak[1]"); | |
| auto subset = range.add_subsets(); | |
| subset->set_offset(18); | |
| subset->set_length(100); | |
| credentio::BMFFBoxMatcher matcher(range, &input); | |
| EXPECT_THAT(MatchBmffBoxRanges(boxes, &matcher), | |
| ElementsAre(credentio::BMFFBoxMatcher::Range{.offset = 46, | |
| .length = 2})); | |
| } | |
| } | |
| } // namespace | |
| } // namespace credentio |