| // 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/png/hasher.h" | |
| #include <algorithm> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #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 "bindings/input_hasher.h" | |
| #include "crypto/algorithms.h" | |
| #include "formats/byte_range.h" | |
| #include "formats/png/constants.h" | |
| #include "formats/png/reader.h" | |
| #include "proto/data_hash_assertion.pb.h" | |
| #include "riegeli/bytes/reader.h" | |
| namespace credentio { | |
| namespace { | |
| constexpr uint64_t kMaxChunkBytes = 1024 * 1024; // 1 MiB | |
| } // namespace | |
| absl::StatusOr<DigestPngResult> DigestPng( | |
| riegeli::Reader& input, HashAlgorithm alg, | |
| std::optional<PngExclusionProcessor> exclusion_processor) { | |
| DigestPngResult result; | |
| result.assertion.set_alg(absl::StrCat(alg)); | |
| std::unique_ptr<InputHasher> hasher; | |
| ABSL_ASSIGN_OR_RETURN(hasher, InputHasher::Create(alg)); | |
| std::optional<int64_t> insertion_offset = std::nullopt; | |
| ABSL_RETURN_IF_ERROR(IterateOverPngChunks( | |
| input, | |
| [&result, &input, &hasher, &insertion_offset, | |
| &exclusion_processor](const PngChunk& chunk) -> absl::Status { | |
| if (chunk.type == kPngChunkTypeC2pa) { | |
| if (insertion_offset.has_value()) { | |
| // Multiple C2PA chunks, or C2PA chunk after IDAT chunk. | |
| return absl::InvalidArgumentError("Unexpected C2PA chunk found"); | |
| } | |
| // Since there is an existing C2PA chunk, overwrite it with the new | |
| // one. | |
| insertion_offset = chunk.offset; | |
| result.existing_c2pa_length = chunk.length; | |
| DataRange* exclusion = result.assertion.add_exclusions(); | |
| exclusion->set_start(chunk.offset); | |
| exclusion->set_length(0); | |
| return absl::OkStatus(); | |
| } | |
| if (!insertion_offset.has_value() && chunk.type == kPngChunkTypeIdat) { | |
| // Insert the manifest store immediately before the first IDAT chunk | |
| // unless there is an existing C2PA chunk. | |
| insertion_offset = chunk.offset; | |
| DataRange* exclusion = result.assertion.add_exclusions(); | |
| exclusion->set_start(chunk.offset); | |
| exclusion->set_length(0); | |
| } | |
| if (exclusion_processor.has_value()) { | |
| ABSL_ASSIGN_OR_RETURN(std::vector<ByteRange> exclusions, | |
| (*exclusion_processor)(input, chunk)); | |
| if (!exclusions.empty()) { | |
| int64_t next_idx_to_hash = chunk.offset; | |
| for (const auto& ex : exclusions) { | |
| if (ex.offset < next_idx_to_hash || | |
| ex.offset + ex.length > chunk.offset + chunk.length) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Invalid or out-of-order exclusion range [", | |
| ex.offset, ", ", ex.offset + ex.length, | |
| ") for chunk at ", chunk.offset)); | |
| } | |
| if (next_idx_to_hash < ex.offset) { | |
| ABSL_RETURN_IF_ERROR(hasher->Update( | |
| input, next_idx_to_hash, ex.offset - next_idx_to_hash)); | |
| } | |
| DataRange* exclusion = result.assertion.add_exclusions(); | |
| exclusion->set_start(ex.offset); | |
| exclusion->set_length(ex.length); | |
| next_idx_to_hash = ex.offset + ex.length; | |
| } | |
| // Hash remaining bytes (includes CRC) | |
| if (next_idx_to_hash < (chunk.offset + chunk.length)) { | |
| ABSL_RETURN_IF_ERROR(hasher->Update( | |
| input, next_idx_to_hash, | |
| chunk.length - (next_idx_to_hash - chunk.offset))); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| } | |
| if (!input.Seek(chunk.offset)) { | |
| return absl::InternalError("failed to seek to chunk offset"); | |
| } | |
| uint64_t remaining_bytes = chunk.length; | |
| while (remaining_bytes > 0) { | |
| size_t chunk_size = std::min(remaining_bytes, kMaxChunkBytes); | |
| std::string chunk_data; | |
| if (!input.Read(chunk_size, chunk_data)) { | |
| return absl::DataLossError("failed to read box data"); | |
| } | |
| hasher->Update(chunk_data); | |
| remaining_bytes -= chunk_size; | |
| } | |
| return absl::OkStatus(); | |
| })); | |
| if (!insertion_offset.has_value()) { | |
| return absl::InvalidArgumentError("No IDAT or C2PA chunk found"); | |
| } | |
| result.insertion_offset = *insertion_offset; | |
| // Complete the entry for the post-C2PA boxes. | |
| result.assertion.set_hash(hasher->Digest()); | |
| return result; | |
| } | |
| } // namespace credentio |