blob: df119ffd425fcc8e4abc948705ae32e54a1acb7f [file]
// 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/extractor.h"
#include <sys/types.h>
#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#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/string_view.h"
#include "formats/asset_box.h"
#include "formats/byte_range.h"
#include "formats/extractor_result.h"
#include "formats/jpeg/testing/app_segment_creator.h"
#include "formats/jpeg/testing/jpeg_creator.h"
#include "formats/jpeg/testing/jumbf_creator.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "riegeli/bytes/string_reader.h"
#include "testing/jumbf_utils.h"
namespace credentio {
namespace {
using ::absl_testing::StatusIs;
using ::testing::Eq;
using ::testing::HasSubstr;
#ifndef ASSERT_OK_AND_ASSIGN
#define ASSERT_OK_AND_ASSIGN_CONCAT2(x, y) x##y
#define ASSERT_OK_AND_ASSIGN_CONCAT(x, y) ASSERT_OK_AND_ASSIGN_CONCAT2(x, y)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr) \
ASSERT_OK_AND_ASSIGN_IMPL(lhs, rexpr, __COUNTER__)
#define ASSERT_OK_AND_ASSIGN_IMPL(lhs, rexpr, id) \
auto ASSERT_OK_AND_ASSIGN_CONCAT(status_or_, id) = (rexpr); \
ASSERT_THAT(ASSERT_OK_AND_ASSIGN_CONCAT(status_or_, id), \
::absl_testing::IsOk()); \
lhs = std::move(*ASSERT_OK_AND_ASSIGN_CONCAT(status_or_, id))
#endif
std::vector<AssetBox> CreateBoxes(
std::vector<std::pair<std::string, uint32_t>> expected) {
uint64_t offset = 0;
std::vector<AssetBox> boxes;
for (const auto& name_and_size : expected) {
boxes.push_back(AssetBox{
.identifier = name_and_size.first,
.byte_range = {.offset = offset, .length = name_and_size.second},
});
offset += name_and_size.second;
}
return boxes;
}
struct PayloadInput {
std::string payload;
JumbfCreatorParams jumbf_creator_params;
AppSegmentParams app_segment_params;
};
struct TestInput {
std::string name;
std::vector<PayloadInput> payload = {};
JpegParams jpeg_params = {};
void (*modify_jumbf_boxes)(std::vector<AppSegmentParams>*) = nullptr;
void (*modify_app_segments)(JpegParams*) = nullptr;
absl::StatusOr<ExtractorResult> expected =
absl::InvalidArgumentError("Unsupported hard binding type");
};
class ExtractorTest : public testing::TestWithParam<TestInput> {
private:
std::vector<AppSegmentParams> PopulateJumbfBoxes(TestInput input) {
std::vector<AppSegmentParams> jumbf_boxes;
jumbf_boxes.reserve(input.payload.size());
for (const auto& payload : input.payload) {
JumbfCreatorParams jumbf_params = payload.jumbf_creator_params;
jumbf_params.payload = payload.payload;
AppSegmentParams app_segment_params = payload.app_segment_params;
app_segment_params.payload = CreateJumbf(jumbf_params);
jumbf_boxes.push_back(app_segment_params);
}
if (input.modify_jumbf_boxes != nullptr) {
input.modify_jumbf_boxes(&jumbf_boxes);
}
return jumbf_boxes;
}
absl::StatusOr<JpegParams> PopulateAppSegments(
TestInput input, std::vector<AppSegmentParams> jumbf_boxes) {
JpegParams result = input.jpeg_params;
result.payload.clear();
result.payload.reserve(jumbf_boxes.size());
uint16_t en = 0;
for (auto& jumbf_box : jumbf_boxes) {
if (!jumbf_box.en.has_value()) {
jumbf_box.en = ++en;
}
ABSL_ASSIGN_OR_RETURN(auto segments, CreateAppSegments(jumbf_box));
for (const auto& segment : segments) {
result.payload.push_back(segment);
}
}
if (input.modify_app_segments != nullptr) {
input.modify_app_segments(&result);
}
return result;
}
protected:
absl::StatusOr<std::string> CreateImage(TestInput input) {
ABSL_ASSIGN_OR_RETURN(
JpegParams params,
PopulateAppSegments(input, PopulateJumbfBoxes(input)));
return CreateJpeg(params);
}
};
TEST_P(ExtractorTest, Extract) {
ASSERT_OK_AND_ASSIGN(auto image, CreateImage(GetParam()));
riegeli::StringReader input(image);
absl::StatusOr<std::string> extractor_result =
JpegExtractor().ExtractManifestStore(input);
if (GetParam().expected.ok()) {
ASSERT_OK_AND_ASSIGN(const std::string& manifest_store, extractor_result);
ASSERT_OK_AND_ASSIGN(const auto& expected, GetParam().expected);
EXPECT_THAT(manifest_store, Eq(expected.manifest_store));
} else {
EXPECT_THAT(extractor_result,
StatusIs(GetParam().expected.status().code(),
HasSubstr(GetParam().expected.status().message())));
}
}
TEST_P(ExtractorTest, ExtractWorksWhenPrefixPadded) {
ASSERT_OK_AND_ASSIGN(auto image, CreateImage(GetParam()));
std::string contents = "padding" + image;
riegeli::StringReader input(contents);
ASSERT_TRUE(input.Seek(7));
absl::StatusOr<std::string> extractor_result =
JpegExtractor().ExtractManifestStore(input);
if (GetParam().expected.ok()) {
ASSERT_OK_AND_ASSIGN(const std::string& manifest_store, extractor_result);
ASSERT_OK_AND_ASSIGN(const auto& expected, GetParam().expected);
EXPECT_THAT(manifest_store, Eq(expected.manifest_store));
} else {
EXPECT_THAT(extractor_result,
StatusIs(GetParam().expected.status().code(),
HasSubstr(GetParam().expected.status().message())));
}
}
TEST_P(ExtractorTest, ExtractManifestStoreLocation) {
ASSERT_OK_AND_ASSIGN(auto image, CreateImage(GetParam()));
riegeli::StringReader input(image);
absl::StatusOr<std::optional<ByteRange>> result =
JpegExtractor().ExtractManifestStoreLocation(input, {});
if (GetParam().expected.ok()) {
ASSERT_OK_AND_ASSIGN(auto manifest_store_location, result);
ASSERT_OK_AND_ASSIGN(const auto& expected, GetParam().expected);
EXPECT_THAT(manifest_store_location,
Eq(expected.asset_byte_info.manifest_store_location));
} else {
EXPECT_THAT(result,
StatusIs(GetParam().expected.status().code(),
HasSubstr(GetParam().expected.status().message())));
}
}
TEST_P(ExtractorTest, ExtractBoxes) {
ASSERT_OK_AND_ASSIGN(auto image, CreateImage(GetParam()));
riegeli::StringReader input(image);
absl::StatusOr<std::vector<AssetBox>> result =
JpegExtractor().ExtractBoxes(input, {});
if (GetParam().expected.ok()) {
ASSERT_OK_AND_ASSIGN(const auto& boxes, result);
ASSERT_OK_AND_ASSIGN(const auto& expected, GetParam().expected);
EXPECT_THAT(boxes, Eq(expected.asset_byte_info.boxes));
} else {
EXPECT_THAT(result,
StatusIs(GetParam().expected.status().code(),
HasSubstr(GetParam().expected.status().message())));
}
}
TEST_P(ExtractorTest, ExtractManifestStoreLocationWithSuffix) {
ASSERT_OK_AND_ASSIGN(auto image, CreateImage(GetParam()));
std::string contents = image + "suffix";
riegeli::StringReader input(contents);
absl::StatusOr<std::optional<ByteRange>> result =
JpegExtractor().ExtractManifestStoreLocation(
input, {.end_offset = static_cast<int64_t>(contents.size() - 6)});
if (GetParam().expected.ok()) {
ASSERT_OK_AND_ASSIGN(auto manifest_store_location, result);
ASSERT_OK_AND_ASSIGN(const auto& expected, GetParam().expected);
EXPECT_THAT(manifest_store_location,
Eq(expected.asset_byte_info.manifest_store_location));
} else {
EXPECT_THAT(result,
StatusIs(GetParam().expected.status().code(),
HasSubstr(GetParam().expected.status().message())));
}
}
TEST_P(ExtractorTest, ExtractBoxesWithSuffix) {
ASSERT_OK_AND_ASSIGN(auto image, CreateImage(GetParam()));
std::string contents = image + "suffix";
riegeli::StringReader input(contents);
absl::StatusOr<std::vector<AssetBox>> result = JpegExtractor().ExtractBoxes(
input, {.end_offset = static_cast<int64_t>(contents.size()) - 6});
if (GetParam().expected.ok()) {
ASSERT_OK_AND_ASSIGN(const auto& boxes, result);
ASSERT_OK_AND_ASSIGN(const auto& expected, GetParam().expected);
EXPECT_THAT(boxes, Eq(expected.asset_byte_info.boxes));
} else {
EXPECT_THAT(result,
StatusIs(GetParam().expected.status().code(),
HasSubstr(GetParam().expected.status().message())));
}
}
INSTANTIATE_TEST_SUITE_P(
ImageEncoding, ExtractorTest,
testing::Values<TestInput>(
TestInput{
.name = "InvalidBeginning",
.payload = {},
.jpeg_params = {.start_marker = 0x0102},
.expected = absl::InvalidArgumentError("Invalid JPEG marker: 258"),
},
TestInput{
.name = "InvalidEnding",
.payload = {},
.jpeg_params = {.end_markers = 0x0304},
.expected = absl::InvalidArgumentError("Invalid JPEG marker: 772"),
},
TestInput{
.name = "NoSegments",
.payload = {},
.jpeg_params = {},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "NoApp11Segments",
.payload = {{.payload = "test",
.app_segment_params = {.marker = 0xffe2}}},
.jpeg_params = {},
.expected = absl::NotFoundError("No manifest store found"),
}),
[](const testing::TestParamInfo<ExtractorTest::ParamType>& info) {
return info.param.name;
});
INSTANTIATE_TEST_SUITE_P(
Segments, ExtractorTest,
testing::Values<TestInput>(
TestInput{
.name = "OnlyApp2Segment",
.payload = {{
.payload = "test",
.app_segment_params = {.marker = 0xffe2},
}},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "IncorrectJpegExtensionType",
.payload = {{
.payload = "test",
.app_segment_params = {.cl = 0101},
}},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "InvalidSegmentId",
.payload = {{
.payload = "test",
.app_segment_params = {.en = 0x0000},
}},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "SegmentsOutOfOrder",
.payload =
{
{
.payload =
"This is a much longer string of test to ensure "
"that there is enough data to expect a C2PA "
"manifest store within the segment payload.",
.app_segment_params = {.max_size = 150},
},
},
.modify_app_segments =
[](JpegParams* params) {
ASSERT_EQ(params->payload.size(), 2);
auto temp = params->payload[0];
params->payload[0] = params->payload[1];
params->payload[1] = temp;
},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "IncompleteSegment",
.payload =
{
{
.payload = "longer test",
.app_segment_params = {.max_size = 50},
},
},
.modify_app_segments =
[](JpegParams* params) {
ASSERT_EQ(params->payload.size(), 2);
params->payload.pop_back();
},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "SplitSegment",
.payload =
{
{
.payload = "longer test",
.app_segment_params = {.max_size = 50},
},
{
.payload = "test",
.app_segment_params = {.marker = 0xffe6},
},
},
.modify_app_segments =
[](JpegParams* params) {
ASSERT_EQ(params->payload.size(), 3);
auto temp = params->payload[1];
params->payload[1] = params->payload[2];
params->payload[2] = temp;
},
.expected = absl::NotFoundError("No manifest store found"),
}),
[](const testing::TestParamInfo<ExtractorTest::ParamType>& info) {
return info.param.name;
});
INSTANTIATE_TEST_SUITE_P(
JumbfBoxes, ExtractorTest,
testing::Values<TestInput>(
TestInput{
.name = "TooFewBytesToDetermineIfC2pa",
.payload = {{.payload = "test"}},
.modify_jumbf_boxes =
[](std::vector<AppSegmentParams>* params) {
ASSERT_EQ(params->size(), 1);
params->at(0).payload = params->at(0).payload.substr(0, 30);
},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "IncorrectSuperBoxTbox",
.payload = {{
.payload = "test",
.jumbf_creator_params = {.super_box_identifier = 0x12345678},
}},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "InvalidUuidHigh",
.payload = {{
.payload = "test",
.jumbf_creator_params = {.uuid_high = 0x0000000000000000},
}},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "InvalidUuidLow",
.payload = {{
.payload = "test",
.jumbf_creator_params = {.uuid_low = 0x0000000000000000},
}},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "InvalidToggles",
.payload = {{
.payload = "test",
.jumbf_creator_params = {.toggles = 0b00110000},
}},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "InvalidLabel",
.payload = {{
.payload = "test",
.jumbf_creator_params = {.label = "demo"},
}},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "ForceSizeIntoXlBox",
.payload = {{
.payload = "test",
.jumbf_creator_params = {.force_xlbox = true},
}},
.expected =
ExtractorResult{
.manifest_store = CreateJumbf({.payload = "test",
.force_xlbox = true}),
.asset_byte_info =
{
.manifest_store_location = ByteRange{.offset = 2,
.length = 78},
.boxes = CreateBoxes({
{"SOI", /*size=*/2},
{"C2PA", /*size=*/78},
{"EOI", /*size=*/2},
}),
},
},
},
TestInput{
.name = "IncorrectLboxSize",
.payload = {{
.payload = "longer test",
.app_segment_params = {.max_size = 50},
}},
.modify_app_segments =
[](JpegParams* params) {
ASSERT_EQ(params->payload.size(), 2);
params->payload.pop_back();
},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "IncorrectXlboxSize",
.payload = {{
.payload = "This is a much longer string of test to ensure "
"that there is enough data to expect a C2PA "
"manifest store within the segment payload.",
.jumbf_creator_params = {.force_xlbox = true},
.app_segment_params = {.max_size = 100},
}},
.modify_app_segments =
[](JpegParams* params) {
ASSERT_EQ(params->payload.size(), 3);
params->payload.pop_back();
},
.expected = absl::NotFoundError("No manifest store found"),
}),
[](const testing::TestParamInfo<ExtractorTest::ParamType>& info) {
return info.param.name;
});
INSTANTIATE_TEST_SUITE_P(
C2paManifestStore, ExtractorTest,
testing::Values<TestInput>(
TestInput{
.name = "OnlyManifestStore",
.payload = {{.payload = "test"}},
.expected =
ExtractorResult{
.manifest_store = CreateJumbf({.payload = "test"}),
.asset_byte_info =
{
.manifest_store_location = ByteRange{.offset = 2,
.length = 62},
.boxes = CreateBoxes({
{"SOI", /*size=*/2},
{"C2PA", /*size=*/62},
{"EOI", /*size=*/2},
}),
},
},
},
TestInput{
.name = "SandwichedManifestStore",
.payload =
{
{
.payload = "app3",
.app_segment_params = {.marker = 0xffe3},
},
{.payload = "test"},
{
.payload = "app6",
.app_segment_params = {.marker = 0xffe6},
},
},
.expected =
ExtractorResult{
.manifest_store = CreateJumbf({.payload = "test"}),
.asset_byte_info =
{
.manifest_store_location = ByteRange{.offset = 64,
.length = 62},
.boxes = CreateBoxes({
{"SOI", /*size=*/2},
{"APP3", /*size=*/62},
{"C2PA", /*size=*/62},
{"APP6", /*size=*/62},
{"EOI", /*size=*/2},
}),
},
},
},
TestInput{
.name = "ManifestStoreAcrossTwoSegments",
.payload =
{
{
.payload = "longer test",
.app_segment_params = {.max_size = 50},
},
},
.expected =
ExtractorResult{
.manifest_store = CreateJumbf({.payload = "longer test"}),
.asset_byte_info =
{
.manifest_store_location = ByteRange{.offset = 2,
.length = 89},
.boxes = CreateBoxes({
{"SOI", /*size=*/2},
{"C2PA", /*size=*/89},
{"EOI", /*size=*/2},
}),
},
},
},
TestInput{
.name = "ManifestStoreAcrossTwoSegmentsDifferentTbox",
.payload =
{
{
.payload = "longer test",
.app_segment_params = {.max_size = 50},
},
},
.modify_app_segments =
[](JpegParams* params) {
ASSERT_EQ(params->payload.size(), 2);
// lowest byte of tbox is at 19, normally 0x62, change it
params->payload[1][19] = 0x69;
},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "ManifestStoreAcrossTwoSegmentsDifferentLbox",
.payload =
{
{
.payload = "longer test",
.app_segment_params = {.max_size = 50},
},
},
.modify_app_segments =
[](JpegParams* params) {
ASSERT_EQ(params->payload.size(), 2);
// lowest byte of lbox is at 15, normally 0x39, change it
params->payload[1][15] = 0x47;
},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "ManifestStoreAcrossTwoSegmentsDifferentXlbox",
.payload =
{
{
.payload = "longer test",
.jumbf_creator_params = {.force_xlbox = true},
.app_segment_params = {.max_size = 60},
},
},
.modify_app_segments =
[](JpegParams* params) {
ASSERT_EQ(params->payload.size(), 2);
// lowest byte of xlbox is at 27, normally 0x49, change it
params->payload[1][27] = 0x57;
},
.expected = absl::NotFoundError("No manifest store found"),
},
TestInput{
.name = "TwoManifestStores",
.payload =
{
{.payload = "store 1"},
{.payload = "store 2"},
},
.expected = absl::NotFoundError("Multiple manifest stores found"),
},
TestInput{
.name = "TwoManifestStoresThenOthers",
.payload =
{{.payload = "store 1"},
{.payload = "store 2"},
{.payload = "app8", .app_segment_params = {.marker = 0xffe8}},
{.payload = "app9", .app_segment_params = {.marker = 0xffe9}}},
.expected = absl::NotFoundError("Multiple manifest stores found"),
},
// If there are multiple manifest stores, but one of them is invalid
// JUMBF we should still return the valid one.
TestInput{
.name = "TwoManifestStoresOneInvalid",
.payload =
{
{.payload = "store 1"},
{
.payload = "store 2",
.app_segment_params = {.max_size = 50},
},
},
.modify_app_segments =
[](JpegParams* params) {
// Remove the last segment of the second manifest store to
// invalidate it
ASSERT_EQ(params->payload.size(), 3);
params->payload.pop_back();
},
.expected =
ExtractorResult{
.manifest_store = CreateJumbf({.payload = "store 1"}),
.asset_byte_info =
{
.manifest_store_location = ByteRange{.offset = 2,
.length = 65},
.boxes = CreateBoxes({
{"SOI", /*size=*/2},
{"C2PA", /*size=*/65},
{"APP11", /*size=*/52},
{"EOI", /*size=*/2},
}),
},
},
},
TestInput{
.name = "MultiSegmentC2paAndMultiSegmentMiscApp11",
.payload =
{
{
.payload =
"This is a much longer string of test to ensure "
"that there is enough data to expect a C2PA "
"manifest store within the segment payload.",
.app_segment_params = {.max_size = 100},
},
{
.payload =
"This is a much longer string of test to ensure "
"that there is enough data to expect a C2PA "
"manifest store within the segment payload.",
.app_segment_params = {.cl = 0x1111, .max_size = 100},
},
},
.expected =
ExtractorResult{
.manifest_store = CreateJumbf(
{.payload =
"This is a much longer string of test to ensure "
"that there is enough data to expect a C2PA "
"manifest store within the segment payload."}),
.asset_byte_info =
{
.manifest_store_location = ByteRange{.offset = 2,
.length = 230},
.boxes = CreateBoxes({
{"SOI", /*size=*/2},
{"C2PA", /*size=*/230},
{"APP11", /*size=*/102},
{"APP11", /*size=*/102},
{"APP11", /*size=*/26},
{"EOI", /*size=*/2},
}),
},
},
}),
[](const testing::TestParamInfo<ExtractorTest::ParamType>& info) {
return info.param.name;
});
TEST(JpegExtractorTest, IsManifestStorePayloadMissingJpegElements) {
EXPECT_FALSE(JpegExtractor().MightBeC2paManifestStore(
CreateStartOfManifestStorePayload()));
}
TEST(JpegExtractorTest, IsManifestStoreIncorrectExtensionType) {
EXPECT_FALSE(JpegExtractor().MightBeC2paManifestStore(
CreateStartOfManifestStorePayload(
{0x12, 0x34, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01})));
}
TEST(JpegExtractorTest, IsManifestStore) {
EXPECT_TRUE(JpegExtractor().MightBeC2paManifestStore(
CreateStartOfManifestStorePayload(
{0x4a, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01})));
}
} // namespace
} // namespace credentio