blob: 076c3f0f96d5dff22b005b44145f426e42343fa3 [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/tiff/writer.h"
#include <algorithm>
#include <cstdint>
#include <limits>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/base/nullability.h"
#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 "absl/types/span.h"
#include "bindings/input_hasher.h"
#include "crypto/algorithms.h"
#include "formats/byte_range.h"
#include "formats/tiff/constants.h"
#include "formats/tiff/hasher.h"
#include "formats/tiff/reader.h"
#include "google/protobuf/repeated_ptr_field.h"
#include "proto/data_hash_assertion.pb.h"
#include "proto/generator.pb.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/string_writer.h"
#include "riegeli/bytes/writer.h"
#include "riegeli/endian/endian_writing.h"
#include "utils/byte_instruction.h"
namespace credentio {
namespace {
// The maximum number of entries per IFD in a TIFF file.
constexpr uint16_t kTiffMaxEntriesPerIfd = 60000;
template <typename T>
absl::Status WriteEndian(T value, TiffEndianness endianness,
riegeli::Writer& writer) {
const bool success = endianness == TiffEndianness::kBigEndian
? riegeli::WriteBigEndian<T>(value, writer)
: riegeli::WriteLittleEndian<T>(value, writer);
return success ? absl::OkStatus()
: writer.StatusOrAnnotate(
absl::InternalError("Failed to write value"));
}
struct ArtifactInformation {
TiffImageFileDirectory ifd_to_add_to;
TiffEndianness endianness;
};
// Scans the TIFF file for the first IFD that can be used to store the C2PA
// data. If no suitable IFD is found, a new IFD will be created at the end of
// the file. Returns an error if a C2PA entry already exists.
absl::StatusOr<ArtifactInformation> ScanForArtifactInformation(
riegeli::Reader& source, uint32_t file_size,
const GenerateManifestStoreRequest& request,
const TiffWriter::ExclusionProcessor& exclusion_processor,
DataHashAssertion* absl_nonnull assertion) {
ArtifactInformation result;
absl::flat_hash_set<uint32_t> visited_offsets;
ABSL_RETURN_IF_ERROR(IterateOverImageFileDirectories(
source, /*known_endianness=*/std::nullopt, visited_offsets,
[&result, &request, &exclusion_processor, assertion, &source,
&visited_offsets](const TiffImageFileDirectory& ifd,
TiffEndianness endianness) -> absl::StatusOr<bool> {
for (const TiffImageFileDirectoryEntry& entry : ifd.entries) {
if (entry.tag == kTiffTagC2pa) {
return absl::AlreadyExistsError("C2PA entry already exists");
}
if (exclusion_processor != nullptr) {
ABSL_ASSIGN_OR_RETURN(
std::vector<ByteRange> exclusions,
exclusion_processor(source, entry, endianness, visited_offsets,
request));
for (const ByteRange& exclusion_range : exclusions) {
auto* exclusion = assertion->add_exclusions();
exclusion->set_start(exclusion_range.offset);
exclusion->set_length(exclusion_range.length);
}
}
}
result.endianness = endianness;
result.ifd_to_add_to = ifd;
return true;
}));
if (result.ifd_to_add_to.offset_of_pointer != 4 ||
result.ifd_to_add_to.entries.size() >= kTiffMaxEntriesPerIfd) {
// Need to insert a new IFD at the end of the file
result.ifd_to_add_to = TiffImageFileDirectory{
.offset_of_pointer = static_cast<uint32_t>(source.pos()),
.offset = file_size,
};
}
return result;
}
template <typename T>
absl::Status AppendNumberToString(T value, TiffEndianness endianness,
std::string& output) {
riegeli::StringWriter writer(
&output, riegeli::StringWriterBase::Options().set_append(true));
const bool success = endianness == TiffEndianness::kBigEndian
? riegeli::WriteBigEndian<T>(value, writer)
: riegeli::WriteLittleEndian<T>(value, writer);
if (!success) {
return writer.StatusOrAnnotate(
absl::InternalError("Failed to write value"));
}
if (!writer.Close()) {
return writer.StatusOrAnnotate(
absl::InternalError("Failed to close writer"));
}
return absl::OkStatus();
}
// Adjusts the pointer to the IFD to point to the new location. If the IFD is
// being moved, zero out the original location.
absl::StatusOr<std::vector<ByteInstruction>>
CreateByteInstructionsForIfdInsertion(
const TiffImageFileDirectory& ifd_to_add_to, TiffEndianness endianness,
uint32_t file_size) {
std::vector<ByteInstruction> byte_instructions;
// Add a byte instruction to update pointer to the IFD.
byte_instructions.push_back(ByteInstruction{
.operation = ByteInstruction::Operation::kReplace,
.offset = ifd_to_add_to.offset_of_pointer,
});
ABSL_RETURN_IF_ERROR(AppendNumberToString<uint32_t>(
file_size, endianness, byte_instructions.back().bytes));
if (ifd_to_add_to.offset_of_pointer == 4) {
// C2PA data will be written to the first (and only) IFD, it will be moved
// to the end of the file, zero out the original location.
byte_instructions.push_back(ByteInstruction{
.operation = ByteInstruction::Operation::kReplace,
.offset = ifd_to_add_to.offset,
.bytes = std::string(
ByteSizeOfImageFileDirectory(ifd_to_add_to.entries.size()), '\0'),
});
}
return std::move(byte_instructions);
}
void SanitizeExclusions(
google::protobuf::RepeatedPtrField<DataRange>* exclusions) {
if (exclusions->empty()) return;
std::sort(exclusions->pointer_begin(), exclusions->pointer_end(),
[](const DataRange* a, const DataRange* b) {
if (a->start() != b->start()) return a->start() < b->start();
return a->length() < b->length();
});
google::protobuf::RepeatedPtrField<DataRange> merged;
DataRange current = (*exclusions)[0];
for (int i = 1; i < exclusions->size(); ++i) {
const DataRange& next = (*exclusions)[i];
uint64_t current_end = current.start() + current.length();
uint64_t next_end = next.start() + next.length();
if (next.start() <= current_end) {
if (next_end > current_end) {
current.set_length(next_end - current.start());
}
} else {
*merged.Add() = current;
current = next;
}
}
*merged.Add() = current;
exclusions->Swap(&merged);
}
absl::Status HashAroundExclusion(const DataRange& exclusion,
riegeli::Reader& source, InputHasher& hasher) {
if (exclusion.start() < source.pos()) {
return absl::InvalidArgumentError(
"Exclusion start is before current source position");
}
// Hash up to the start of the exclusion.
uint64_t hash_length = exclusion.start() - source.pos();
ABSL_RETURN_IF_ERROR(ReadHash(source, hash_length, hasher));
// Skip the excluded data
if (!source.Skip(exclusion.length())) {
return absl::InternalError("failed to skip data");
}
return absl::OkStatus();
}
absl::Status HashInstructionAroundExclusion(const DataRange& exclusion,
uint64_t instruction_offset,
absl::string_view instruction_bytes,
InputHasher& hasher) {
if (exclusion.start() < instruction_offset ||
exclusion.start() - instruction_offset > instruction_bytes.size()) {
return absl::InvalidArgumentError(
"Exclusion range out of bounds of byte instruction");
}
// hash until the exclusion offset
absl::string_view data_to_hash =
instruction_bytes.substr(0, exclusion.start() - instruction_offset);
hasher.Update(data_to_hash);
if (exclusion.length() >
instruction_bytes.size() - (exclusion.start() - instruction_offset)) {
return absl::InvalidArgumentError(
"Exclusion data out of bounds of byte instruction");
}
return absl::OkStatus();
}
absl::Status Hash(riegeli::Reader& source, uint64_t length_with_ifd,
absl::Span<const ByteInstruction> byte_instructions,
const DataHashAssertion& assertion, InputHasher& hasher) {
auto exclusion_iter = assertion.exclusions().begin();
for (const ByteInstruction& ins : byte_instructions) {
while (exclusion_iter->start() < ins.offset) {
ABSL_RETURN_IF_ERROR(
HashAroundExclusion(*exclusion_iter, source, hasher));
++exclusion_iter;
if (exclusion_iter == assertion.exclusions().end()) {
return absl::InternalError("ran out of exclusions");
}
}
// Next exclusion is after the start of the instruction, hash up to the
// instruction offset.
if (ins.offset < source.pos()) {
return absl::InvalidArgumentError(
"Byte instruction offset is before current source position");
}
ABSL_RETURN_IF_ERROR(ReadHash(source, ins.offset - source.pos(), hasher));
uint64_t ins_processed = 0;
while (exclusion_iter->start() < ins.offset + ins.bytes.size()) {
ABSL_RETURN_IF_ERROR(HashInstructionAroundExclusion(
*exclusion_iter, ins.offset + ins_processed,
absl::string_view(ins.bytes).substr(ins_processed), hasher));
ins_processed =
(exclusion_iter->start() + exclusion_iter->length()) - ins.offset;
++exclusion_iter;
if (exclusion_iter == assertion.exclusions().end()) {
return absl::InternalError("ran out of exclusions");
}
}
// Process remaining instruction bytes
if (ins_processed < ins.bytes.size()) {
absl::string_view data_to_hash =
absl::string_view(ins.bytes).substr(ins_processed);
hasher.Update(data_to_hash);
}
if (ins.operation == ByteInstruction::Operation::kReplace) {
if (!source.Skip(ins.bytes.size())) {
return absl::InternalError("failed to skip data");
}
}
}
return absl::OkStatus();
}
absl::Status WriteImageFileDirectory(
absl::Span<const TiffImageFileDirectoryEntry> entries,
TiffEndianness endianness, riegeli::Writer& writer) {
// Entry Count
ABSL_RETURN_IF_ERROR(
WriteEndian<uint16_t>(entries.size(), endianness, writer));
// Entries
for (const auto& entry : entries) {
ABSL_RETURN_IF_ERROR(WriteEndian<uint16_t>(entry.tag, endianness, writer));
ABSL_RETURN_IF_ERROR(WriteEndian<uint16_t>(entry.type, endianness, writer));
ABSL_RETURN_IF_ERROR(
WriteEndian<uint32_t>(entry.count, endianness, writer));
ABSL_RETURN_IF_ERROR(
WriteEndian<uint32_t>(entry.value_or_offset, endianness, writer));
}
// Next IFD Offset
ABSL_RETURN_IF_ERROR(WriteEndian<uint32_t>(0, endianness, writer));
return absl::OkStatus();
}
absl::Status WriteImageFileDirectory(
absl::Span<const TiffImageFileDirectoryEntry> entries,
TiffEndianness endianness, std::string& destination) {
riegeli::StringWriter<> writer(
&destination, riegeli::StringWriterBase::Options().set_append(true));
ABSL_RETURN_IF_ERROR(WriteImageFileDirectory(entries, endianness, writer));
if (!writer.Close()) {
return writer.status();
}
return absl::OkStatus();
}
} // namespace
absl::StatusOr<std::vector<ByteInstruction>> TiffWriter::WriteInternal(
GenerateManifestStoreRequest request, riegeli::Reader* absl_nonnull source,
OutputMetadata* absl_nullable output_metadata) const {
if (request.has_hard_binding()) {
return absl::InvalidArgumentError(
"request must not include `hard_binding` (binding is computed by "
"TiffWriter)");
}
if (!source->SupportsSize()) {
return absl::InvalidArgumentError("source must support Size()");
}
if (!source->Size().has_value()) {
return absl::InternalError("source->Size() returned no value");
}
uint64_t source_size = *source->Size();
uint64_t padding_size = source_size % 2;
uint64_t ifd_offset = source_size + padding_size;
auto hash_algorithm = HashAlgorithm::kSha256;
auto* assertion = request.mutable_hard_binding()->mutable_data_hash();
assertion->set_alg(absl::StrCat(hash_algorithm));
assertion->set_hash(std::string(32, '\0'));
ABSL_ASSIGN_OR_RETURN(
ArtifactInformation artifact_information,
ScanForArtifactInformation(*source, ifd_offset, request,
options_.exclusion_processor, assertion));
ABSL_ASSIGN_OR_RETURN(std::vector<ByteInstruction> byte_instructions,
CreateByteInstructionsForIfdInsertion(
artifact_information.ifd_to_add_to,
artifact_information.endianness, ifd_offset));
uint32_t file_size_after_ifd =
ifd_offset + ByteSizeOfImageFileDirectory(
artifact_information.ifd_to_add_to.entries.size() + 1);
artifact_information.ifd_to_add_to.entries.push_back(
TiffImageFileDirectoryEntry{
.tag = kTiffTagC2pa,
.type = kTiffTypeUndefined,
.count = 1,
.value_or_offset = file_size_after_ifd,
});
std::sort(artifact_information.ifd_to_add_to.entries.begin(),
artifact_information.ifd_to_add_to.entries.end(),
[](const TiffImageFileDirectoryEntry& a,
const TiffImageFileDirectoryEntry& b) { return a.tag < b.tag; });
{
// Holder for the C2PA IFD entry count.
auto* count_exclusion = assertion->add_exclusions();
// IFD count, IFD Entry tag, and IFD Entry type are 3 uint16_t's
uint16_t c2pa_entry_index = 0;
for (const auto& entry : artifact_information.ifd_to_add_to.entries) {
if (entry.tag == kTiffTagC2pa) {
break;
}
++c2pa_entry_index;
}
count_exclusion->set_start(ifd_offset + // Offset of relocated IFD
sizeof(uint16_t) + // final IFD count
// Number of entries before C2PA entry
(c2pa_entry_index * kTiffIfdEntrySize) +
sizeof(uint16_t) + // C2PA entry tag
sizeof(uint16_t)); // C2PA entry type
count_exclusion->set_length(sizeof(uint32_t)); // C2PA entry count
// Holder for the Manifest Store.
auto* length_exclusion = assertion->add_exclusions();
// Will be located after the IFD containing the C2PA data.
length_exclusion->set_start(file_size_after_ifd);
length_exclusion->set_length(1);
}
SanitizeExclusions(assertion->mutable_exclusions());
ABSL_ASSIGN_OR_RETURN(auto template_response,
generator_.GenerateManifestStoreTemplate(request));
if ((std::numeric_limits<uint32_t>::max() - file_size_after_ifd -
template_response.manifest_store_size()) < 0) {
return absl::InvalidArgumentError(
"Manifest store size is too large (not enough space for TIFF "
"structure)");
}
// Update the length exclusion to the actual length of the manifest store.
assertion->mutable_exclusions()->rbegin()->set_length(
template_response.manifest_store_size());
for (auto& entry : artifact_information.ifd_to_add_to.entries) {
if (entry.tag == kTiffTagC2pa) {
entry.count = template_response.manifest_store_size();
break;
}
}
{
ByteInstruction last_ifd{
.operation = ByteInstruction::Operation::kInsert,
.offset = source_size,
.bytes = std::string(padding_size, '\0'),
};
ABSL_RETURN_IF_ERROR(WriteImageFileDirectory(
artifact_information.ifd_to_add_to.entries,
artifact_information.endianness, last_ifd.bytes));
byte_instructions.push_back(last_ifd);
}
std::sort(byte_instructions.begin(), byte_instructions.end(),
[](const ByteInstruction& a, const ByteInstruction& b) {
return a.offset < b.offset;
});
source->Seek(0);
std::unique_ptr<credentio::InputHasher> hasher;
ABSL_ASSIGN_OR_RETURN(hasher, InputHasher::Create(hash_algorithm));
ABSL_RETURN_IF_ERROR(Hash(*source, file_size_after_ifd, byte_instructions,
*assertion, *hasher));
FinalizeManifestStoreRequest finalize_request;
*finalize_request.mutable_manifest_store_template() =
std::move(template_response);
*finalize_request.mutable_hard_binding()->mutable_data_hash() =
request.hard_binding().data_hash();
finalize_request.mutable_hard_binding()->mutable_data_hash()->set_hash(
hasher->Digest());
ABSL_ASSIGN_OR_RETURN(auto finalize_response,
generator_.FinalizeManifestStore(finalize_request));
byte_instructions.push_back(
ByteInstruction{.operation = ByteInstruction::Operation::kInsert,
.offset = static_cast<uint64_t>(file_size_after_ifd),
.bytes = finalize_response.manifest_store()});
std::sort(byte_instructions.begin(), byte_instructions.end(),
[](const ByteInstruction& a, const ByteInstruction& b) {
return a.offset < b.offset;
});
if (output_metadata != nullptr) {
output_metadata->timestamp_included = finalize_response.is_timestamped();
}
return byte_instructions;
}
absl::Status TiffWriter::Write(
GenerateManifestStoreRequest request, riegeli::Reader* absl_nonnull source,
riegeli::Writer* absl_nonnull destination,
OutputMetadata* absl_nullable output_metadata) const {
ABSL_ASSIGN_OR_RETURN(
auto byte_instructions,
WriteInternal(std::move(request), source, output_metadata));
return ApplyByteInstructions(source, byte_instructions, destination);
}
absl::Status TiffWriter::Write(
GenerateManifestStoreRequest request, riegeli::Writer* absl_nonnull asset,
OutputMetadata* absl_nullable output_metadata) const {
if (!asset->SupportsRandomAccess()) {
return absl::InvalidArgumentError("asset must support RandomAccess()");
}
if (!asset->SupportsReadMode()) {
return absl::InvalidArgumentError("asset must support ReadMode()");
}
riegeli::Reader* source = asset->ReadMode(0);
if (source == nullptr) {
return absl::InternalError("Failed to switch asset to ReadMode");
}
ABSL_ASSIGN_OR_RETURN(
auto byte_instructions,
WriteInternal(std::move(request), source, output_metadata));
return ApplyByteInstructions(byte_instructions, asset);
}
} // namespace credentio