| // 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/reader.h" | |
| #include <sys/types.h> | |
| #include <cstdint> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/strings/str_format.h" | |
| #include "absl/strings/string_view.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "riegeli/bytes/string_reader.h" | |
| namespace credentio { | |
| namespace { | |
| using ::absl_testing::IsOk; | |
| using ::absl_testing::StatusIs; | |
| using ::testing::HasSubstr; | |
| using ::testing::PrintToString; | |
| using ::testing::SizeIs; | |
| class MarkerTest : public testing::TestWithParam<uint16_t> {}; | |
| INSTANTIATE_TEST_SUITE_P(MarkerBasedTests, MarkerTest, | |
| ::testing::Range(uint16_t{0xff00}, uint16_t{0xffff}), | |
| [](const testing::TestParamInfo<uint16_t>& marker) { | |
| return absl::StrFormat("%04x", marker.param); | |
| }); | |
| TEST_P(MarkerTest, HandlesMarker) { | |
| std::string data("\0\0\0\4\1\1\2\2\1\1\1\1\1\1\1\1", 16); | |
| data[0] = static_cast<uint8_t>((GetParam() & 0x0000ff00) >> 8); | |
| data[1] = static_cast<uint8_t>((GetParam() & 0x000000ff)); | |
| data[6] = 0xff; | |
| data[7] = 0xff; | |
| riegeli::StringReader<> input(data); | |
| JpegSegment segment; | |
| bool called = false; | |
| EXPECT_THAT( | |
| IterateOverJpegSegments(input, | |
| [&segment, &called](const JpegSegment& chunk) { | |
| segment = std::move(chunk); | |
| called = true; | |
| return false; // stop after first | |
| }), | |
| IsOk()); | |
| ASSERT_TRUE(called); | |
| EXPECT_EQ(segment.marker, GetParam()); | |
| EXPECT_EQ(segment.offset, 0); | |
| if (GetParam() == 0xff01 || GetParam() == 0xffd8 || GetParam() == 0xffd9) { | |
| // No payload. | |
| EXPECT_EQ(segment.length, 2); | |
| EXPECT_EQ(segment.payload_offset, 2); | |
| EXPECT_EQ(segment.payload_length, 0); | |
| return; | |
| } | |
| EXPECT_EQ(segment.length, 6); | |
| if ((GetParam() >= 0xffd0 && GetParam() <= 0xffd7)) { | |
| // No defined length, so length bytes are part of payload | |
| EXPECT_EQ(segment.payload_offset, 2); | |
| EXPECT_EQ(segment.payload_length, 4); | |
| return; | |
| } | |
| EXPECT_EQ(segment.payload_offset, 4); | |
| EXPECT_EQ(segment.payload_length, 2); | |
| } | |
| TEST(ReaderTest, IterateOverMultipleSegments) { | |
| // SOI (2 bytes), APP0 (6 bytes with 2 bytes payload), EOI (2 bytes) | |
| std::string data("\xff\xd8\xff\xe0\x00\x04\x01\x02\xff\xd9", 10); | |
| riegeli::StringReader<> input(data); | |
| std::vector<JpegSegment> segments; | |
| EXPECT_THAT(IterateOverJpegSegments(input, | |
| [&segments](const JpegSegment& chunk) { | |
| segments.push_back(chunk); | |
| return true; // continue | |
| }), | |
| IsOk()); | |
| // SOI is processed, APP0 is processed, EOI is processed (and breaks the loop) | |
| ASSERT_EQ(segments.size(), 3); | |
| EXPECT_EQ(segments[0].marker, 0xffd8); | |
| EXPECT_EQ(segments[0].marker_label, "SOI"); | |
| EXPECT_EQ(segments[1].marker, 0xffe0); | |
| EXPECT_EQ(segments[1].marker_label, "APP0"); | |
| EXPECT_EQ(segments[2].marker, 0xffd9); | |
| EXPECT_EQ(segments[2].marker_label, "EOI"); | |
| } | |
| TEST(ReaderTest, ParsesValidApp11Segment) { | |
| std::string data( | |
| "\xff\xd8" // SOI | |
| "\xff\xeb\x00\x12" // APP11, length 18 | |
| "\x4a\x50\x00\x01\x00\x00\x00\x01" // APP11 header (type 0x4a50, id 1, | |
| // seq 1) | |
| "\x00\x00\x00\x08\x6a\x75\x6d\x62" // JUMBF header (lbox 8, tbox 'jumb') | |
| "\xff\xd9", // EOI | |
| 24); | |
| riegeli::StringReader<> input(data); | |
| std::vector<JpegSegment> segments; | |
| EXPECT_THAT(IterateOverJpegSegments(input, | |
| [&segments](const JpegSegment& chunk) { | |
| segments.push_back(chunk); | |
| return true; | |
| }), | |
| IsOk()); | |
| ASSERT_EQ(segments.size(), 3); | |
| EXPECT_EQ(segments[1].marker, 0xffeb); | |
| EXPECT_EQ(segments[1].marker_label, "APP11"); | |
| ASSERT_TRUE(segments[1].app11_info.has_value()); | |
| EXPECT_EQ(segments[1].app11_info->extension_type, 0x4a50); | |
| EXPECT_EQ(segments[1].app11_info->segment_id, 1); | |
| EXPECT_EQ(segments[1].app11_info->sequence_number, 1); | |
| ASSERT_TRUE(segments[1].app11_info->jumbf_data.has_value()); | |
| EXPECT_EQ(segments[1].app11_info->jumbf_data->lbox, 8); | |
| EXPECT_EQ(segments[1].app11_info->jumbf_data->tbox, 0x6a756d62); | |
| EXPECT_EQ(segments[1].app11_info->jumbf_data->xlbox, 0); | |
| } | |
| TEST(ReaderTest, HandlesTruncatedBox) { | |
| // SOI (2 bytes), APP0 (18 bytes total: marker 2, length 2, payload 14) | |
| std::string data( | |
| "\xff\xd8" | |
| "\xff\xe0\x00\x10\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d" | |
| "\x0e", | |
| 20); | |
| riegeli::StringReader<> input(data); | |
| // We pass end_offset = 15, which truncates the APP0 box. | |
| EXPECT_THAT( | |
| IterateOverJpegSegments( | |
| input, [](const JpegSegment& chunk) { return true; }, 15), | |
| StatusIs(absl::StatusCode::kInvalidArgument, "truncated JPEG box")); | |
| } | |
| TEST(ReaderTest, ParsesSosWithByteStuffing) { | |
| std::string data( | |
| "\xff\xd8" // SOI | |
| "\xff\xda\x00\x06\xaa\xbb\xcc\xdd" // SOS header (size 8) | |
| "\x11\x22\xff\x00\x33\x44" // Scan data (6 bytes, containing stuffed | |
| // \xff\x00) | |
| "\xff\xd9", // EOI | |
| 18); | |
| riegeli::StringReader<> input(data); | |
| std::vector<JpegSegment> segments; | |
| EXPECT_THAT(IterateOverJpegSegments(input, | |
| [&segments](const JpegSegment& chunk) { | |
| segments.push_back(chunk); | |
| return true; | |
| }), | |
| IsOk()); | |
| ASSERT_EQ(segments.size(), 3); | |
| EXPECT_EQ(segments[1].marker, 0xffda); | |
| EXPECT_EQ(segments[1].marker_label, "SOS"); | |
| EXPECT_EQ(segments[1].offset, 2); | |
| EXPECT_EQ(segments[1].length, 14); // 8 (header) + 6 (scan data) | |
| EXPECT_EQ(segments[1].payload_offset, 6); | |
| EXPECT_EQ(segments[1].payload_length, 10); | |
| EXPECT_EQ(segments[2].marker, 0xffd9); | |
| EXPECT_EQ(segments[2].marker_label, "EOI"); | |
| EXPECT_EQ(segments[2].offset, 16); | |
| } | |
| TEST(ReaderTest, ParsesC2paAfterSegment) { | |
| std::string data( | |
| "\xff\xd8" // SOI | |
| "\xff\xd9" // EOI | |
| "\x01\x02\x03\x04", // Extra data | |
| 8); | |
| riegeli::StringReader<> input(data); | |
| std::vector<JpegSegment> segments; | |
| EXPECT_THAT(IterateOverJpegSegments(input, | |
| [&segments](const JpegSegment& chunk) { | |
| segments.push_back(chunk); | |
| return true; | |
| }), | |
| IsOk()); | |
| ASSERT_EQ(segments.size(), 3); | |
| EXPECT_EQ(segments[0].marker, 0xffd8); | |
| EXPECT_EQ(segments[1].marker, 0xffd9); | |
| EXPECT_EQ(segments[2].marker, 0x0000); | |
| EXPECT_EQ(segments[2].marker_label, "c2pa.after"); | |
| EXPECT_EQ(segments[2].offset, 4); | |
| EXPECT_EQ(segments[2].length, 4); | |
| EXPECT_EQ(segments[2].payload_offset, 0); | |
| EXPECT_EQ(segments[2].payload_length, 0); | |
| } | |
| TEST(ReaderTest, PropagatesProcessorError) { | |
| std::string data("\xff\xd8\xff\xe0\x00\x04\x01\x02\xff\xd9", 10); | |
| riegeli::StringReader<> input(data); | |
| EXPECT_THAT( | |
| IterateOverJpegSegments(input, | |
| [](const JpegSegment& chunk) { | |
| return absl::InternalError("test error"); | |
| }), | |
| StatusIs(absl::StatusCode::kInternal, "test error")); | |
| } | |
| TEST(ReaderTest, PrintToJpegJumbfData) { | |
| JpegJumbfData data = {.lbox = 1, | |
| .tbox = 2, | |
| .xlbox = 3, | |
| .payload_offset = 4, | |
| .payload_length = 5}; | |
| EXPECT_EQ( | |
| PrintToString(data), | |
| "{lbox: 1, tbox: 2, xlbox: 3, payload_offset: 4, payload_length: 5}"); | |
| } | |
| TEST(ReaderTest, PrintToJpegApp11Segment) { | |
| JpegApp11Segment segment = { | |
| .extension_type = 1, | |
| .segment_id = 2, | |
| .sequence_number = 3, | |
| .payload_offset = 4, | |
| .payload_length = 5, | |
| .jumbf_data = JpegJumbfData{.lbox = 10, | |
| .tbox = 20, | |
| .xlbox = 30, | |
| .payload_offset = 40, | |
| .payload_length = 50}}; | |
| EXPECT_EQ(PrintToString(segment), | |
| "{extension_type: 1, segment_id: 2, sequence_number: 3, " | |
| "payload_offset: 4, payload_length: 5, jumbf_data: {lbox: 10, " | |
| "tbox: 20, xlbox: 30, payload_offset: 40, payload_length: 50}}"); | |
| JpegApp11Segment segment_no_jumbf = {.extension_type = 1, | |
| .segment_id = 2, | |
| .sequence_number = 3, | |
| .payload_offset = 4, | |
| .payload_length = 5, | |
| .jumbf_data = std::nullopt}; | |
| EXPECT_EQ(PrintToString(segment_no_jumbf), | |
| "{extension_type: 1, segment_id: 2, sequence_number: 3, " | |
| "payload_offset: 4, payload_length: 5, jumbf_data: nullopt}"); | |
| } | |
| TEST(ReaderTest, PrintToJpegSegment) { | |
| JpegSegment segment = { | |
| .offset = 1, | |
| .marker = 2, | |
| .marker_label = "LABEL", | |
| .length = 3, | |
| .payload_offset = 4, | |
| .payload_length = 5, | |
| .app11_info = JpegApp11Segment{.extension_type = 10, | |
| .segment_id = 20, | |
| .sequence_number = 30, | |
| .payload_offset = 40, | |
| .payload_length = 50, | |
| .jumbf_data = std::nullopt}}; | |
| EXPECT_EQ(PrintToString(segment), | |
| "{offset: 1, marker: 2, marker_label: \"LABEL\", length: 3, " | |
| "payload_offset: 4, payload_length: 5, app11_info: " | |
| "{extension_type: 10, segment_id: 20, sequence_number: 30, " | |
| "payload_offset: 40, payload_length: 50, jumbf_data: nullopt}}"); | |
| JpegSegment segment_no_app11 = {.offset = 1, | |
| .marker = 2, | |
| .marker_label = "LABEL", | |
| .length = 3, | |
| .payload_offset = 4, | |
| .payload_length = 5, | |
| .app11_info = std::nullopt}; | |
| EXPECT_EQ(PrintToString(segment_no_app11), | |
| "{offset: 1, marker: 2, marker_label: \"LABEL\", length: 3, " | |
| "payload_offset: 4, payload_length: 5, app11_info: nullopt}"); | |
| } | |
| TEST(ReaderTest, MalformedSegmentSize) { | |
| std::string data("\xff\xd8\xff\xe0\x00\x00\xff\xd9", 8); | |
| riegeli::StringReader<> input(data); | |
| std::vector<JpegSegment> segments; | |
| EXPECT_THAT(IterateOverJpegSegments(input, | |
| [&segments](const JpegSegment& chunk) { | |
| segments.push_back(chunk); | |
| return true; | |
| }), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Invalid JPEG segment size: 0"))); | |
| } | |
| TEST(ReaderTest, App11SegmentShortLengthDesynchronization) { | |
| std::string data( | |
| "\xff\xd8" | |
| "\xff\xeb\x00\x04\x00\x00" | |
| "\xff\xeb\x00\x12\x4a\x50\x00\x01\x00\x00\x00\x01\x00\x00\x00\x08\x6a\x75" | |
| "\x6d\x62" | |
| "\xff\xd9", | |
| 30); | |
| riegeli::StringReader<> input(data); | |
| std::vector<JpegSegment> segments; | |
| EXPECT_THAT(IterateOverJpegSegments(input, | |
| [&segments](const JpegSegment& chunk) { | |
| segments.push_back(chunk); | |
| return true; | |
| }), | |
| IsOk()); | |
| ASSERT_EQ(segments.size(), 4); | |
| EXPECT_EQ(segments[0].marker_label, "SOI"); | |
| EXPECT_EQ(segments[1].marker, 0xffeb); | |
| EXPECT_EQ(segments[1].length, 6); | |
| EXPECT_FALSE(segments[1].app11_info.has_value()); | |
| EXPECT_EQ(segments[2].marker, 0xffeb); | |
| EXPECT_EQ(segments[2].offset, 8); | |
| EXPECT_EQ(segments[2].length, 20); | |
| EXPECT_TRUE(segments[2].app11_info.has_value()); | |
| EXPECT_EQ(segments[3].marker_label, "EOI"); | |
| } | |
| TEST(ReaderTest, App11JumbfBoxXlboxTooShort) { | |
| std::string data( | |
| "\xff\xd8" | |
| "\xff\xeb\x00\x1c\x4a\x50\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x6a\x75" | |
| "\x6d\x62\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x00" | |
| "\xff\xd9", | |
| 34); | |
| riegeli::StringReader<> input(data); | |
| std::vector<JpegSegment> segments; | |
| EXPECT_THAT(IterateOverJpegSegments(input, | |
| [&segments](const JpegSegment& chunk) { | |
| segments.push_back(chunk); | |
| return true; | |
| }), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("JUMBf box payload too short"))); | |
| } | |
| TEST(ReaderTest, App11JumbfBoxLboxReserved) { | |
| std::string data( | |
| "\xff\xd8" | |
| "\xff\xeb\x00\x12\x4a\x50\x00\x01\x00\x00\x00\x01\x00\x00\x00\x05\x6a\x75" | |
| "\x6d\x62" | |
| "\xff\xd9", | |
| 24); | |
| riegeli::StringReader<> input(data); | |
| std::vector<JpegSegment> segments; | |
| EXPECT_THAT(IterateOverJpegSegments(input, | |
| [&segments](const JpegSegment& chunk) { | |
| segments.push_back(chunk); | |
| return true; | |
| }), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("JUMBf box payload too short"))); | |
| } | |
| TEST(ReaderTest, App11JumbfBoxPayloadLengthTooShort) { | |
| // SOI (2 bytes) | |
| // APP11 (16 bytes) | |
| // - header (4 bytes) | |
| // - extension info (8 bytes) | |
| // - JUMBF header (4 bytes) * Invalid: JUMBF needs a minimum of 8 bytes | |
| // EOI (2 bytes) | |
| std::string data( | |
| "\xff\xd8" | |
| "\xff\xeb\x00\x0e\x4a\x50\x00\x01\x00\x00\x00\x01\x00\x00\x00\x08" | |
| "\xff\xd9", | |
| 20); | |
| riegeli::StringReader<> input(data); | |
| std::vector<JpegSegment> segments; | |
| EXPECT_THAT(IterateOverJpegSegments(input, | |
| [&segments](const JpegSegment& chunk) { | |
| segments.push_back(chunk); | |
| return true; | |
| }), | |
| IsOk()); | |
| EXPECT_THAT(segments, SizeIs(3)); | |
| EXPECT_EQ(segments[0].marker_label, "SOI"); | |
| EXPECT_EQ(segments[1].marker_label, "APP11"); | |
| EXPECT_TRUE(segments[1].app11_info.has_value()); | |
| EXPECT_FALSE(segments[1].app11_info->jumbf_data.has_value()); | |
| EXPECT_EQ(segments[2].marker_label, "EOI"); | |
| } | |
| TEST(ReaderTest, PayloadLengthDoesNotUnderflowOnShortSegment) { | |
| // SOI (2 bytes: \xff\xd8) followed by EOI (\xff\xd9) | |
| std::string data("\xff\xd8\xff\xd9", 4); | |
| riegeli::StringReader<> input(data); | |
| std::vector<JpegSegment> segments; | |
| EXPECT_THAT(IterateOverJpegSegments(input, | |
| [&segments](const JpegSegment& chunk) { | |
| segments.push_back(chunk); | |
| return true; | |
| }), | |
| IsOk()); | |
| ASSERT_GE(segments.size(), 1); | |
| EXPECT_EQ(segments[0].marker, 0xffd8); | |
| EXPECT_EQ(segments[0].payload_length, 0); | |
| } | |
| } // namespace | |
| } // namespace credentio |