| // 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/jpeg/c2pa_segment_handler.h" |
| |
| #include <cstdint> |
| #include <vector> |
| |
| #include "absl/container/btree_set.h" |
| #include "absl/container/flat_hash_map.h" |
| #include "absl/status/status.h" |
| #include "absl/status/status_matchers.h" |
| #include "formats/jpeg/reader.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "jumbf/constants.h" |
| #include "riegeli/bytes/string_reader.h" |
| |
| namespace credentio { |
| namespace { |
| |
| using ::absl_testing::StatusIs; |
| using ::testing::ElementsAre; |
| using ::testing::HasSubstr; |
| using ::testing::IsEmpty; |
| using ::testing::SizeIs; |
| |
| class TestC2paSegmentHandler : public C2paSegmentHandler { |
| public: |
| std::vector<std::vector<JpegSegment>> AllRecorded() const { |
| absl::btree_set<uint16_t> segment_ids; |
| for (const auto& [segment_id, segments] : GetSegmentMap()) { |
| segment_ids.insert(segment_id); |
| } |
| |
| std::vector<std::vector<JpegSegment>> result; |
| for (uint16_t segment_id : segment_ids) { |
| result.push_back(GetSegmentMap().at(segment_id)); |
| } |
| return result; |
| } |
| |
| absl::btree_set<uint16_t> AllIgnored() const { |
| return absl::btree_set<uint16_t>(GetIgnoredSegmentIds().begin(), |
| GetIgnoredSegmentIds().end()); |
| } |
| }; |
| |
| TEST(RecordSegmentTest, DoesNotRecordSegmentWithoutApp11Info) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment(JpegSegment{}); |
| EXPECT_THAT(handler.AllRecorded(), IsEmpty()); |
| EXPECT_THAT(handler.AllIgnored(), IsEmpty()); |
| } |
| |
| TEST(RecordSegmentTest, DoesNotRecordSegmentIdZero) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment( |
| JpegSegment{.app11_info = JpegApp11Segment{.segment_id = 0}}); |
| EXPECT_THAT(handler.AllRecorded(), IsEmpty()); |
| EXPECT_THAT(handler.AllIgnored(), IsEmpty()); |
| } |
| |
| TEST(RecordSegmentTest, DoesNotRecordSegmentWithoutJumbfData) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment( |
| JpegSegment{.app11_info = JpegApp11Segment{.segment_id = 1}}); |
| EXPECT_THAT(handler.AllRecorded(), IsEmpty()); |
| EXPECT_THAT(handler.AllIgnored(), IsEmpty()); |
| } |
| |
| TEST(RecordSegmentTest, DoesNotRecordSegmentIncorrectTbox) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment(JpegSegment{ |
| .app11_info = JpegApp11Segment{ |
| .segment_id = 1, .jumbf_data = JpegJumbfData{.tbox = 0x1234}}}); |
| EXPECT_THAT(handler.AllRecorded(), IsEmpty()); |
| EXPECT_THAT(handler.AllIgnored(), IsEmpty()); |
| } |
| |
| TEST(RecordSegmentTest, RecordSegment) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment(JpegSegment{ |
| .app11_info = JpegApp11Segment{ |
| .segment_id = 1, |
| .jumbf_data = JpegJumbfData{.tbox = jumbf::kSuperBoxType}}}); |
| EXPECT_THAT( |
| handler.AllRecorded(), |
| ElementsAre(ElementsAre(JpegSegment{ |
| .app11_info = JpegApp11Segment{ |
| .segment_id = 1, |
| .jumbf_data = JpegJumbfData{.tbox = jumbf::kSuperBoxType}}}))); |
| EXPECT_THAT(handler.AllIgnored(), IsEmpty()); |
| } |
| |
| TEST(RecordSegmentTest, DoesNotRecordNextSegmentIncorrectSequence) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment(JpegSegment{ |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1, |
| .jumbf_data = JpegJumbfData{ |
| .tbox = jumbf::kSuperBoxType, |
| }}}); |
| handler.RecordSegment(JpegSegment{ |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1, |
| .jumbf_data = JpegJumbfData{ |
| .tbox = jumbf::kSuperBoxType, |
| }}}); |
| EXPECT_THAT(handler.AllRecorded(), IsEmpty()); |
| EXPECT_THAT(handler.AllIgnored(), ElementsAre(1)); |
| } |
| |
| TEST(RecordSegmentTest, DoesNotRecordNextSegmentNotContiguous) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment(JpegSegment{ |
| .offset = 100, |
| .length = 10, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1, |
| .jumbf_data = JpegJumbfData{ |
| .tbox = jumbf::kSuperBoxType, |
| }}}); |
| handler.RecordSegment(JpegSegment{ |
| .offset = 200, |
| .length = 10, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 2, |
| .jumbf_data = JpegJumbfData{ |
| .tbox = jumbf::kSuperBoxType, |
| }}}); |
| EXPECT_THAT(handler.AllRecorded(), IsEmpty()); |
| EXPECT_THAT(handler.AllIgnored(), ElementsAre(1)); |
| } |
| |
| TEST(RecordSegmentTest, DoesNotRecordNextSegmentDifferentLbox) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment(JpegSegment{ |
| .offset = 100, |
| .length = 10, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 10, |
| .tbox = jumbf::kSuperBoxType, |
| }}}); |
| handler.RecordSegment(JpegSegment{ |
| .offset = 110, |
| .length = 10, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 2, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 20, |
| .tbox = jumbf::kSuperBoxType, |
| }}}); |
| EXPECT_THAT(handler.AllRecorded(), IsEmpty()); |
| EXPECT_THAT(handler.AllIgnored(), ElementsAre(1)); |
| } |
| |
| TEST(RecordSegmentTest, DoesNotRecordNextSegmentDifferentXlbox) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment(JpegSegment{ |
| .offset = 100, |
| .length = 10, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 1, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 10, |
| }}}); |
| handler.RecordSegment(JpegSegment{ |
| .offset = 110, |
| .length = 10, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 2, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 1, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 20, |
| }}}); |
| EXPECT_THAT(handler.AllRecorded(), IsEmpty()); |
| EXPECT_THAT(handler.AllIgnored(), ElementsAre(1)); |
| } |
| |
| TEST(RecordSegmentTest, RecordsNextSegment) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment(JpegSegment{ |
| .offset = 100, |
| .length = 10, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 10, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| }}}); |
| handler.RecordSegment(JpegSegment{ |
| .offset = 110, |
| .length = 10, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 2, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 10, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| }}}); |
| EXPECT_THAT(handler.AllRecorded(), |
| ElementsAre(ElementsAre( |
| JpegSegment{ |
| .offset = 100, |
| .length = 10, |
| .app11_info = |
| JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1, |
| .jumbf_data = |
| JpegJumbfData{ |
| .lbox = 10, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| }}, |
| }, |
| JpegSegment{ |
| .offset = 110, |
| .length = 10, |
| .app11_info = |
| JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 2, |
| .jumbf_data = |
| JpegJumbfData{ |
| .lbox = 10, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| }}, |
| }))); |
| EXPECT_THAT(handler.AllIgnored(), IsEmpty()); |
| } |
| |
| TEST(RecordSegmentTest, DoesNotRecordIgnoredSegment) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment(JpegSegment{ |
| .offset = 100, |
| .length = 10, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 10, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| }}}); |
| handler.RecordSegment(JpegSegment{ |
| .offset = 110, |
| .length = 10, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 10, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| }}}); |
| EXPECT_THAT(handler.AllRecorded(), IsEmpty()); |
| EXPECT_THAT(handler.AllIgnored(), ElementsAre(1)); |
| |
| handler.RecordSegment(JpegSegment{ |
| .offset = 210, |
| .length = 10, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 10, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| }}}); |
| |
| EXPECT_THAT(handler.AllRecorded(), IsEmpty()); |
| EXPECT_THAT(handler.AllIgnored(), ElementsAre(1)); |
| } |
| |
| TEST(RecordSegmentTest, DoesNotRecordMoreThanMaxSegmentsPerId) { |
| TestC2paSegmentHandler handler; |
| for (int i = 1; i <= 1000; ++i) { |
| handler.RecordSegment(JpegSegment{ |
| .offset = static_cast<uint64_t>((i - 1) * 10), |
| .length = 10, |
| .app11_info = |
| JpegApp11Segment{.segment_id = 1, |
| .sequence_number = static_cast<uint32_t>(i), |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 10, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| }}}); |
| } |
| EXPECT_THAT(handler.AllRecorded(), SizeIs(1)); |
| EXPECT_THAT(handler.AllRecorded().front(), SizeIs(1000)); |
| EXPECT_THAT(handler.AllIgnored(), IsEmpty()); |
| |
| handler.RecordSegment(JpegSegment{ |
| .offset = 10000, |
| .length = 10, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1001, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 10, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| }}}); |
| EXPECT_THAT(handler.AllRecorded(), IsEmpty()); |
| EXPECT_THAT(handler.AllIgnored(), ElementsAre(1)); |
| } |
| |
| TEST(GetC2paSegmentTest, RejectsDeclaredPayloadSizeTooLarge) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment(JpegSegment{ |
| .offset = 0, |
| .length = 100, |
| .app11_info = JpegApp11Segment{ |
| .segment_id = 1, |
| .sequence_number = 1, |
| .payload_offset = 0, |
| .payload_length = 10, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 1024 * 1024 * 10 + 1, // > kMaxPayloadSize (10 MiB) |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| }}}); |
| std::string data(10, 'A'); |
| riegeli::StringReader input(data); |
| EXPECT_THAT(handler.GetC2paSegment(input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Declared JUMBF payload size is invalid"))); |
| } |
| |
| TEST(GetC2paSegmentTest, RejectsDeclaredPayloadSizeZero) { |
| TestC2paSegmentHandler handler; |
| handler.RecordSegment(JpegSegment{ |
| .offset = 0, |
| .length = 100, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1, |
| .payload_offset = 0, |
| .payload_length = 10, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = 0, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| }}}); |
| std::string data(10, 'A'); |
| riegeli::StringReader input(data); |
| EXPECT_THAT(handler.GetC2paSegment(input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Declared JUMBF payload size is invalid"))); |
| } |
| |
| TEST(GetC2paSegmentTest, RejectsCumulativePayloadSizeExceedingMax) { |
| TestC2paSegmentHandler handler; |
| const uint64_t kSixMiB = 6 * 1024 * 1024; |
| const uint64_t kFiveMiB = 5 * 1024 * 1024; |
| const uint64_t kTenMiB = 10 * 1024 * 1024; |
| handler.RecordSegment(JpegSegment{ |
| .offset = 0, |
| .length = 100, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 1, |
| .payload_offset = 0, |
| .payload_length = kSixMiB, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = kTenMiB, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| .payload_offset = 0, |
| .payload_length = kSixMiB, |
| }}}); |
| handler.RecordSegment(JpegSegment{ |
| .offset = 100, |
| .length = 100, |
| .app11_info = JpegApp11Segment{.segment_id = 1, |
| .sequence_number = 2, |
| .payload_offset = kSixMiB, |
| .payload_length = kFiveMiB, |
| .jumbf_data = JpegJumbfData{ |
| .lbox = kTenMiB, |
| .tbox = jumbf::kSuperBoxType, |
| .xlbox = 0, |
| .payload_offset = kSixMiB, |
| .payload_length = kFiveMiB, |
| }}}); |
| std::string data(kSixMiB, 'A'); |
| riegeli::StringReader input(data); |
| EXPECT_THAT(handler.GetC2paSegment(input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("JPEG segment is too large to extract"))); |
| } |
| |
| } // namespace |
| } // namespace credentio |