blob: 45f8779a2acd6c9d32a4cff01785ea376d2d2c78 [file] [edit]
// 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 <cstddef>
#include <cstdint>
#include <string>
#include <utility>
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "absl/strings/substitute.h"
#include "absl/types/span.h"
#include "constants/labels.h"
#include "formats/jpeg/reader.h"
#include "jumbf/box.h"
#include "jumbf/constants.h"
#include "jumbf/parse.h"
#include "riegeli/bytes/reader.h"
namespace credentio {
namespace {
constexpr uint64_t kMaxPayloadSize = 1024 * 1024 * 10; // 10 MiB
constexpr size_t kMaxSegmentsPerId = 1000;
bool IsValidNextSegment(const JpegSegment& prev_segment,
const JpegSegment& curr_segment) {
if (!prev_segment.app11_info.has_value() ||
!curr_segment.app11_info.has_value()) {
// Missing APP11 information, invalid.
return false;
}
if (!prev_segment.app11_info->jumbf_data.has_value() ||
!curr_segment.app11_info->jumbf_data.has_value()) {
// Segments do not contain JUMBF data, invalid.
return false;
}
// C2PA Spec requires sequence numbers to be increasing.
if (prev_segment.app11_info->sequence_number >=
curr_segment.app11_info->sequence_number) {
// Sequence number is not increasing, invalid.
return false;
}
// C2PA Spec requires segments to be contiguous.
if ((prev_segment.offset + prev_segment.length) != curr_segment.offset) {
// Segments are not contiguous, invalid.
return false;
}
// All segments must have the same lbox and xlbox.
if (prev_segment.app11_info->jumbf_data->lbox !=
curr_segment.app11_info->jumbf_data->lbox) {
// Lbox is different, invalid.
return false;
}
if (prev_segment.app11_info->jumbf_data->xlbox !=
curr_segment.app11_info->jumbf_data->xlbox) {
// Xlbox is different, invalid.
return false;
}
return true;
}
absl::StatusOr<std::string> ExtractCompletePayload(
riegeli::Reader& input, absl::Span<const JpegSegment> segments) {
if (segments.empty() || !segments.front().app11_info.has_value() ||
!segments.front().app11_info->jumbf_data.has_value()) {
return "";
}
uint64_t expected_payload_size =
segments.front().app11_info->jumbf_data->lbox == 1
? segments.front().app11_info->jumbf_data->xlbox
: segments.front().app11_info->jumbf_data->lbox;
if (expected_payload_size == 0 || expected_payload_size > kMaxPayloadSize) {
return absl::InvalidArgumentError(absl::Substitute(
"Declared JUMBF payload size is invalid ($0)", expected_payload_size));
}
std::string payload;
for (const JpegSegment& segment : segments) {
if (!segment.app11_info.has_value() ||
!segment.app11_info->jumbf_data.has_value()) {
// Segment does not contain JUMBF data within an APP11 segment, skip it.
continue;
}
uint64_t payload_offset = segment.app11_info->payload_offset;
uint64_t payload_length = segment.app11_info->payload_length;
if (!payload.empty()) {
// This is not the first segment, so skip the JUMBF header data
payload_offset = segment.app11_info->jumbf_data->payload_offset;
payload_length = segment.app11_info->jumbf_data->payload_length;
}
if (payload.size() + payload_length > kMaxPayloadSize) {
return absl::InvalidArgumentError(
absl::Substitute("JPEG segment is too large to extract ($0 > $1)",
payload.size() + payload_length, kMaxPayloadSize));
}
if (!input.Seek(payload_offset) || input.pos() != payload_offset) {
return absl::InvalidArgumentError("Failed to seek to payload offset");
}
std::string segment_payload;
if (!input.Read(payload_length, segment_payload)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read payload"));
}
payload.append(segment_payload);
}
if (payload.size() != expected_payload_size) {
// Incomplete payload, return empty string.
return "";
}
return payload;
}
bool IsC2paManifestStore(absl::string_view payload) {
absl::StatusOr<jumbf::SuperBox> box =
jumbf::ConsumeSuperBox(&payload, /*recursion_limit=*/0);
if (!box.ok()) {
// If we can't read a JUMBF Superbox, it's not a C2PA Manifest Store,
// ignore.
return false;
}
if (box->description.type_uuid != kManifestStoreUuid) {
// JUMBF Superbox is not a C2PA Manifest Store, ignore.
return false;
};
if (!box->description.requestable) {
// Manifest store must be requestable.
return false;
}
return box->description.label == kManifestStoreLabel;
}
} // namespace
void C2paSegmentHandler::RecordSegment(JpegSegment segment) {
if (!segment.app11_info.has_value() ||
!segment.app11_info->jumbf_data.has_value()) {
// Segment does not contain APP11 information, skip it.
return;
}
const JpegApp11Segment& app11_info = *segment.app11_info;
const JpegJumbfData& jumbf_data = *app11_info.jumbf_data;
if (ignored_segment_ids_.contains(app11_info.segment_id)) {
// Already ignored this segment ID, skip it.
return;
}
if (app11_info.segment_id == 0) {
// Invalid segment id, skip it.
return;
}
if (jumbf_data.tbox != jumbf::kSuperBoxType) {
// Segment is not a JUMBF Superbox, skip it.
return;
}
auto [it, inserted] = segment_map_.try_emplace(app11_info.segment_id);
if (inserted) {
// First segment for this segment ID, record it.
it->second.push_back(std::move(segment));
} else {
// Segment ID already exists.
if (it->second.size() >= kMaxSegmentsPerId ||
!IsValidNextSegment(it->second.back(), segment)) {
// Exceeded max segments per ID or invalid continuing segment, remove from
// map and ignore.
segment_map_.erase(it);
ignored_segment_ids_.insert(app11_info.segment_id);
} else {
// Valid continuing segment, record it.
it->second.push_back(std::move(segment));
}
}
}
absl::StatusOr<C2paSegmentHandler::Segment> C2paSegmentHandler::GetC2paSegment(
riegeli::Reader& input) const {
C2paSegmentHandler::Segment result;
int64_t c2pa_count = 0;
for (const auto& [segment_id, segments] : segment_map_) {
if (segments.empty()) {
continue;
}
ABSL_ASSIGN_OR_RETURN(std::string payload,
ExtractCompletePayload(input, segments));
if (payload.empty()) {
// Incomplete payload, ignore.
continue;
}
if (IsC2paManifestStore(payload)) {
++c2pa_count;
if (c2pa_count > 1) {
return absl::NotFoundError("Multiple manifest stores found");
}
result.manifest_store = std::move(payload);
result.segment = JpegSegment{
.offset = segments.front().offset,
.marker_label = "C2PA",
.length = (segments.back().offset - segments.front().offset) +
segments.back().length,
};
}
}
if (c2pa_count == 0) {
return absl::NotFoundError("No manifest store found");
}
return std::move(result);
}
} // namespace credentio