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