| // 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 "jumbf/utils.h" | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <cstring> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/numeric/int128.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" // IWYU pragma: keep | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/strings/cord.h" | |
| #include "absl/strings/string_view.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "jumbf/box.h" | |
| #include "jumbf/box_builder.h" | |
| #include "jumbf/constants.h" | |
| #include "jumbf/parse.h" | |
| #include "jumbf/test_utils.h" | |
| #include "riegeli/endian/endian_reading.h" | |
| #include "riegeli/endian/endian_writing.h" | |
| #include "uuid/uuid.h" | |
| namespace jumbf { | |
| namespace { | |
| void WriteUint32NetworkOrder(uint32_t value, std::vector<uint8_t>* bytes) { | |
| bytes->push_back(static_cast<uint8_t>((value >> 24) & 0xFF)); | |
| bytes->push_back(static_cast<uint8_t>((value >> 16) & 0xFF)); | |
| bytes->push_back(static_cast<uint8_t>((value >> 8) & 0xFF)); | |
| bytes->push_back(static_cast<uint8_t>(value & 0xFF)); | |
| } | |
| void WriteUint64NetworkOrder(uint64_t value, std::vector<uint8_t>* bytes) { | |
| bytes->push_back(static_cast<uint8_t>((value >> 56) & 0xFF)); | |
| bytes->push_back(static_cast<uint8_t>((value >> 48) & 0xFF)); | |
| bytes->push_back(static_cast<uint8_t>((value >> 40) & 0xFF)); | |
| bytes->push_back(static_cast<uint8_t>((value >> 32) & 0xFF)); | |
| bytes->push_back(static_cast<uint8_t>((value >> 24) & 0xFF)); | |
| bytes->push_back(static_cast<uint8_t>((value >> 16) & 0xFF)); | |
| bytes->push_back(static_cast<uint8_t>((value >> 8) & 0xFF)); | |
| bytes->push_back(static_cast<uint8_t>(value & 0xFF)); | |
| } | |
| using ::absl_testing::IsOkAndHolds; | |
| using ::absl_testing::StatusIs; | |
| using ::testing::HasSubstr; | |
| inline constexpr uint32_t kUnknownBoxType = 0x1111'1111; | |
| inline constexpr credentio::Uuid kUuid1( | |
| credentio::Uuid::FromStringOrDie("11111111-1111-1111-1111-111111111111")); | |
| inline constexpr absl::uint128 kUuid1_uint128 = | |
| absl::MakeUint128(0x1111111111111111, 0x1111111111111111); | |
| inline constexpr absl::uint128 kUuid2_uint128 = | |
| absl::MakeUint128(0x2222222222222222, 0x2222222222222222); | |
| inline constexpr uint8_t kToggles1 = 0b00001111; | |
| inline constexpr uint8_t kToggles2 = 0b11110000; | |
| inline constexpr absl::string_view kLabel1 = "expected"; | |
| inline constexpr absl::string_view kLabel2 = "unexpected"; | |
| TEST(UtilsTest, TrimJumbfSuperBoxHeader) { | |
| std::string input = WrapBox("content", kSuperBoxType); | |
| EXPECT_THAT(StripBoxHeaders(input), IsOkAndHolds("content")); | |
| } | |
| TEST(UtilsTest, TrimEmptyJumbfSuperBoxHeader) { | |
| EXPECT_THAT(StripBoxHeaders(""), StatusIs(absl::StatusCode::kOutOfRange)); | |
| } | |
| struct BoxData { | |
| std::optional<uint32_t> super_lbox = std::nullopt; | |
| std::optional<uint32_t> super_tbox = std::nullopt; | |
| std::optional<uint64_t> super_xlbox = std::nullopt; | |
| std::optional<uint32_t> desc_lbox = std::nullopt; | |
| std::optional<uint32_t> desc_tbox = std::nullopt; | |
| std::optional<uint64_t> desc_xlbox = std::nullopt; | |
| std::optional<absl::uint128> uuid = std::nullopt; | |
| std::optional<uint8_t> toggles = std::nullopt; | |
| std::optional<absl::string_view> label = std::nullopt; | |
| bool null_terminator = true; | |
| }; | |
| std::string CreatePayload(const BoxData& box_data) { | |
| std::vector<uint8_t> bytes; | |
| if (box_data.super_lbox.has_value()) { | |
| WriteUint32NetworkOrder(*box_data.super_lbox, &bytes); | |
| } | |
| if (box_data.super_tbox.has_value()) { | |
| WriteUint32NetworkOrder(*box_data.super_tbox, &bytes); | |
| } | |
| if (box_data.super_xlbox.has_value()) { | |
| WriteUint64NetworkOrder(*box_data.super_xlbox, &bytes); | |
| } | |
| if (box_data.desc_lbox.has_value()) { | |
| WriteUint32NetworkOrder(*box_data.desc_lbox, &bytes); | |
| } | |
| if (box_data.desc_tbox.has_value()) { | |
| WriteUint32NetworkOrder(*box_data.desc_tbox, &bytes); | |
| } | |
| if (box_data.desc_xlbox.has_value()) { | |
| WriteUint64NetworkOrder(*box_data.desc_xlbox, &bytes); | |
| } | |
| if (box_data.uuid.has_value()) { | |
| WriteUint64NetworkOrder(absl::Uint128High64(*box_data.uuid), &bytes); | |
| WriteUint64NetworkOrder(absl::Uint128Low64(*box_data.uuid), &bytes); | |
| } | |
| if (box_data.toggles.has_value()) { | |
| bytes.push_back(*box_data.toggles); | |
| } | |
| if (box_data.label.has_value()) { | |
| bytes.insert(bytes.end(), box_data.label->begin(), box_data.label->end()); | |
| } | |
| if (box_data.null_terminator) { | |
| bytes.push_back(0x00); | |
| } | |
| return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size()); | |
| } | |
| // Serializes an absl::string_view to a char array, followed by a null | |
| // terminator. | |
| void SerializeStringWithNull(char** dest, absl::string_view str) { | |
| memcpy(*dest, str.data(), str.size()); | |
| *dest += str.size(); | |
| **dest = 0x00; | |
| *dest += 1; | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, NoData) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({}), kUuid1, kToggles1, kLabel1), | |
| StatusIs(absl::StatusCode::kOutOfRange, | |
| HasSubstr("not enough input bytes remaining; have 1 need 4"))); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsSuperBoxLbox) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({.super_lbox = 1}), kUuid1, | |
| kToggles1, kLabel1), | |
| StatusIs(absl::StatusCode::kOutOfRange, | |
| HasSubstr("not enough input bytes remaining; have 1 need 4"))); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsSuperBoxTbox) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching( | |
| CreatePayload({.super_lbox = 1, .super_tbox = 1}), kUuid1, kToggles1, | |
| kLabel1), | |
| StatusIs( | |
| absl::StatusCode::kOutOfRange, | |
| HasSubstr("ran out of input bytes; 8 bytes requested, 1 remaining"))); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsSuperBoxXlboxIncorrectTbox) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching( | |
| CreatePayload({.super_lbox = 1, .super_tbox = 1, .super_xlbox = 1}), | |
| kUuid1, kToggles1, kLabel1), | |
| IsOkAndHolds(false)); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsSuperBoxTboxCorrect) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching( | |
| CreatePayload( | |
| {.super_lbox = 1, .super_tbox = kSuperBoxType, .super_xlbox = 1}), | |
| kUuid1, kToggles1, kLabel1), | |
| StatusIs(absl::StatusCode::kOutOfRange, | |
| HasSubstr("not enough input bytes remaining; have 1 need 4"))); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsDescriptorBoxLbox) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({.super_lbox = 1, | |
| .super_tbox = kSuperBoxType, | |
| .super_xlbox = 1, | |
| .desc_lbox = 1}), | |
| kUuid1, kToggles1, kLabel1), | |
| StatusIs(absl::StatusCode::kOutOfRange, | |
| HasSubstr("not enough input bytes remaining; have 1 need 4"))); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsDescriptorBoxTbox) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({.super_lbox = 1, | |
| .super_tbox = kSuperBoxType, | |
| .super_xlbox = 1, | |
| .desc_lbox = 1, | |
| .desc_tbox = 1}), | |
| kUuid1, kToggles1, kLabel1), | |
| StatusIs( | |
| absl::StatusCode::kOutOfRange, | |
| HasSubstr("ran out of input bytes; 8 bytes requested, 1 remaining"))); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsDescriptorBoxXlboxIncorrectTbox) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({.super_lbox = 1, | |
| .super_tbox = kSuperBoxType, | |
| .super_xlbox = 1, | |
| .desc_lbox = 1, | |
| .desc_tbox = 1, | |
| .desc_xlbox = 1}), | |
| kUuid1, kToggles1, kLabel1), | |
| IsOkAndHolds(false)); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsDescriptorBoxTboxCorrect) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({.super_lbox = 1, | |
| .super_tbox = kSuperBoxType, | |
| .super_xlbox = 1, | |
| .desc_lbox = 1, | |
| .desc_tbox = kDescriptionBoxType, | |
| .desc_xlbox = 1}), | |
| kUuid1, kToggles1, kLabel1), | |
| StatusIs(absl::StatusCode::kOutOfRange, | |
| HasSubstr("Description Box UUID is missing."))); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, | |
| ContainsDescriptorBoxTboxCorrectEmptyUuidPayload) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({.super_lbox = 1, | |
| .super_tbox = kSuperBoxType, | |
| .super_xlbox = 1, | |
| .desc_lbox = 1, | |
| .desc_tbox = kDescriptionBoxType, | |
| .desc_xlbox = 1, | |
| .null_terminator = false}), | |
| kUuid1, kToggles1, kLabel1), | |
| StatusIs(absl::StatusCode::kOutOfRange, | |
| HasSubstr("Description Box UUID is missing."))); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsUuidIncorrect) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({.super_lbox = 1, | |
| .super_tbox = kSuperBoxType, | |
| .super_xlbox = 1, | |
| .desc_lbox = 1, | |
| .desc_tbox = kDescriptionBoxType, | |
| .desc_xlbox = 1, | |
| .uuid = kUuid2_uint128}), | |
| kUuid1, kToggles1, kLabel1), | |
| IsOkAndHolds(false)); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsUuidCorrect) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({.super_lbox = 1, | |
| .super_tbox = kSuperBoxType, | |
| .super_xlbox = 1, | |
| .desc_lbox = 1, | |
| .desc_tbox = kDescriptionBoxType, | |
| .desc_xlbox = 1, | |
| .uuid = kUuid1_uint128, | |
| .null_terminator = false}), | |
| kUuid1, kToggles1, kLabel1), | |
| StatusIs(absl::StatusCode::kOutOfRange, | |
| HasSubstr("not enough input bytes remaining; have 0 need 1"))); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsTogglesIncorrect) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({.super_lbox = 1, | |
| .super_tbox = kSuperBoxType, | |
| .super_xlbox = 1, | |
| .desc_lbox = 1, | |
| .desc_tbox = kDescriptionBoxType, | |
| .desc_xlbox = 1, | |
| .uuid = kUuid1_uint128, | |
| .toggles = kToggles2}), | |
| kUuid1, kToggles1, kLabel1), | |
| IsOkAndHolds(false)); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsTogglesCorrect) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({.super_lbox = 1, | |
| .super_tbox = kSuperBoxType, | |
| .super_xlbox = 1, | |
| .desc_lbox = 1, | |
| .desc_tbox = kDescriptionBoxType, | |
| .desc_xlbox = 1, | |
| .uuid = kUuid1_uint128, | |
| .toggles = kToggles1}), | |
| kUuid1, kToggles1, kLabel1), | |
| IsOkAndHolds(false)); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsWrongLabel) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({.super_lbox = 1, | |
| .super_tbox = kSuperBoxType, | |
| .super_xlbox = 1, | |
| .desc_lbox = 1, | |
| .desc_tbox = kDescriptionBoxType, | |
| .desc_xlbox = 1, | |
| .uuid = kUuid1_uint128, | |
| .toggles = kToggles1, | |
| .label = kLabel2}), | |
| kUuid1, kToggles1, kLabel1), | |
| IsOkAndHolds(false)); | |
| } | |
| TEST(HasDescriptionBoxMatchingTest, ContainsManifest) { | |
| EXPECT_THAT( | |
| HasDescriptionBoxMatching(CreatePayload({.super_lbox = 1, | |
| .super_tbox = kSuperBoxType, | |
| .super_xlbox = 1, | |
| .desc_lbox = 1, | |
| .desc_tbox = kDescriptionBoxType, | |
| .desc_xlbox = 1, | |
| .uuid = kUuid1_uint128, | |
| .toggles = kToggles1, | |
| .label = kLabel1}), | |
| kUuid1, kToggles1, kLabel1), | |
| IsOkAndHolds(true)); | |
| } | |
| TEST(GetContextBoxSizeTest, GetContextBoxSize) { | |
| std::string payload = "test"; | |
| SuperBoxBuilder builder( | |
| kUuid1, {.requestable_iff_label = true, .label = absl::Cord("main")}); | |
| size_t efdb_payload_length = 0; | |
| { | |
| constexpr uint8_t kToggles = jumbf::kEfdbToggleFileNamePresent; | |
| constexpr absl::string_view kFakeMediatype = "media/MyFormat"; | |
| constexpr absl::string_view kFakePath = "path/to/some/file"; | |
| efdb_payload_length = | |
| 1 /*toggles*/ + kFakeMediatype.size() + 1 + kFakePath.size() + 1; | |
| std::string payload(efdb_payload_length, 'A'); | |
| char* cursor = payload.data(); | |
| riegeli::WriteBigEndian<uint8_t>(kToggles, cursor); | |
| cursor += sizeof(uint8_t); | |
| SerializeStringWithNull(&cursor, kFakeMediatype); | |
| SerializeStringWithNull(&cursor, kFakePath); | |
| ABSL_ASSERT_OK(builder.AddContent(kEmbeddedFileDescriptionBoxType, | |
| absl::Cord(payload))); | |
| } | |
| ABSL_ASSERT_OK(builder.AddContent(kUuidBoxType, absl::Cord(payload))); | |
| ABSL_ASSERT_OK(builder.AddContent(kJsonBoxType, absl::Cord(payload))); | |
| ABSL_ASSERT_OK(builder.AddContent(kC2paSaltBoxType, absl::Cord(payload))); | |
| ABSL_ASSERT_OK(builder.AddContent(kBinaryDataBoxType, absl::Cord(payload))); | |
| size_t superbox_payload_size = 0; | |
| size_t child_payload_size = 0; | |
| { | |
| SuperBoxBuilder child_builder( | |
| kUuid1, {.requestable_iff_label = true, .label = absl::Cord("child")}); | |
| ABSL_ASSERT_OK( | |
| child_builder.AddContent(kSuperBoxType, absl::Cord(payload))); | |
| auto child_box_or = std::move(child_builder).Finalize(); | |
| ABSL_ASSERT_OK(child_box_or); | |
| BuiltSuperBox child_box = std::move(*child_box_or); | |
| absl::Cord child_cord = std::move(child_box).AsCord(); | |
| superbox_payload_size = child_cord.size(); | |
| absl::string_view child_view = child_cord.Flatten(); | |
| auto parsed_child_box_or = | |
| ConsumeSuperBox(&child_view, /*recursion_limit=*/0); | |
| ABSL_ASSERT_OK(parsed_child_box_or); | |
| SuperBox parsed_child_box = std::move(*parsed_child_box_or); | |
| child_payload_size = parsed_child_box.contents[0] | |
| .Get<SerializedSuperbox>() | |
| .serialized.size(); | |
| ABSL_ASSERT_OK(builder.AddChild(child_cord)); | |
| } | |
| ABSL_ASSERT_OK(builder.AddContent(kCborBoxType, absl::Cord(payload))); | |
| ABSL_ASSERT_OK(builder.AddContent(kUnknownBoxType, absl::Cord(payload))); | |
| auto built_superbox_or = std::move(builder).Finalize(); | |
| ABSL_ASSERT_OK(built_superbox_or); | |
| BuiltSuperBox built_superbox = std::move(*built_superbox_or); | |
| absl::Cord superbox_cord = std::move(built_superbox).AsCord(); | |
| absl::string_view superbox_view = superbox_cord.Flatten(); | |
| auto box_or = ConsumeSuperBox(&superbox_view, /*recursion_limit=*/1); | |
| ABSL_ASSERT_OK(box_or); | |
| SuperBox box = std::move(*box_or); | |
| EXPECT_TRUE(box.contents[0].Holds<UnknownBox>()); | |
| EXPECT_THAT(GetContextBoxSize(box.contents[0]), payload.size()); | |
| EXPECT_TRUE(box.contents[1].Holds<CborBox>()); | |
| EXPECT_THAT(GetContextBoxSize(box.contents[1]), payload.size()); | |
| EXPECT_TRUE(box.contents[2].Holds<SuperBox>()); | |
| EXPECT_THAT(GetContextBoxSize(box.contents[2]), superbox_payload_size); | |
| SuperBox serialized_superbox = box.contents[2].Get<SuperBox>(); | |
| EXPECT_TRUE(serialized_superbox.contents[0].Holds<SerializedSuperbox>()); | |
| EXPECT_THAT(GetContextBoxSize(serialized_superbox.contents[0]), | |
| child_payload_size); | |
| EXPECT_TRUE(box.contents[3].Holds<BinaryDataBox>()); | |
| EXPECT_THAT(GetContextBoxSize(box.contents[3]), payload.size()); | |
| EXPECT_TRUE(box.contents[4].Holds<C2paSaltBox>()); | |
| EXPECT_THAT(GetContextBoxSize(box.contents[4]), payload.size()); | |
| EXPECT_TRUE(box.contents[5].Holds<JsonBox>()); | |
| EXPECT_THAT(GetContextBoxSize(box.contents[5]), payload.size()); | |
| EXPECT_TRUE(box.contents[6].Holds<UuidBox>()); | |
| EXPECT_THAT(GetContextBoxSize(box.contents[6]), payload.size()); | |
| EXPECT_TRUE(box.contents[7].Holds<EmbeddedFileDescriptionBox>()); | |
| EXPECT_THAT(GetContextBoxSize(box.contents[7]), efdb_payload_length); | |
| } | |
| TEST(TestUtilsTest, SerializeUuid) { | |
| char buf[32] = {'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', | |
| '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', | |
| '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', | |
| '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', | |
| '\xff', '\xff', '\xff', '\xff'}; | |
| char* cursor = buf; | |
| SerializeUuid(&cursor, credentio::Uuid::kInvalid); | |
| EXPECT_EQ(cursor - buf, 16); | |
| for (int i = 0; i < 16; ++i) { | |
| EXPECT_EQ(buf[i], '\0'); | |
| } | |
| SerializeUuid(&cursor, kUuid1); | |
| EXPECT_EQ(cursor - buf, 32); | |
| EXPECT_EQ(std::string(buf + 16, 16), kUuid1.ToProtoBytes()); | |
| } | |
| TEST(UtilsTest, CreateC2paSaltBox) { | |
| constexpr absl::string_view kSalt = "0123456789abcdef"; | |
| auto salt_box_cord = CreateC2paSaltBox(kSalt); | |
| ABSL_ASSERT_OK(salt_box_cord); | |
| std::string salt_box_bytes = std::string(*salt_box_cord); | |
| absl::string_view sv = salt_box_bytes; | |
| ASSERT_GE(sv.size(), 8); | |
| uint32_t lbox = riegeli::ReadBigEndian<uint32_t>(&sv[0]); | |
| uint32_t tbox = riegeli::ReadBigEndian<uint32_t>(&sv[4]); | |
| EXPECT_EQ(lbox, 8 + kSalt.size()); | |
| EXPECT_EQ(tbox, kC2paSaltBoxType); | |
| EXPECT_EQ(sv.substr(8), kSalt); | |
| } | |
| } // namespace | |
| } // namespace jumbf |