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