| // 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 |