| // 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/embedder.h" | |
| #include <algorithm> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <limits> | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/algorithm/container.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/match.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/types/span.h" | |
| #include "bindings/input_hasher.h" | |
| #include "constants/labels.h" | |
| #include "formats/bmff/box_header.h" | |
| #include "formats/bmff/box_matcher.h" | |
| #include "formats/bmff/offset_adjustment.h" | |
| #include "formats/byte_range.h" | |
| #include "proto/bmff_based_hash_assertion.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" | |
| #include "utils/byte_writers.h" | |
| namespace credentio { | |
| namespace { | |
| std::string Uint64ToBytes(uint64_t value) { | |
| std::vector<uint8_t> bytes; | |
| WriteUint64NetworkOrder(value, &bytes); | |
| return std::string(bytes.begin(), bytes.end()); | |
| } | |
| constexpr absl::string_view kZeroFlags("\x00\x00\x00", 3); | |
| constexpr absl::string_view kC2paBoxPurpose = "manifest"; | |
| constexpr uint64_t kMaxChunkBytes = 1024 * 1024; // 1 MiB | |
| BmffBox C2paBmffBoxHeader(uint64_t manifest_store_size) { | |
| return BmffBox{ | |
| .type = "uuid", | |
| .user_type = std::string(kC2paBmffBoxUuid), | |
| .flags = std::string(kZeroFlags), | |
| // Nul-terminated purpose + 8-byte merkle offset + manifest_store. | |
| .data_size = kC2paBoxPurpose.size() + 1 + 8 + manifest_store_size, | |
| }; | |
| } | |
| bool BoxHasOffsets(absl::string_view type) { | |
| return type == "stco" || type == "saio" || type == "co64" || type == "iloc"; | |
| } | |
| // Adjusts the offsets in the given box by adding the size of the manifest | |
| // store and writes it to the destination. | |
| absl::Status FixOffsets(const BmffBoxHeader& box_header, | |
| riegeli::Reader& source, uint64_t byte_adjustment, | |
| riegeli::Writer& destination) { | |
| ABSL_ASSIGN_OR_RETURN( | |
| int64_t written_header_size, | |
| WriteBmffBoxHeader( | |
| BmffBox{ | |
| .type = box_header.type, | |
| .user_type = box_header.user_type, | |
| .version = box_header.version, | |
| .flags = box_header.flags, | |
| // The box_size includes the header size, the write method expects | |
| // only the data size. | |
| .data_size = box_header.box_size - box_header.header_size - | |
| box_header.version_and_flags_size, | |
| }, | |
| destination)); | |
| // Since we didn't read the header from the source, we need to skip over those | |
| // bytes. | |
| if (!source.Seek(box_header.start + written_header_size)) { | |
| return absl::DataLossError("failed to skip header size"); | |
| } | |
| if (box_header.type == "iloc") { | |
| ABSL_ASSIGN_OR_RETURN(IlocBox box, ParseIloc(source, box_header)); | |
| AdjustOffsets(box, byte_adjustment); | |
| return SerializeIloc(box, box_header, destination); | |
| } | |
| if (box_header.type == "stco") { | |
| ABSL_ASSIGN_OR_RETURN(StcoBox box, ParseStco(source)); | |
| AdjustOffsets(box, byte_adjustment); | |
| return SerializeStco(box, destination); | |
| } | |
| if (box_header.type == "co64") { | |
| ABSL_ASSIGN_OR_RETURN(Co64Box box, ParseCo64(source)); | |
| AdjustOffsets(box, byte_adjustment); | |
| return SerializeCo64(box, destination); | |
| } | |
| if (box_header.type == "saio") { | |
| ABSL_ASSIGN_OR_RETURN(SaioBox box, ParseSaio(source, box_header)); | |
| AdjustOffsets(box, byte_adjustment); | |
| return SerializeSaio(box, box_header, destination); | |
| } | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Unexpected box type for offset fixing: ", box_header.type)); | |
| } | |
| absl::Status ProcessC2paAtom(const BmffBoxHeader& box, riegeli::Reader& source, | |
| const ByteRange& insertion_point, | |
| std::vector<ByteInstruction>& byte_instructions) { | |
| if (box.start >= insertion_point.offset && | |
| box.start + box.box_size <= | |
| insertion_point.offset + insertion_point.length) { | |
| // This box is completely within the insertion point, it will be handled by | |
| // the instruction which inserts the manifest store. | |
| return absl::OkStatus(); | |
| } | |
| // Just convert any other C2PA atom outside of the insertion point to a free | |
| // box. | |
| std::string free_box; | |
| riegeli::StringWriter<> free_box_writer(&free_box); | |
| ABSL_RETURN_IF_ERROR(CreateFreeBox(box.box_size, free_box_writer)); | |
| if (!free_box_writer.Close()) { | |
| return absl::InternalError("failed to close string writer"); | |
| } | |
| byte_instructions.push_back( | |
| ByteInstruction{.operation = ByteInstruction::Operation::kReplace, | |
| .offset = box.start, | |
| .bytes = std::move(free_box)}); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status ProcessBox(const BmffBoxHeader& box, riegeli::Reader& source, | |
| bool can_replace_manifest, | |
| absl::Span<const BMFFBoxMatcher> exclusion_matchers, | |
| const ByteRange& insertion_point, | |
| uint64_t bytes_needed_to_add, | |
| std::vector<ByteInstruction>& byte_instructions, | |
| std::vector<std::string>& excluded_atoms, | |
| InputHasher& hasher) { | |
| if (box.type == "uuid" && box.user_type == kC2paBmffBoxUuid) { | |
| if (!can_replace_manifest) { | |
| return absl::AlreadyExistsError( | |
| "C2PA chunk already exists, but no `opened` action was provided"); | |
| } | |
| ABSL_RETURN_IF_ERROR( | |
| ProcessC2paAtom(box, source, insertion_point, byte_instructions)); | |
| // Skip this box | |
| source.Seek(box.start + box.box_size); | |
| return absl::OkStatus(); | |
| } | |
| bool added_byte_instruction = false; | |
| if (BoxHasOffsets(box.type) && bytes_needed_to_add > 0) { | |
| // Move the current position to this box. | |
| if (!source.Seek(box.start) || source.pos() != box.start) { | |
| return absl::InternalError("failed to seek to box start"); | |
| } | |
| auto byte_instruction = ByteInstruction{ | |
| .operation = ByteInstruction::Operation::kReplace, .offset = box.start}; | |
| riegeli::StringWriter<> writer(&byte_instruction.bytes); | |
| ABSL_RETURN_IF_ERROR(FixOffsets(box, source, bytes_needed_to_add, writer)); | |
| if (!writer.Close()) { | |
| return absl::InternalError("failed to close string writer"); | |
| } | |
| byte_instructions.push_back(std::move(byte_instruction)); | |
| added_byte_instruction = true; | |
| } | |
| if (absl::c_any_of(excluded_atoms, [&box](absl::string_view excluded_atom) { | |
| return absl::StartsWith(box.xpath, excluded_atom); | |
| })) { | |
| // Parent atom is excluded, so skip this box. | |
| return absl::OkStatus(); | |
| } | |
| for (const auto& matcher : exclusion_matchers) { | |
| if (matcher.Matches(box)) { | |
| excluded_atoms.push_back(box.xpath); | |
| if (matcher.IsMatchingEntireBox()) { | |
| // The entire box is excluded, so we can skip it. | |
| return absl::OkStatus(); | |
| } | |
| // Note: Excluding parts of a box is not supported, as best practice would | |
| // see no additional exclusions aside from required exclusions. | |
| // See: | |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_exclusion_list_requirements | |
| return absl::InvalidArgumentError( | |
| "partial exclusions of atom are not supported"); | |
| } | |
| } | |
| if (box.IsRootBox()) { | |
| int64_t adjusted_box_start = box.start >= insertion_point.offset | |
| ? box.start + bytes_needed_to_add | |
| : box.start; | |
| hasher.Update(Uint64ToBytes(adjusted_box_start)); | |
| } | |
| if (added_byte_instruction) { | |
| hasher.Update(byte_instructions.back().bytes); | |
| return absl::OkStatus(); | |
| } | |
| if (box.IsContainerBox()) { | |
| // The box is a container, so we need to hash the header then continue for | |
| // the data. | |
| int64_t data_start = source.pos(); | |
| source.Seek(box.start); | |
| std::string header_data; | |
| source.Read(data_start - box.start, header_data); | |
| hasher.Update(header_data); | |
| return absl::OkStatus(); | |
| } | |
| // not a container, not excluded, and no offsets, so just hash the whole box | |
| source.Seek(box.start); | |
| uint64_t remaining_bytes = box.box_size; | |
| while (remaining_bytes > 0) { | |
| size_t chunk_size = std::min(remaining_bytes, kMaxChunkBytes); | |
| std::string chunk_data; | |
| if (!source.Read(chunk_size, chunk_data)) { | |
| return absl::DataLossError("failed to read box data"); | |
| } | |
| hasher.Update(chunk_data); | |
| remaining_bytes -= chunk_size; | |
| } | |
| return absl::OkStatus(); | |
| } | |
| } // namespace | |
| absl::StatusOr<std::vector<ByteInstruction>> BmffEmbedder::InspectAndHash( | |
| riegeli::Reader& source, uint64_t manifest_store_size, | |
| const ByteRange& insertion_point, bool can_replace_manifest, | |
| BmffBasedHashAssertion& assertion) const { | |
| uint64_t c2pa_atom_size = CalculateC2paSizeBmff(manifest_store_size); | |
| uint64_t bytes_needed_to_add = insertion_point.length >= c2pa_atom_size | |
| ? 0 | |
| : c2pa_atom_size - insertion_point.length; | |
| std::vector<BMFFBoxMatcher> exclusion_matchers; | |
| for (const auto& bmff_range : assertion.exclusions()) { | |
| exclusion_matchers.push_back(BMFFBoxMatcher(bmff_range, &source)); | |
| } | |
| source.Seek(0); | |
| ABSL_ASSIGN_OR_RETURN(auto hasher, InputHasher::Create(assertion.alg())); | |
| if (!source.SupportsSize()) { | |
| return absl::InvalidArgumentError( | |
| "manifest store not embedded: source does not support size"); | |
| } | |
| std::vector<ByteInstruction> byte_instructions; | |
| std::vector<std::string> excluded_atoms; | |
| ABSL_RETURN_IF_ERROR(IterateOverBmffBoxes( | |
| source, | |
| [&source, &insertion_point, &can_replace_manifest, &byte_instructions, | |
| &exclusion_matchers, &excluded_atoms, &hasher, | |
| &bytes_needed_to_add](const BmffBoxHeader& box) -> absl::StatusOr<bool> { | |
| ABSL_RETURN_IF_ERROR(ProcessBox(box, source, can_replace_manifest, | |
| exclusion_matchers, insertion_point, | |
| bytes_needed_to_add, byte_instructions, | |
| excluded_atoms, *hasher)); | |
| return true; | |
| })); | |
| assertion.set_hash(hasher->Digest()); | |
| return byte_instructions; | |
| } | |
| int64_t CalculateC2paSizeBmff(uint64_t manifest_store_size) { | |
| BmffBox box = C2paBmffBoxHeader(manifest_store_size); | |
| return BmffBoxHeaderSize(box) + box.data_size; | |
| } | |
| absl::Status WriteC2paBmff(absl::string_view manifest_store, | |
| riegeli::Writer& output) { | |
| bool failed = false; | |
| ABSL_RETURN_IF_ERROR( | |
| WriteBmffBoxHeader(C2paBmffBoxHeader(manifest_store.size()), output) | |
| .status()); | |
| failed |= !output.Write(kC2paBoxPurpose); | |
| failed |= !output.WriteByte(0); | |
| failed |= !riegeli::WriteBigEndian<uint64_t>(0, output); | |
| failed |= !output.Write(manifest_store); | |
| if (failed) { | |
| return absl::InternalError("manifest store not embedded: write failed"); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| absl::Status CreateFreeBox(uint64_t size, riegeli::Writer& output) { | |
| if (size < 8) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Invalid free box size: ", size)); | |
| } | |
| uint64_t data_size = 0; | |
| if (size > std::numeric_limits<uint32_t>::max()) { | |
| data_size = size - 16; | |
| } else { | |
| data_size = size - 8; | |
| } | |
| ABSL_RETURN_IF_ERROR( | |
| WriteBmffBoxHeader(BmffBox{.type = "free", .data_size = data_size}, | |
| output) | |
| .status()); | |
| uint64_t remaining_padding = data_size; | |
| constexpr size_t kChunkSize = 64 * 1024; // 64 KiB | |
| std::string zero_chunk(kChunkSize, '\0'); | |
| while (remaining_padding > 0) { | |
| size_t write_size = std::min<uint64_t>(remaining_padding, kChunkSize); | |
| if (!output.Write(absl::string_view(zero_chunk.data(), write_size))) { | |
| return absl::InternalError("manifest store not embedded: write failed"); | |
| } | |
| remaining_padding -= write_size; | |
| } | |
| return absl::OkStatus(); | |
| } | |
| } // namespace credentio |