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