| // 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. |
| // |
| |
| // TIFF Format Information: |
| // https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf |
| // C2PA embedding: |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_embedding_manifests_into_tiff_based_assets |
| #include "formats/tiff/extractor.h" |
| |
| #include <sys/types.h> |
| |
| #include <cstdint> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| |
| #include "absl/container/flat_hash_set.h" |
| #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/byte_range.h" |
| #include "formats/tiff/constants.h" |
| #include "formats/tiff/reader.h" |
| #include "jumbf/utils.h" |
| #include "riegeli/bytes/reader.h" |
| |
| namespace credentio { |
| |
| namespace { |
| |
| constexpr uint64_t kMaxPayloadSize = 1024 * 1024 * 10; // 10 MiB |
| |
| absl::StatusOr<std::optional<ByteRange>> FindManifestStore( |
| riegeli::Reader& source) { |
| absl::flat_hash_set<uint32_t> visited_offsets; |
| |
| std::optional<ByteRange> manifest_store_location = std::nullopt; |
| bool reading_first_ifd = true; |
| absl::Status iteration_status = IterateOverImageFileDirectories( |
| source, std::nullopt, visited_offsets, |
| [&reading_first_ifd, &manifest_store_location]( |
| const TiffImageFileDirectory& ifd, |
| TiffEndianness endianness) -> absl::StatusOr<bool> { |
| // A previous loop found a manifest store. |
| if (manifest_store_location.has_value()) { |
| return absl::NotFoundError("Manifest Store must be in the last IFD"); |
| } |
| |
| for (const TiffImageFileDirectoryEntry& entry : ifd.entries) { |
| if (entry.tag != kTiffTagC2pa) { |
| continue; |
| } |
| |
| if (!reading_first_ifd && ifd.entries.size() != 1) { |
| return absl::NotFoundError( |
| "Manifest Store must be the only entry in the IFD"); |
| } |
| |
| if (manifest_store_location.has_value()) { |
| return absl::NotFoundError("Multiple manifest stores found"); |
| } |
| |
| manifest_store_location = entry.RangeOfValue(); |
| } |
| |
| reading_first_ifd = false; |
| return true; |
| }); |
| ABSL_RETURN_IF_ERROR(iteration_status); |
| return manifest_store_location; |
| } |
| |
| bool IsPartialAsset(int64_t current_offset, int64_t end_offset, |
| int64_t asset_size) { |
| if (end_offset == -1) { |
| // Not a windowed read, so not partial. |
| return false; |
| } |
| if (current_offset == 0 && end_offset == asset_size) { |
| // The window covers the entire asset, so it is not partial. |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| absl::StatusOr<std::string> TiffExtractor::ExtractManifestStore( |
| riegeli::Reader& input) const { |
| ABSL_ASSIGN_OR_RETURN(std::optional<ByteRange> location, |
| ExtractManifestStoreLocation(input, {})); |
| if (!location.has_value()) { |
| return absl::NotFoundError("No manifest store found"); |
| } |
| |
| if (location->length > kMaxPayloadSize) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Manifest store is too large: ", location->length, " > ", |
| kMaxPayloadSize)); |
| } |
| |
| if (!input.Seek(location->offset)) { |
| return input.StatusOrAnnotate( |
| absl::DataLossError("Failed to seek to offset")); |
| } |
| |
| std::string manifest_store; |
| if (!input.Read(location->length, manifest_store)) { |
| return input.StatusOrAnnotate( |
| absl::DataLossError("Failed to read manifest store")); |
| } |
| return std::move(manifest_store); |
| } |
| |
| absl::StatusOr<std::optional<ByteRange>> |
| TiffExtractor::ExtractManifestStoreLocation(riegeli::Reader& input, |
| ExtractOptions options) const { |
| if (IsPartialAsset(input.pos(), options.end_offset, |
| input.Size().value_or(0))) { |
| return absl::InvalidArgumentError( |
| "TiffExtractor::ExtractManifestStoreLocation only supports operations " |
| "over the entire file."); |
| } |
| |
| if (!input.Seek(0)) { |
| return input.StatusOrAnnotate( |
| absl::DataLossError("Failed to seek to offset")); |
| } |
| |
| ABSL_ASSIGN_OR_RETURN(auto result, FindManifestStore(input)); |
| if (!options.requires_c2pa) { |
| return result; |
| } |
| if (!result.has_value()) { |
| return absl::NotFoundError("No manifest store found"); |
| } |
| return result; |
| } |
| |
| bool TiffExtractor::MightBeC2paManifestStore(absl::string_view payload) const { |
| return jumbf::HasDescriptionBoxMatching(payload, kManifestStoreUuid, |
| kMinimumJumbfDescriptionToggles, |
| kManifestStoreLabel) |
| .value_or(false); |
| } |
| |
| } // namespace credentio |