| // 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_header.h" | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <string> | |
| #include <vector> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.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/bmff/constants.h" | |
| #include "formats/bmff/test_utils.h" | |
| #include "formats/byte_range.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "riegeli/bytes/string_reader.h" | |
| #include "riegeli/bytes/string_writer.h" | |
| #include "riegeli/bytes/writer.h" | |
| namespace credentio { | |
| namespace { | |
| using ::absl_testing::IsOk; | |
| 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::C2paUuidBox; | |
| using ::credentio_testing::UuidBoxPayload; | |
| using ::testing::AnyOf; | |
| using ::testing::ElementsAre; | |
| using ::testing::Eq; | |
| using ::testing::HasSubstr; | |
| using ::testing::IsEmpty; | |
| constexpr absl::string_view kZeroFlags("\x00\x00\x00", 3); | |
| absl::StatusOr<std::vector<BmffBoxHeader>> ReadAllBmffBoxHeaders( | |
| absl::string_view contents) { | |
| riegeli::StringReader<> reader(contents); | |
| return ReadBmffBoxHeaders(reader); | |
| } | |
| class BoxHeaderTest : public testing::Test { | |
| public: | |
| absl::StatusOr<int64_t> WriteBox(const BmffBox& box, absl::string_view data, | |
| std::string& destination, | |
| bool start_with_error = false) { | |
| riegeli::StringWriter<> writer(&destination); | |
| if (start_with_error) { | |
| writer.Fail(absl::InternalError("failed to write box header")); | |
| } | |
| ABSL_ASSIGN_OR_RETURN(int64_t written, WriteBmffBoxHeader(box, writer)); | |
| if (!writer.Write(data)) { | |
| return writer.status(); | |
| } | |
| if (!writer.Close()) { | |
| return writer.status(); | |
| } | |
| return written; | |
| } | |
| absl::Status ReadBox(absl::string_view data, BmffBoxHeader& header) { | |
| riegeli::StringReader<> reader(data); | |
| ABSL_ASSIGN_OR_RETURN(header, ReadBmffBoxHeader(reader)); | |
| return absl::OkStatus(); | |
| } | |
| }; | |
| TEST_F(BoxHeaderTest, ReadAllBoxesWithEmptyContent) { | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(""), IsOkAndHolds(IsEmpty())); | |
| } | |
| TEST_F(BoxHeaderTest, ReadAllBoxesUUIDBox) { | |
| std::string uuid_box = Box( | |
| "uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(1), BoxFlags(2), "")); | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(uuid_box), | |
| IsOkAndHolds(ElementsAre(BmffBoxHeader{ | |
| .start = 0, | |
| .box_size = 28, // header_size + 4 (version+flags) | |
| .header_size = 24, // 4 (size) + 4 (type) + 16 (UUID) | |
| .version_and_flags_size = 4, | |
| .type = "uuid", | |
| .xpath = "/uuid[1]", | |
| .user_type = std::string(kC2paBmffBoxUuid), | |
| .version = 1, | |
| .flags = BoxFlags(2)}))); | |
| } | |
| TEST_F(BoxHeaderTest, ReadAllBoxesFullBox) { | |
| std::string uuid_box = | |
| Box("pdin", absl::StrCat(BoxVersion(0), BoxFlags(0), "")); | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(uuid_box), | |
| IsOkAndHolds(ElementsAre(BmffBoxHeader{ | |
| .start = 0, | |
| .box_size = 12, // header_size + 4 (version+flags) | |
| .header_size = 8, // 4 (size) + 4 (type) | |
| .version_and_flags_size = 4, | |
| .type = "pdin", | |
| .xpath = "/pdin[1]", | |
| .user_type = "", | |
| .version = 0, | |
| .flags = std::string(kZeroFlags)}))); | |
| } | |
| TEST_F(BoxHeaderTest, ReadAllBoxesTwoBoxes) { | |
| std::string uuid_box = Box( | |
| "uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), BoxFlags(0), "")); | |
| std::string other_box = Box("blah", "abcd"); | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(absl::StrCat(uuid_box, other_box)), | |
| IsOkAndHolds(ElementsAre( | |
| BmffBoxHeader{.start = 0, | |
| .box_size = 28, | |
| .header_size = 24, | |
| .version_and_flags_size = 4, | |
| .type = "uuid", | |
| .xpath = "/uuid[1]", | |
| .user_type = std::string(kC2paBmffBoxUuid), | |
| .version = 0, | |
| .flags = std::string(kZeroFlags)}, | |
| BmffBoxHeader{.start = 28, | |
| .box_size = 12, | |
| .header_size = 8, | |
| .type = "blah", | |
| .xpath = "/blah[1]"}))); | |
| } | |
| TEST_F(BoxHeaderTest, ReadAllBoxesWithSubBoxes) { | |
| std::string uuid = Box( | |
| "uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), BoxFlags(0), "")); | |
| std::string trak_trak1_trak1 = Box("trak", "abcd"); // box_size = 4+4+4; | |
| std::string trak_trak1 = Box("trak", trak_trak1_trak1); // box_size = 4+4+12; | |
| std::string trak_trak2 = BigBox("trak", "efgh"); // box_size = (4+8)+4+4; | |
| std::string moov = Box("moov", "moov"); // box_size = 4+4+4; | |
| std::string trak1 = BigBox( | |
| "trak", absl::StrCat(trak_trak1, moov, | |
| trak_trak2)); // box_size = (4+8)+4+(20+20+12); | |
| std::string trak2 = Box("trak", "ijkl"); // box_size = 4+4+4; | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(absl::StrCat(uuid, trak1, trak2)), | |
| IsOkAndHolds(ElementsAre( | |
| BmffBoxHeader{.start = 0, | |
| .box_size = 28, | |
| .header_size = 24, | |
| .version_and_flags_size = 4, | |
| .type = "uuid", | |
| .xpath = "/uuid[1]", | |
| .user_type = std::string(kC2paBmffBoxUuid), | |
| .version = 0, | |
| .flags = std::string(kZeroFlags)}, | |
| BmffBoxHeader{.start = 28, | |
| .box_size = 68, | |
| .header_size = 16, | |
| .type = "trak", | |
| .xpath = "/trak[1]"}, | |
| BmffBoxHeader{.start = 44, | |
| .box_size = 20, | |
| .header_size = 8, | |
| .type = "trak", | |
| .xpath = "/trak[1]/trak[1]"}, | |
| BmffBoxHeader{.start = 52, | |
| .box_size = 12, | |
| .header_size = 8, | |
| .type = "trak", | |
| .xpath = "/trak[1]/trak[1]/trak[1]"}, | |
| BmffBoxHeader{.start = 64, | |
| .box_size = 12, | |
| .header_size = 8, | |
| .type = "moov", | |
| .xpath = "/trak[1]/moov[1]"}, | |
| BmffBoxHeader{.start = 76, | |
| .box_size = 20, | |
| .header_size = 16, | |
| .type = "trak", | |
| .xpath = "/trak[1]/trak[2]"}, | |
| BmffBoxHeader{.start = 96, | |
| .box_size = 12, | |
| .header_size = 8, | |
| .type = "trak", | |
| .xpath = "/trak[2]"}))); | |
| } | |
| TEST_F(BoxHeaderTest, ReadAllBoxesBoxExtendsBeyondParent) { | |
| std::string input = Box("moov", BadBox("trak", "ijkl", /*data_size=*/100)); | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(input), | |
| StatusIs(absl::StatusCode::kDataLoss, | |
| HasSubstr("sub-box extends beyond the parent"))); | |
| } | |
| TEST_F(BoxHeaderTest, ReadAllBoxesBoxExtendsBeyondInput) { | |
| std::string input = | |
| absl::StrCat(BadBox("trak", "ijkl", /*data_size=*/100), Box("moov", "")); | |
| EXPECT_THAT( | |
| ReadAllBmffBoxHeaders(input), | |
| StatusIs(absl::StatusCode::kDataLoss, HasSubstr("truncated BMFF box"))); | |
| } | |
| TEST_F(BoxHeaderTest, ReadAllBoxesMissing64bitSize) { | |
| std::string moov = | |
| BadBox("moov", "ijkl", | |
| /*data_size=*/1); // box size set to 1, indicating a 64-bit size, | |
| // but does not have a 64-bit size. | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(moov), | |
| StatusIs(absl::StatusCode::kDataLoss, | |
| AnyOf(HasSubstr("kUnexpectedEof"), | |
| HasSubstr("failed to read box size")))); | |
| } | |
| TEST_F(BoxHeaderTest, ReadAllBoxesBoxDataSizeTooSmall) { | |
| std::string moov = | |
| BadBox("moov", "ijkl", /*data_size=*/2); // data size must be at least 8. | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(moov), | |
| StatusIs(absl::StatusCode::kDataLoss, | |
| AnyOf(HasSubstr("kInvalidData"), | |
| HasSubstr("unsupported box size")))); | |
| } | |
| TEST_F(BoxHeaderTest, ReadsQuickTimeMetaBox) { | |
| std::string data = Box( | |
| "meta", Box("hdlr", absl::StrCat(BoxVersion(0), BoxFlags(0), "abcd"))); | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(data), IsOkAndHolds(ElementsAre( | |
| BmffBoxHeader{ | |
| .start = 0, | |
| .box_size = 24, | |
| .header_size = 8, | |
| .version_and_flags_size = 0, | |
| .type = "meta", | |
| .xpath = "/meta[1]", | |
| .user_type = "", | |
| .version = 0, | |
| .flags = "", | |
| }, | |
| BmffBoxHeader{ | |
| .start = 8, | |
| .box_size = 16, | |
| .header_size = 8, | |
| .version_and_flags_size = 4, | |
| .type = "hdlr", | |
| .xpath = "/meta[1]/hdlr[1]", | |
| .user_type = "", | |
| .version = 0, | |
| .flags = BoxFlags(0), | |
| }))); | |
| } | |
| TEST_F(BoxHeaderTest, ReadsIsoMetaBox) { | |
| std::string data = | |
| absl::StrCat(Box("meta", absl::StrCat(BoxVersion(0), BoxFlags(0))), | |
| Box("free", "padding")); | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(data), IsOkAndHolds(ElementsAre( | |
| BmffBoxHeader{ | |
| .start = 0, | |
| .box_size = 12, | |
| .header_size = 8, | |
| .version_and_flags_size = 4, | |
| .type = "meta", | |
| .xpath = "/meta[1]", | |
| .user_type = "", | |
| .version = 0, | |
| .flags = BoxFlags(0), | |
| }, | |
| BmffBoxHeader{ | |
| .start = 12, | |
| .box_size = 15, | |
| .header_size = 8, | |
| .version_and_flags_size = 0, | |
| .type = "free", | |
| .xpath = "/free[1]", | |
| .user_type = "", | |
| .version = 0, | |
| .flags = "", | |
| }))); | |
| } | |
| TEST_F(BoxHeaderTest, StopsEarly) { | |
| std::string uuid_box = Box( | |
| "uuid", UuidBoxPayload(kC2paBmffBoxUuid, BoxVersion(0), BoxFlags(0), "")); | |
| std::string other_box = Box("blah", "abcd"); | |
| std::string data = absl::StrCat(uuid_box, other_box); | |
| std::vector<BmffBoxHeader> result; | |
| riegeli::StringReader<> reader(data); | |
| EXPECT_THAT(IterateOverBmffBoxes(reader, | |
| [&result](const BmffBoxHeader& box) { | |
| result.push_back(box); | |
| return false; // Stop early! | |
| }), | |
| IsOk()); | |
| EXPECT_THAT(result, ElementsAre(BmffBoxHeader{ | |
| .start = 0, | |
| .box_size = 28, | |
| .header_size = 24, | |
| .version_and_flags_size = 4, | |
| .type = "uuid", | |
| .xpath = "/uuid[1]", | |
| .user_type = std::string(kC2paBmffBoxUuid), | |
| .version = 0, | |
| .flags = std::string(kZeroFlags)})); | |
| } | |
| TEST_F(BoxHeaderTest, PropagatesError) { | |
| std::string data = Box("blah", "abcd"); | |
| riegeli::StringReader<> reader(data); | |
| auto status = IterateOverBmffBoxes(reader, [](const BmffBoxHeader& box) { | |
| return absl::InternalError("custom error"); | |
| }); | |
| EXPECT_THAT(status, StatusIs(absl::StatusCode::kInternal, "custom error")); | |
| } | |
| TEST_F(BoxHeaderTest, IterateOverBmffBoxesStopsEarlyInSubBox) { | |
| std::string sub_box = Box("blah", "abcd"); | |
| std::string container_box = Box("moov", sub_box); | |
| std::string data = container_box; | |
| std::vector<BmffBoxHeader> result; | |
| riegeli::StringReader<> reader(data); | |
| EXPECT_THAT(IterateOverBmffBoxes(reader, | |
| [&result](const BmffBoxHeader& box) { | |
| result.push_back(box); | |
| if (box.type == "blah") { | |
| return false; // Stop early in sub-box! | |
| } | |
| return true; | |
| }), | |
| IsOk()); | |
| EXPECT_THAT(result, ElementsAre(BmffBoxHeader{.start = 0, | |
| .box_size = 20, | |
| .header_size = 8, | |
| .type = "moov", | |
| .xpath = "/moov[1]"}, | |
| BmffBoxHeader{.start = 8, | |
| .box_size = 12, | |
| .header_size = 8, | |
| .type = "blah", | |
| .xpath = "/moov[1]/blah[1]"})); | |
| } | |
| class NoSizeReader : public riegeli::Reader { | |
| public: | |
| bool SupportsSize() override { return false; } | |
| bool PullSlow(size_t min_length, size_t recommended_length) override { | |
| return false; | |
| } | |
| }; | |
| TEST_F(BoxHeaderTest, IterateOverBmffBoxesRequiresSize) { | |
| NoSizeReader reader; | |
| EXPECT_THAT( | |
| IterateOverBmffBoxes(reader, [](const BmffBoxHeader&) { return true; }), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("reader size cannot be determined"))); | |
| } | |
| TEST_F(BoxHeaderTest, BoxSizeTooSmallForUuidExploit) { | |
| std::string custom_uuid = absl::StrCat( | |
| absl::string_view("\x00\x00\x00\x08", 4), "free", std::string(8, '\x00')); | |
| ASSERT_EQ(custom_uuid.size(), 16); | |
| std::string payload = absl::StrCat(custom_uuid, BoxVersion(0), BoxFlags(0)); | |
| std::string input = BadBox("uuid", payload, 8); | |
| riegeli::StringReader<> reader(input); | |
| std::vector<std::string> parsed_types; | |
| auto status = IterateOverBmffBoxes(reader, [&](const BmffBoxHeader& box) { | |
| parsed_types.push_back(box.type); | |
| return true; | |
| }); | |
| // We expect it to fail and NOT process the "free" box. | |
| EXPECT_FALSE(status.ok()); | |
| EXPECT_THAT(parsed_types, IsEmpty()); | |
| } | |
| TEST_F(BoxHeaderTest, BoxSizeTooSmallFor64BitHeader) { | |
| std::string input = | |
| absl::StrCat(absl::string_view("\x00\x00\x00\x01", 4), "blah", | |
| absl::string_view("\x00\x00\x00\x00\x00\x00\x00\x08", 8)); | |
| riegeli::StringReader<> reader(input); | |
| std::vector<std::string> parsed_types; | |
| auto status = IterateOverBmffBoxes(reader, [&](const BmffBoxHeader& box) { | |
| parsed_types.push_back(box.type); | |
| return true; | |
| }); | |
| EXPECT_FALSE(status.ok()); | |
| EXPECT_THAT(parsed_types, IsEmpty()); | |
| } | |
| TEST_F(BoxHeaderTest, GenericUuidBoxAtEndWithoutVersionFlags) { | |
| std::string custom_uuid(16, '\x00'); | |
| std::string input = BadBox("uuid", custom_uuid, 24); | |
| EXPECT_THAT( | |
| ReadAllBmffBoxHeaders(input), | |
| IsOkAndHolds(ElementsAre(BmffBoxHeader{.start = 0, | |
| .box_size = 24, | |
| .header_size = 24, | |
| .version_and_flags_size = 0, | |
| .type = "uuid", | |
| .xpath = "/uuid[1]", | |
| .user_type = custom_uuid, | |
| .version = 0, | |
| .flags = ""}))); | |
| } | |
| std::string NestBoxes(absl::string_view type, int depth) { | |
| std::string current_data = ""; | |
| for (int i = 0; i < depth; ++i) { | |
| current_data = Box(type, current_data); | |
| } | |
| return current_data; | |
| } | |
| TEST_F(BoxHeaderTest, DeeplyNestedBoxes) { | |
| std::string input = NestBoxes("moov", 100); | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(input), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Too many nested BMFF boxes"))); | |
| } | |
| using BoxHeaderContainerTest = testing::TestWithParam<std::string>; | |
| INSTANTIATE_TEST_SUITE_P(, BoxHeaderContainerTest, | |
| testing::Values("moov", "trak", "mdia", "minf", "stbl", | |
| "moof", "traf", "edts", "udta", "dinf", | |
| "tref", "treg", "mvex", "mfra", "meta", | |
| "schi"), | |
| [](const testing::TestParamInfo<std::string>& info) { | |
| return info.param.data(); | |
| }); | |
| TEST_P(BoxHeaderContainerTest, IsContainerBox) { | |
| EXPECT_TRUE(BmffBoxHeader{.type = GetParam()}.IsContainerBox()); | |
| } | |
| TEST(BoxHeader, IsRootBox) { | |
| { | |
| BmffBoxHeader header = {}; | |
| EXPECT_TRUE(header.IsRootBox()); | |
| } | |
| { | |
| BmffBoxHeader header = {.xpath = "/"}; | |
| EXPECT_TRUE(header.IsRootBox()); | |
| } | |
| { | |
| BmffBoxHeader header = {.xpath = "/uuid"}; | |
| EXPECT_TRUE(header.IsRootBox()); | |
| } | |
| { | |
| BmffBoxHeader header = {.xpath = "/trak[1]"}; | |
| EXPECT_TRUE(header.IsRootBox()); | |
| } | |
| { | |
| BmffBoxHeader header = {.xpath = "/trak/moov"}; | |
| EXPECT_FALSE(header.IsRootBox()); | |
| } | |
| { | |
| BmffBoxHeader header = {.xpath = "/trak[1]/moov"}; | |
| EXPECT_FALSE(header.IsRootBox()); | |
| } | |
| } | |
| TEST_F(BoxHeaderTest, WriteUUIDBox) { | |
| std::string data = "abcd"; | |
| BmffBox box{.type = "uuid", | |
| .user_type = std::string(kC2paBmffBoxUuid), | |
| .version = 1, | |
| .flags = BoxFlags(2), | |
| .data_size = data.size()}; | |
| std::string serialized; | |
| EXPECT_THAT(WriteBox(box, data, serialized), IsOkAndHolds(28)); | |
| EXPECT_EQ(serialized.size(), BmffBoxHeaderSize(box) + data.size()); | |
| EXPECT_THAT( | |
| ReadAllBmffBoxHeaders(serialized), | |
| IsOkAndHolds(ElementsAre(BmffBoxHeader{ | |
| .start = 0, | |
| .box_size = 28 + data.size(), // header_size + 4 (version+flags) | |
| .header_size = 24, // 4 (size) + 4 (type) + 16 (UUID) | |
| .version_and_flags_size = 4, | |
| .type = "uuid", | |
| .xpath = "/uuid[1]", | |
| .user_type = std::string(kC2paBmffBoxUuid), | |
| .version = 1, | |
| .flags = BoxFlags(2)}))); | |
| } | |
| TEST_F(BoxHeaderTest, WriteFullBox) { | |
| std::string data = "abcd"; | |
| BmffBox box{.type = "pdin", | |
| .user_type = "", | |
| .version = 0, | |
| .flags = std::string(kZeroFlags), | |
| .data_size = data.size()}; | |
| std::string serialized; | |
| EXPECT_THAT(WriteBox(box, data, serialized), IsOkAndHolds(12)); | |
| EXPECT_EQ(serialized.size(), BmffBoxHeaderSize(box) + data.size()); | |
| EXPECT_THAT( | |
| ReadAllBmffBoxHeaders(serialized), | |
| IsOkAndHolds(ElementsAre(BmffBoxHeader{ | |
| .start = 0, | |
| .box_size = 12 + data.size(), // header_size + 4 (version+flags) | |
| .header_size = 8, // 4 (size) + 4 (type) | |
| .version_and_flags_size = 4, | |
| .type = "pdin", | |
| .xpath = "/pdin[1]", | |
| .user_type = "", | |
| .version = 0, | |
| .flags = std::string(kZeroFlags)}))); | |
| } | |
| TEST_F(BoxHeaderTest, WriteNotFullBox) { | |
| std::string data = "abcd"; | |
| BmffBox box{.type = "xxxx", .data_size = data.size()}; | |
| std::string serialized; | |
| EXPECT_THAT(WriteBox(box, data, serialized), IsOkAndHolds(8)); | |
| EXPECT_EQ(serialized.size(), BmffBoxHeaderSize(box) + data.size()); | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(serialized), | |
| IsOkAndHolds(ElementsAre( | |
| BmffBoxHeader{.start = 0, | |
| .box_size = 8 + data.size(), | |
| .header_size = 8, // 4 (size) + 4 (type) | |
| .type = "xxxx", | |
| .xpath = "/xxxx[1]"}))); | |
| } | |
| TEST_F(BoxHeaderTest, WriteTwoBoxes) { | |
| std::string data = "abcd"; | |
| BmffBox uuid_box{.type = "uuid", | |
| .user_type = std::string(kC2paBmffBoxUuid), | |
| .version = 1, | |
| .flags = BoxFlags(2), | |
| .data_size = data.size()}; | |
| BmffBox other_box{.type = "blah", .data_size = data.size()}; | |
| std::string uuid_serialized; | |
| EXPECT_THAT(WriteBox(uuid_box, data, uuid_serialized), IsOkAndHolds(28)); | |
| std::string other_serialized; | |
| EXPECT_THAT(WriteBox(other_box, data, other_serialized), IsOkAndHolds(8)); | |
| std::string serialized = absl::StrCat(uuid_serialized, other_serialized); | |
| EXPECT_EQ(serialized.size(), BmffBoxHeaderSize(uuid_box) + data.size() + | |
| BmffBoxHeaderSize(other_box) + data.size()); | |
| EXPECT_THAT(ReadAllBmffBoxHeaders(serialized), | |
| IsOkAndHolds(ElementsAre( | |
| BmffBoxHeader{.start = 0, | |
| .box_size = 28 + data.size(), | |
| .header_size = 24, | |
| .version_and_flags_size = 4, | |
| .type = "uuid", | |
| .xpath = "/uuid[1]", | |
| .user_type = std::string(kC2paBmffBoxUuid), | |
| .version = 1, | |
| .flags = BoxFlags(2)}, | |
| BmffBoxHeader{.start = 32, | |
| .box_size = 8 + data.size(), | |
| .header_size = 8, | |
| .type = "blah", | |
| .xpath = "/blah[1]"}))); | |
| } | |
| TEST_F(BoxHeaderTest, WriteFailsWhenStatusNotOKBeforeWrite) { | |
| std::string data = "abcd"; | |
| BmffBox box{.type = "xxxx", .data_size = data.size()}; | |
| std::string serialized; | |
| EXPECT_THAT(WriteBox(box, data, serialized, /*start_with_error=*/true), | |
| StatusIs(absl::StatusCode::kInternal)); | |
| EXPECT_TRUE(serialized.empty()); | |
| } | |
| TEST_F(BoxHeaderTest, WriteSize64bits) { | |
| uint64_t data_size = 4294967296; // uint32_t max + 1 | |
| BmffBox box{.type = "xxxx", .data_size = data_size}; | |
| std::string serialized; | |
| EXPECT_THAT(WriteBox(box, "", serialized), IsOkAndHolds(16)); | |
| EXPECT_EQ(serialized.size(), BmffBoxHeaderSize(box)); | |
| BmffBoxHeader read; | |
| ABSL_ASSERT_OK(ReadBox(serialized, read)); | |
| BmffBoxHeader expected{ | |
| .start = 0, | |
| .box_size = 8 + 8 + data_size, | |
| .header_size = 8 + 8, // 4 (size) + 4 (type) + 8 (64bits) | |
| .type = "xxxx"}; | |
| EXPECT_EQ(read, expected); | |
| } | |
| struct LocateInsertionPointTestParam { | |
| std::string test_name; | |
| std::string contents; | |
| absl::StatusOr<ByteRange> expected; | |
| }; | |
| using LocateInsertionPointTest = | |
| testing::TestWithParam<LocateInsertionPointTestParam>; | |
| INSTANTIATE_TEST_SUITE_P( | |
| LocateInsertionPointTests, LocateInsertionPointTest, | |
| testing::ValuesIn<LocateInsertionPointTestParam>({ | |
| { | |
| .test_name = "BlankFile", | |
| .contents = "", | |
| .expected = absl::InvalidArgumentError("no mdat/moov box found"), | |
| }, | |
| { | |
| .test_name = "NoMdatOrMoov", | |
| .contents = Box("free", "00"), | |
| .expected = absl::InvalidArgumentError("no mdat/moov box found"), | |
| }, | |
| { | |
| .test_name = "OnlyMdat", | |
| .contents = Box("mdat", "00"), | |
| .expected = ByteRange{.offset = 0, .length = 0}, | |
| }, | |
| { | |
| .test_name = "OnlyMoov", | |
| .contents = Box("moov", "00"), | |
| .expected = ByteRange{.offset = 0, .length = 0}, | |
| }, | |
| { | |
| .test_name = "Mdat", | |
| .contents = absl::StrCat(Box("ftyp", "00"), Box("mdat", "00")), | |
| .expected = ByteRange{.offset = 10, .length = 0}, | |
| }, | |
| { | |
| .test_name = "Moov", | |
| .contents = absl::StrCat(Box("ftyp", "00"), Box("moov", "00")), | |
| .expected = ByteRange{.offset = 10, .length = 0}, | |
| }, | |
| { | |
| .test_name = "MdatAndMoov", | |
| .contents = absl::StrCat(Box("ftyp", "00"), Box("mdat", "00"), | |
| Box("moov", "00")), | |
| .expected = ByteRange{.offset = 10, .length = 0}, | |
| }, | |
| { | |
| .test_name = "FreeAfterMdat", | |
| .contents = absl::StrCat(Box("ftyp", "00"), Box("mdat", "00"), | |
| Box("free", "00")), | |
| .expected = ByteRange{.offset = 10, .length = 0}, | |
| }, | |
| { | |
| .test_name = "FreeAfterMoov", | |
| .contents = absl::StrCat(Box("ftyp", "00"), Box("moov", "00"), | |
| Box("free", "00")), | |
| .expected = ByteRange{.offset = 10, .length = 0}, | |
| }, | |
| { | |
| .test_name = "OneFreeBox", | |
| .contents = absl::StrCat(Box("ftyp", "00"), Box("free", "00"), | |
| Box("mdat", "00")), | |
| .expected = ByteRange{.offset = 10, .length = 10}, | |
| }, | |
| { | |
| .test_name = "TwoSeparateFreeBoxesFirstLarger", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "00"), Box("free", "000000000000"), | |
| Box("data", "00"), Box("free", "00"), Box("mdat", "00")), | |
| .expected = ByteRange{.offset = 10, .length = 20}, | |
| }, | |
| { | |
| .test_name = "TwoSeparateFreeBoxesSecondLarger", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "00"), Box("free", "00"), Box("data", "00"), | |
| Box("free", "000000000000"), Box("mdat", "00")), | |
| .expected = ByteRange{.offset = 30, .length = 20}, | |
| }, | |
| { | |
| .test_name = "AdjacentFreeBoxes", | |
| .contents = absl::StrCat(Box("ftyp", "00"), Box("free", "00"), | |
| Box("free", "00"), Box("mdat", "00")), | |
| .expected = ByteRange{.offset = 10, .length = 20}, | |
| }, | |
| { | |
| .test_name = "LargerFreeBoxAndSeparateC2paManifest", | |
| .contents = absl::StrCat( | |
| Box("ftyp", "00"), Box("free", std::string(102, '0')), | |
| Box("data", "00"), C2paUuidBox("00000"), Box("mdat", "00")), | |
| .expected = ByteRange{.offset = 130, .length = 50}, | |
| }, | |
| { | |
| .test_name = "C2paManifestThenFreeBox", | |
| .contents = absl::StrCat(Box("ftyp", "00"), C2paUuidBox("00000"), | |
| Box("free", "00"), Box("mdat", "00")), | |
| .expected = ByteRange{.offset = 10, .length = 60}, | |
| }, | |
| { | |
| .test_name = "FreeBoxThenC2paManifest", | |
| .contents = absl::StrCat(Box("ftyp", "00"), Box("free", "00"), | |
| C2paUuidBox("00000"), Box("mdat", "00")), | |
| .expected = ByteRange{.offset = 10, .length = 60}, | |
| }, | |
| { | |
| .test_name = "C2paManifest", | |
| .contents = absl::StrCat(Box("ftyp", "00"), C2paUuidBox("00000"), | |
| Box("mdat", "00")), | |
| .expected = ByteRange{.offset = 10, .length = 50}, | |
| }, | |
| { | |
| .test_name = "C2paOriginal", | |
| .contents = absl::StrCat(Box("ftyp", "00"), | |
| C2paUuidBox("00000", | |
| kBmffC2paBoxPurposeOriginal), | |
| Box("mdat", "00")), | |
| .expected = ByteRange{.offset = 10, .length = 50}, | |
| }, | |
| { | |
| .test_name = "C2paMerkle", | |
| .contents = absl::StrCat(Box("ftyp", "00"), | |
| C2paUuidBox("0000000", | |
| kBmffC2paBoxPurposeMerkle), | |
| Box("mdat", "00")), | |
| .expected = ByteRange{.offset = 60, .length = 0}, | |
| }, | |
| { | |
| .test_name = "C2paUpdate", | |
| .contents = absl::StrCat(Box("ftyp", "00"), | |
| C2paUuidBox("0000000", | |
| kBmffC2paBoxPurposeUpdate), | |
| Box("mdat", "00")), | |
| .expected = ByteRange{.offset = 60, .length = 0}, | |
| }, | |
| }), | |
| [](const testing::TestParamInfo<LocateInsertionPointTest::ParamType>& | |
| info) { return info.param.test_name; }); | |
| TEST_P(LocateInsertionPointTest, FindsInsertionPoint) { | |
| riegeli::StringReader reader(GetParam().contents); | |
| if (GetParam().expected.ok()) { | |
| EXPECT_THAT(LocateManifestInsertionPoint(reader), | |
| IsOkAndHolds(Eq(GetParam().expected.value()))); | |
| } else { | |
| EXPECT_THAT(LocateManifestInsertionPoint(reader), | |
| StatusIs(GetParam().expected.status().code(), | |
| HasSubstr(GetParam().expected.status().message()))); | |
| } | |
| } | |
| TEST(IsBmffFullBoxTest, True) { | |
| for (const absl::string_view type : credentio_internal::kFullBoxTypes) { | |
| EXPECT_TRUE(IsBmffFullBox(type)); | |
| } | |
| } | |
| TEST(IsBmffFullBoxTest, False) { | |
| EXPECT_FALSE(IsBmffFullBox("")); | |
| EXPECT_FALSE(IsBmffFullBox("a")); | |
| EXPECT_FALSE(IsBmffFullBox("uri")); | |
| EXPECT_FALSE(IsBmffFullBox("aaaa")); | |
| EXPECT_FALSE(IsBmffFullBox("aaaaaaaaaaaaaaaaaaaa")); | |
| } | |
| } // namespace | |
| } // namespace credentio |