| // 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/gif/extractor.h" |
| |
| #include <algorithm> |
| #include <cstddef> |
| #include <cstdint> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/status/status.h" |
| #include "absl/status/status_macros.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "constants/labels.h" |
| #include "formats/asset_box.h" |
| #include "formats/byte_range.h" |
| #include "formats/gif/constants.h" |
| #include "formats/gif/reader.h" |
| #include "jumbf/utils.h" |
| #include "riegeli/bytes/reader.h" |
| |
| namespace credentio { |
| |
| namespace { |
| |
| constexpr uint64_t kMaxPayloadSize = 10 * 1024 * 1024; // 10 MiB |
| |
| absl::StatusOr<std::string> DecodeDataBlocks(absl::string_view data) { |
| std::string result; |
| result.reserve(data.size()); |
| |
| for (size_t i = 0; i < data.size();) { |
| uint8_t block_size = data[i]; |
| i++; |
| if (block_size == 0) { |
| break; |
| } |
| if (i + block_size > data.size()) { |
| return absl::InvalidArgumentError("Invalid block size in GIF C2PA data"); |
| } |
| result.append(data.substr(i, block_size)); |
| i += block_size; |
| } |
| return result; |
| } |
| |
| } // namespace |
| |
| absl::StatusOr<std::string> GifExtractor::ExtractManifestStore( |
| riegeli::Reader& input) const { |
| ABSL_ASSIGN_OR_RETURN( |
| std::optional<ByteRange> c2pa_location, |
| ExtractManifestStoreLocation(input, {.requires_c2pa = true})); |
| if (!c2pa_location.has_value()) { |
| return absl::NotFoundError("No manifest store found"); |
| } |
| |
| // The block starts with two markers identifying it as an application ext. |
| // Then there is a single byte identifying the size of the extension info. |
| // After that is the extension info itself and then starts the alternating |
| // size and data bytes for the manifest store. |
| int64_t manifest_data_offset = c2pa_location->offset + 14; |
| int64_t manifest_data_length = c2pa_location->length - 14; |
| |
| if (manifest_data_length < 0 || manifest_data_length > kMaxPayloadSize) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Manifest store is too large: ", manifest_data_length, |
| " > ", kMaxPayloadSize)); |
| } |
| |
| if (!input.Seek(manifest_data_offset)) { |
| return input.StatusOrAnnotate( |
| absl::InvalidArgumentError("Failed to seek to manifest data offset")); |
| } |
| std::string encoded_manifest_store; |
| if (!input.Read(manifest_data_length, encoded_manifest_store)) { |
| return input.StatusOrAnnotate( |
| absl::DataLossError("Failed to read manifest data from input")); |
| } |
| |
| // Data is stored as blocks of size-prefixed data. |
| return DecodeDataBlocks(encoded_manifest_store); |
| } |
| |
| absl::StatusOr<std::optional<ByteRange>> |
| GifExtractor::ExtractManifestStoreLocation(riegeli::Reader& input, |
| ExtractOptions options) const { |
| std::optional<GifBlock> c2pa_block = std::nullopt; |
| absl::Status iteration_status = IterateOverGifBlocks( |
| input, |
| [&options, &c2pa_block](const GifBlock& block) -> absl::StatusOr<bool> { |
| if (block.offset >= options.end_offset || |
| block.length > options.end_offset - block.offset) { |
| // We've gone past the declared end of the input. |
| return false; // stop processing |
| } |
| if (block.type == kGifImageDescriptorLabel) { |
| // The manifest store must be before the first image descriptor. |
| // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_embedding_manifests_into_gifs |
| return false; // stop processing |
| } |
| if (block.type == "C2PA") { |
| if (c2pa_block.has_value()) { |
| return absl::NotFoundError("Multiple manifest stores found"); |
| } |
| c2pa_block = std::move(block); |
| } |
| return true; |
| }, |
| options.end_offset); |
| ABSL_RETURN_IF_ERROR(iteration_status); |
| if (!c2pa_block.has_value()) { |
| if (options.requires_c2pa) { |
| return absl::NotFoundError("No manifest store found"); |
| } |
| return std::nullopt; |
| } |
| return ByteRange{.offset = c2pa_block->offset, .length = c2pa_block->length}; |
| } |
| |
| absl::StatusOr<std::vector<AssetBox>> GifExtractor::ExtractBoxes( |
| riegeli::Reader& input, ExtractOptions options) const { |
| if (!input.SupportsSize() || !input.Size().has_value()) { |
| return absl::InvalidArgumentError( |
| "Input does not support size or size is unknown"); |
| } |
| uint64_t end_offset = input.Size().value(); |
| if (options.end_offset >= 0) { |
| end_offset = |
| std::min(end_offset, static_cast<uint64_t>(options.end_offset)); |
| } |
| |
| std::vector<AssetBox> blocks; |
| bool c2pa_block_found = false; |
| absl::Status iteration_status = IterateOverGifBlocks( |
| input, |
| [&end_offset, &c2pa_block_found, |
| &blocks](const GifBlock& block) -> absl::StatusOr<bool> { |
| if (block.offset >= end_offset) { |
| // The box is past the declared end of the input. |
| return false; // stop processing |
| } |
| if (block.length > end_offset - block.offset) { |
| // The box extends past the declared end of the input. |
| if (block.type == "c2pa.after") { |
| // We can truncate the c2pa.after block to the end of the input. |
| blocks.push_back(AssetBox{ |
| .identifier = block.type, |
| .byte_range = {.offset = block.offset, |
| .length = end_offset - block.offset}, |
| }); |
| return false; |
| } |
| return absl::InvalidArgumentError( |
| "Block extends past the declared end of the input"); |
| } |
| if (block.type == "C2PA") { |
| if (c2pa_block_found) { |
| return absl::NotFoundError("Multiple manifest stores found"); |
| } |
| c2pa_block_found = true; |
| } |
| |
| blocks.push_back(AssetBox{ |
| .identifier = block.type, |
| .byte_range = {.offset = block.offset, .length = block.length}, |
| }); |
| return true; |
| }, |
| end_offset); |
| ABSL_RETURN_IF_ERROR(iteration_status); |
| if (!c2pa_block_found && options.requires_c2pa) { |
| return absl::NotFoundError("No manifest store found"); |
| } |
| return blocks; |
| } |
| |
| bool GifExtractor::MightBeC2paManifestStore(absl::string_view payload) const { |
| return jumbf::HasDescriptionBoxMatching(payload, kManifestStoreUuid, |
| kMinimumJumbfDescriptionToggles, |
| kManifestStoreLabel) |
| .value_or(false); |
| } |
| |
| } // namespace credentio |