blob: d1d34827b0381b2cb2b8467e79fba301a93067a8 [file]
// 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