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