blob: 3581109db96ebfd46f9b6d703b5e6dd8caecfa68 [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/bmff/writer.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/base/nullability.h"
#include "absl/functional/any_invocable.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/synchronization/notification.h"
#include "async/async_context.h"
#include "crypto/algorithms.h"
#include "formats/bmff/box_header.h"
#include "formats/bmff/embedder.h"
#include "formats/bmff/hasher.h"
#include "formats/byte_range.h"
#include "proto/bmff_based_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 "utils/byte_instruction.h"
namespace credentio {
namespace {
constexpr int64_t kCreateFreeBoxThreshold = 30;
// Scans through the source, modifying the source's values to adjust offsets to
// account for the manifest size as needed and hashing the applicable bytes.
// The method will return the offset where the manifest store should be
// inserted, the length of the manifest store, and an ordered vector of byte
// ranges of atoms which require offset adjustment before being written to the
// destination.
absl::StatusOr<std::vector<ByteInstruction>> InspectAndHash(
riegeli::Reader& source, uint64_t manifest_store_size,
const ByteRange& insertion_point, bool can_replace_manifest,
BmffBasedHashAssertion& assertion) {
if (!source.Seek(0) || source.pos() != 0) {
return absl::InvalidArgumentError("cannot seek to start of input");
}
BmffEmbedder embedder;
ABSL_ASSIGN_OR_RETURN(
std::vector<ByteInstruction> instructions,
embedder.InspectAndHash(source, manifest_store_size, insertion_point,
can_replace_manifest, assertion));
return std::move(instructions);
}
// Wraps the manifest store in an atom.
absl::StatusOr<std::string> WrapManifestStoreInAtom(
absl::string_view manifest_store) {
std::string atom;
riegeli::StringWriter writer(&atom);
ABSL_RETURN_IF_ERROR(WriteC2paBmff(manifest_store, writer));
if (!writer.Close()) {
return absl::InternalError("failed to close string writer");
}
return std::move(atom);
}
absl::Status GenerateManifestStoreInstructions(
const ByteRange& insertion_point, absl::string_view manifest_store,
std::vector<ByteInstruction>& byte_instructions,
bool client_supplied_hard_binding = false) {
// Wrap the manifest store in an atom.
ABSL_ASSIGN_OR_RETURN(std::string manifest_atom,
WrapManifestStoreInAtom(manifest_store));
if (client_supplied_hard_binding &&
insertion_point.length < manifest_atom.size()) {
return absl::ResourceExhaustedError(absl::StrCat(
"not enough space to embed C2PA manifest with client-supplied hard "
"binding (insertion_point.length=",
insertion_point.length, ", manifest_atom.size=", manifest_atom.size(),
"); skipping embedding to avoid byte shifting"));
}
if (insertion_point.length == 0) {
// No overwritable bytes, only insert.
byte_instructions.push_back(
{.operation = ByteInstruction::Operation::kInsert,
.offset = insertion_point.offset,
.bytes = std::move(manifest_atom)});
return absl::OkStatus();
}
if (insertion_point.length == manifest_atom.size()) {
// The atom fits exactly, just overwrite.
byte_instructions.push_back(
{.operation = ByteInstruction::Operation::kReplace,
.offset = insertion_point.offset,
.bytes = std::move(manifest_atom)});
return absl::OkStatus();
}
if (insertion_point.length < manifest_atom.size()) {
// Not enough space, overwrite the initial data, insert the
// remaining.
byte_instructions.push_back(
{.operation = ByteInstruction::Operation::kReplace,
.offset = insertion_point.offset,
.bytes = manifest_atom.substr(0, insertion_point.length)});
byte_instructions.push_back(
{.operation = ByteInstruction::Operation::kInsert,
.offset = insertion_point.offset + insertion_point.length,
.bytes = manifest_atom.substr(insertion_point.length)});
return absl::OkStatus();
}
// More than enough space
int64_t manifest_size = manifest_atom.size();
int64_t padding_size = insertion_point.length - manifest_size;
if (padding_size < kCreateFreeBoxThreshold) {
// Minor amount of remaining space, just pad with \0.
ABSL_ASSIGN_OR_RETURN(
manifest_atom, WrapManifestStoreInAtom(absl::StrCat(
manifest_store, std::string(padding_size, '\0'))));
byte_instructions.push_back(
{.operation = ByteInstruction::Operation::kReplace,
.offset = insertion_point.offset,
.bytes = std::move(manifest_atom)});
return absl::OkStatus();
}
// Significant amount of remaining space, create a free box.
std::string free_box;
riegeli::StringWriter<> free_box_writer(&free_box);
ABSL_RETURN_IF_ERROR(CreateFreeBox(padding_size, free_box_writer));
if (!free_box_writer.Close()) {
return absl::InternalError("failed to close string writer");
}
byte_instructions.push_back(
{.operation = ByteInstruction::Operation::kReplace,
.offset = insertion_point.offset,
.bytes = std::move(manifest_atom)});
byte_instructions.push_back(
{.operation = ByteInstruction::Operation::kReplace,
.offset = insertion_point.offset + manifest_size,
.bytes = std::move(free_box)});
return absl::OkStatus();
}
absl::StatusOr<bool> CanReplaceManifest(
const GenerateManifestStoreRequest& request) {
if (!request.has_actions()) {
return absl::InvalidArgumentError("request must include `actions`");
}
if (request.actions().inception_action_case() ==
Actions::INCEPTION_ACTION_NOT_SET) {
return absl::InvalidArgumentError(
"request must include an inception action (created or opened)");
}
return request.actions().has_opened();
}
} // namespace
absl::Status BmffWriter::Write(
GenerateManifestStoreRequest request, riegeli::Reader* absl_nonnull source,
riegeli::Writer* absl_nonnull destination,
OutputMetadata* absl_nullable output_metadata) const {
absl::Notification done;
absl::Status status;
Write(std::move(request), source, destination, output_metadata,
AsyncContext(), [&](absl::Status s) {
status = std::move(s);
done.Notify();
});
done.WaitForNotification();
return status;
}
void BmffWriter::Write(GenerateManifestStoreRequest request,
riegeli::Reader* absl_nonnull source,
riegeli::Writer* absl_nonnull destination,
OutputMetadata* absl_nullable output_metadata,
AsyncContext context,
absl::AnyInvocable<void(absl::Status) &&> cb) const {
if (!source->SupportsRandomAccess()) {
AsyncResolver<void>(std::move(context), std::move(cb))
.Return(absl::InvalidArgumentError(
"source does not support random access"));
return;
}
auto byte_instructions = std::make_shared<std::vector<ByteInstruction>>();
WriteInternal(
std::move(request), source, output_metadata, byte_instructions, context,
[resolver = AsyncResolver<void>(context, std::move(cb)), source,
byte_instructions, destination](absl::Status status) mutable {
if (!status.ok()) {
resolver.Return(std::move(status));
return;
}
status = ApplyByteInstructions(source, *byte_instructions, destination);
resolver.Return(std::move(status));
});
}
absl::Status BmffWriter::Write(
GenerateManifestStoreRequest request, riegeli::Writer* absl_nonnull asset,
OutputMetadata* absl_nullable output_metadata) const {
absl::Notification done;
absl::Status status;
Write(std::move(request), asset, output_metadata, AsyncContext(),
[&](absl::Status s) {
status = std::move(s);
done.Notify();
});
done.WaitForNotification();
return status;
}
void BmffWriter::Write(GenerateManifestStoreRequest request,
riegeli::Writer* absl_nonnull asset,
OutputMetadata* absl_nullable output_metadata,
AsyncContext context,
absl::AnyInvocable<void(absl::Status) &&> cb) const {
if (!asset->SupportsRandomAccess()) {
AsyncResolver<void>(std::move(context), std::move(cb))
.Return(
absl::InvalidArgumentError("asset does not support random access"));
return;
}
if (!asset->SupportsReadMode()) {
AsyncResolver<void>(std::move(context), std::move(cb))
.Return(absl::InvalidArgumentError("asset does not support read mode"));
return;
}
if (request.options().has_max_bytes_to_shift() &&
request.options().max_bytes_to_shift() < 0) {
AsyncResolver<void>(std::move(context), std::move(cb))
.Return(absl::InvalidArgumentError(
"max_bytes_to_shift must be non-negative"));
return;
}
auto options = request.options();
riegeli::Reader* source = asset->ReadMode(0);
auto byte_instructions = std::make_shared<std::vector<ByteInstruction>>();
WriteInternal(
std::move(request), source, output_metadata, byte_instructions, context,
[resolver = AsyncResolver<void>(context, std::move(cb)), asset,
options = std::move(options),
byte_instructions](absl::Status status) mutable {
if (!status.ok()) {
resolver.Return(std::move(status));
return;
}
std::optional<size_t> insert_idx;
auto first_insert_it = std::find_if(
byte_instructions->begin(), byte_instructions->end(),
[](const ByteInstruction& instr) {
return instr.operation == ByteInstruction::Operation::kInsert;
});
if (first_insert_it != byte_instructions->end()) {
insert_idx = first_insert_it - byte_instructions->begin();
}
auto file_size = asset->Size();
uint64_t bytes_shifted = 0;
if (file_size.has_value() && insert_idx.has_value() &&
*file_size > (*byte_instructions)[*insert_idx].offset) {
bytes_shifted = *file_size - (*byte_instructions)[*insert_idx].offset;
}
if (options.has_max_bytes_to_shift() &&
bytes_shifted >
static_cast<uint64_t>(options.max_bytes_to_shift())) {
uint64_t overwritable_bytes = 0;
if (*insert_idx > 0) {
const auto& prev_instruction =
(*byte_instructions)[*insert_idx - 1];
if (prev_instruction.operation ==
ByteInstruction::Operation::kReplace &&
prev_instruction.offset + prev_instruction.bytes.size() ==
(*byte_instructions)[*insert_idx].offset) {
overwritable_bytes = prev_instruction.bytes.size();
}
}
uint64_t manifest_atom_size =
overwritable_bytes +
(*byte_instructions)[*insert_idx].bytes.size();
resolver.Return(absl::InvalidArgumentError(absl::StrCat(
"embedding requires shifting ", bytes_shifted,
" bytes, which exceeds the limit of ",
options.max_bytes_to_shift(),
" bytes (pre-allocated manifest size: ", overwritable_bytes,
" bytes, final manifest size: ", manifest_atom_size, " bytes)")));
return;
}
if (!asset->Seek(0)) {
resolver.Return(
absl::InternalError("failed to seek to start of asset"));
return;
}
status = ApplyByteInstructions(*byte_instructions, asset);
resolver.Return(std::move(status));
});
}
void BmffWriter::WriteInternal(
GenerateManifestStoreRequest request, riegeli::Reader* absl_nonnull source,
OutputMetadata* absl_nullable output_metadata,
std::shared_ptr<std::vector<ByteInstruction>> byte_instructions,
AsyncContext context, absl::AnyInvocable<void(absl::Status) &&> cb) const {
AsyncResolver<void> resolver(context, std::move(cb));
if (source->pos() != 0) {
resolver.Return(absl::InvalidArgumentError(
"source must be positioned at the start of the file"));
return;
}
auto can_replace_manifest = CanReplaceManifest(request);
if (!can_replace_manifest.ok()) {
resolver.Return(
absl::Status(can_replace_manifest.status().code(),
absl::StrCat(can_replace_manifest.status().message(),
"; verifying request has valid actions")));
return;
}
if (request.has_hard_binding() &&
!request.hard_binding().has_bmff_based_hash()) {
resolver.Return(absl::InvalidArgumentError(
"request `hard_binding` must be a `bmff_based_hash` assertion"));
return;
}
auto insertion_point = LocateManifestInsertionPoint(*source);
if (!insertion_point.ok()) {
resolver.Return(
absl::Status(insertion_point.status().code(),
absl::StrCat(insertion_point.status().message(),
"; locating manifest insertion point")));
return;
}
if (request.has_hard_binding()) {
generator_.GenerateManifestStore(
std::move(request), context,
[output_metadata, byte_instructions, insertion_point = *insertion_point,
resolver = std::move(resolver)](
absl::StatusOr<GenerateManifestStoreResponse> response) mutable {
if (!response.ok()) {
resolver.Return(response.status());
return;
}
constexpr int64_t kMaxManifestStoreSizeBytes =
100 * 1024 * 1024; // 100 MiB
if (response->manifest_store().empty() ||
response->manifest_store().size() > kMaxManifestStoreSizeBytes) {
resolver.Return(absl::InvalidArgumentError(
absl::StrCat("invalid manifest store size: ",
response->manifest_store().size())));
return;
}
// Add the byte instructions for the manifest store atom to the
// result.
auto gen_status = GenerateManifestStoreInstructions(
insertion_point, response->manifest_store(), *byte_instructions,
/*client_supplied_hard_binding=*/true);
if (!gen_status.ok()) {
resolver.Return(std::move(gen_status));
return;
}
// Sort all byte instructions by offset.
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 = response->is_timestamped();
}
resolver.Return(absl::OkStatus());
});
return;
}
// Step 1: Estimate the manifest size
*request.mutable_hard_binding()->mutable_bmff_based_hash() =
GetBmffHashTemplate(HashAlgorithm::kSha256);
if (options_.exclusion_processor != nullptr) {
auto status = options_.exclusion_processor(*source, request);
if (!status.ok()) {
resolver.Return(std::move(status));
return;
}
}
generator_.GenerateManifestStoreTemplate(
request, context,
[this, source, request, can_replace_manifest = *can_replace_manifest,
output_metadata, byte_instructions, insertion_point = *insertion_point,
context, resolver = std::move(resolver)](
absl::StatusOr<ManifestStoreTemplate> template_response) mutable {
if (!template_response.ok()) {
resolver.Return(template_response.status());
return;
}
// Step 2: Construct the final generated manifest store request.
constexpr int64_t kMaxManifestStoreSizeBytes =
100 * 1024 * 1024; // 100 MiB
if (template_response->manifest_store_size() <= 0 ||
template_response->manifest_store_size() >
kMaxManifestStoreSizeBytes) {
resolver.Return(absl::InvalidArgumentError(
absl::StrCat("invalid manifest store template size: ",
template_response->manifest_store_size())));
return;
}
FinalizeManifestStoreRequest finalize_request;
*finalize_request.mutable_manifest_store_template() =
std::move(*template_response);
*finalize_request.mutable_hard_binding()->mutable_bmff_based_hash() =
request.hard_binding().bmff_based_hash();
// Step 3: Determine where the manifest should be inserted, where atoms
// which need adjustment are located, and compute the hash of the
// applicable bytes.
auto inspect_res = InspectAndHash(
*source,
finalize_request.manifest_store_template().manifest_store_size(),
insertion_point, can_replace_manifest,
*finalize_request.mutable_hard_binding()
->mutable_bmff_based_hash());
if (!inspect_res.ok()) {
resolver.Return(absl::Status(
inspect_res.status().code(),
absl::StrCat(inspect_res.status().message(),
"; embedding manifest store template")));
return;
}
*byte_instructions = std::move(*inspect_res);
// Step 4: Generate the finalized manifest store.
const size_t expected_manifest_store_size =
finalize_request.manifest_store_template().manifest_store_size();
generator_.FinalizeManifestStore(
std::move(finalize_request), context,
[expected_manifest_store_size, insertion_point, output_metadata,
byte_instructions, resolver = std::move(resolver)](
absl::StatusOr<FinalizeManifestStoreResponse>
response) mutable {
if (!response.ok()) {
resolver.Return(response.status());
return;
}
if (response->manifest_store().size() !=
expected_manifest_store_size) {
resolver.Return(absl::InternalError(
"manifest store size does not match template"));
return;
}
// Add the byte instructions for the manifest store atom to the
// result.
auto gen_status = GenerateManifestStoreInstructions(
insertion_point, response->manifest_store(),
*byte_instructions);
if (!gen_status.ok()) {
resolver.Return(std::move(gen_status));
return;
}
// Sort all byte instructions by offset.
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 =
response->is_timestamped();
}
resolver.Return(absl::OkStatus());
});
});
}
} // namespace credentio