| // 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/box_header.h" |
| |
| #include <cstdint> |
| #include <limits> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/container/flat_hash_map.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/string_view.h" |
| #include "constants/labels.h" |
| #include "formats/bmff/constants.h" |
| #include "formats/bmff/xpath.h" |
| #include "formats/byte_range.h" |
| #include "riegeli/bytes/reader.h" |
| #include "riegeli/bytes/writer.h" |
| #include "riegeli/endian/endian_reading.h" |
| #include "riegeli/endian/endian_writing.h" |
| |
| namespace credentio { |
| |
| namespace { |
| |
| // Returns true if the given type is "uuid". |
| inline bool IsUuid(absl::string_view type) { return type == "uuid"; } |
| |
| // ISOBMFF and QTFF define "meta" boxes differently. ISOBMFF defines it |
| // as a full box with flags and version (see 4.2 'Object Structure' in ISO/IEC |
| // 14496-12:2005), while QuickTime does not have such a full box structure. |
| // Here, we check the 8 bytes and see if it is the "hdlr" box, as defined by |
| // https://developer.apple.com/documentation/quicktime-file-format/metadata_atom. |
| bool IsQuickTimeMetaBox(const BmffBoxHeader& header, riegeli::Reader& input) { |
| if (header.type != "meta") { |
| return false; |
| } |
| // The next 8 bytes can be either: |
| // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom] |
| // (qt) [4 byte size of next atom ][4 byte hdlr atom type ] |
| auto pin = input.pos(); |
| bool success = input.Seek(pin + 4); |
| std::string type; |
| success &= input.Read(4, type); |
| success &= input.Seek(pin); |
| return success && type == "hdlr"; |
| }; |
| |
| constexpr int kAtomTypeSize = 4; |
| constexpr int kVersionFlagsSize = 4; |
| constexpr int kSizeOf32BitSize = 4; |
| constexpr int kSizeOf64BitSize = 8; |
| |
| bool ShouldWriteSize64Bits(uint64_t box_size) { |
| return box_size > std::numeric_limits<uint32_t>::max(); |
| } |
| |
| bool ShouldWriteUserType(const BmffBox& box) { return IsUuid(box.type); } |
| |
| bool ShouldWriteVersionFlags(const BmffBox& box) { |
| return IsBmffFullBox(box.type) || IsUuid(box.type); |
| } |
| |
| constexpr int kMaxBmffDepth = 64; |
| |
| void SetInsertionPoint(std::optional<ByteRange> candidate, int64_t c2pa_offset, |
| std::optional<ByteRange>& result) { |
| if (!candidate.has_value()) { |
| // No candidate |
| return; |
| } |
| |
| if (!result.has_value()) { |
| // No existing |
| result = candidate; |
| return; |
| } |
| |
| if (c2pa_offset == -1) { |
| // No C2PA range found yet, use the larger range |
| if (result->length < candidate->length) { |
| // Existing is smaller and no C2PA range to account for |
| result = candidate; |
| } |
| return; |
| } |
| |
| if (c2pa_offset >= candidate->offset && |
| c2pa_offset <= candidate->offset + candidate->length) { |
| // C2PA is contained within the candidate, use it. |
| result = candidate; |
| } |
| } |
| |
| absl::StatusOr<bool> CanOverwrite(const BmffBoxHeader& box, |
| riegeli::Reader& reader) { |
| if (box.type == "free") { |
| // Free boxes are always overwritable. |
| return true; |
| } |
| if (box.type != "uuid" || box.user_type != kC2paBmffBoxUuid) { |
| // The only other overwritable box is a C2PA uuid box. |
| return false; |
| } |
| |
| std::string purpose; |
| if (!reader.Read(8, purpose)) { |
| return reader.StatusOrAnnotate( |
| absl::DataLossError("kUnexpectedEof; purpose")); |
| } |
| |
| if (absl::StartsWith(purpose, kBmffC2paBoxPurposeMerkle)) { |
| // Merkle boxes are not overwritable, we replace them with a free box. |
| return false; |
| } |
| if (absl::StartsWith(purpose, kBmffC2paBoxPurposeUpdate)) { |
| // Update boxes are not overwritable, we remove them. |
| return false; |
| } |
| |
| // Original and Manifest boxes are overwritable. |
| return true; |
| } |
| |
| absl::StatusOr<bool> IterateOverBmffBoxesInternal(XPath* parent, |
| riegeli::Reader& reader, |
| uint64_t end_offset, |
| BmffBoxProcessor processor, |
| int depth = 0) { |
| if (depth > kMaxBmffDepth) { |
| return absl::InvalidArgumentError("Too many nested BMFF boxes"); |
| } |
| absl::flat_hash_map<std::string, int> count_by_type; |
| while (reader.pos() < (end_offset - kBmffMinSizeofBoxHeader)) { |
| BmffBoxHeader box; |
| if (auto box_or = ReadBmffBoxHeader(reader); box_or.ok()) { |
| box = std::move(box_or.value()); |
| } else { |
| return box_or.status(); |
| } |
| |
| if (box.start > (std::numeric_limits<uint64_t>::max() - box.box_size)) { |
| return reader.StatusOrAnnotate( |
| absl::DataLossError("Final box offset exceeds uint64_t::max")); |
| } |
| uint64_t box_end_offset = box.start + box.box_size; |
| |
| // If the box extends beyond the end of the input, it's invalid. |
| if (box_end_offset > end_offset) { |
| if (parent == nullptr) { |
| return reader.StatusOrAnnotate( |
| absl::DataLossError("truncated BMFF box")); |
| } |
| return reader.StatusOrAnnotate( |
| absl::DataLossError("sub-box extends beyond the parent")); |
| } |
| |
| XPath xpath(parent, box.type); |
| xpath.SetPosition(++count_by_type[box.type]); |
| box.xpath = xpath.ToString(); |
| |
| // If it fails during processing, we can't continue. |
| bool continue_processing; |
| if (auto result = processor(box); result.ok()) { |
| continue_processing = *result; |
| } else { |
| return result.status(); |
| } |
| if (!continue_processing) { |
| return false; |
| } |
| |
| if (reader.pos() < box_end_offset && box.IsContainerBox()) { |
| if (auto sub_continue_or = IterateOverBmffBoxesInternal( |
| &xpath, reader, box_end_offset, processor, depth + 1); |
| sub_continue_or.ok()) { |
| continue_processing = *sub_continue_or; |
| } else { |
| return sub_continue_or.status(); |
| } |
| if (!continue_processing) { |
| return false; |
| } |
| } |
| |
| reader.Seek(box_end_offset); |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| bool BmffBoxHeader::IsContainerBox() const { |
| // This list mirrors the C2PA SDK's implementation: |
| // https://github.com/contentauth/c2pa-rs/blob/main/sdk/src/asset_handlers/bmff_io.rs#L982. |
| return type == "moov" || type == "trak" || type == "mdia" || type == "minf" || |
| type == "stbl" || type == "moof" || type == "traf" || type == "edts" || |
| type == "udta" || type == "dinf" || type == "tref" || type == "treg" || |
| type == "mvex" || type == "mfra" || type == "meta" || type == "schi"; |
| } |
| |
| absl::StatusOr<BmffBoxHeader> ReadBmffBoxHeader(riegeli::Reader& input) { |
| BmffBoxHeader header; |
| |
| header.start = input.pos(); |
| |
| uint32_t box_size32; |
| if (!riegeli::ReadBigEndian<uint32_t>(input, box_size32)) { |
| return input.StatusOrAnnotate( |
| absl::DataLossError("kUnexpectedEof; box_size32")); |
| } |
| if (!input.Read(4, header.type)) { |
| return input.StatusOrAnnotate(absl::DataLossError("kUnexpectedEof; type")); |
| } |
| header.header_size = 8; |
| |
| if (box_size32 == 1) { |
| if (!riegeli::ReadBigEndian<uint64_t>(input, header.box_size)) { |
| return input.StatusOrAnnotate( |
| absl::DataLossError("kUnexpectedEof; box_size64")); |
| } |
| header.header_size = 16; |
| } else { |
| header.box_size = box_size32; |
| } |
| |
| if (header.box_size < kBmffMinSizeofBoxHeader) { |
| // Unsupported box size. |
| return input.StatusOrAnnotate( |
| absl::DataLossError("kInvalidData; unsupported box size")); |
| } |
| |
| if (IsUuid(header.type)) { |
| if (!input.Read(kBmffUserTypeSize, header.user_type)) { |
| return input.StatusOrAnnotate( |
| absl::DataLossError("kUnexpectedEof; user_type")); |
| } |
| header.header_size += kBmffUserTypeSize; |
| } |
| if ((IsBmffFullBox(header.type) || |
| (IsUuid(header.type) && header.user_type == kC2paBmffBoxUuid)) && |
| !IsQuickTimeMetaBox(header, input)) { |
| header.version_and_flags_size = 4; |
| if (!input.ReadByte(header.version)) { |
| return input.StatusOrAnnotate( |
| absl::DataLossError("kUnexpectedEof; version")); |
| } |
| if (!input.Read(3, header.flags)) { |
| return input.StatusOrAnnotate( |
| absl::DataLossError("kUnexpectedEof; flags")); |
| } |
| } |
| if (header.box_size < header.header_size + header.version_and_flags_size) { |
| return input.StatusOrAnnotate( |
| absl::DataLossError("kInvalidData; box size too small for header")); |
| } |
| return header; |
| } |
| |
| int64_t BmffBoxHeaderSize(const BmffBox& box) { |
| uint64_t header_size = kSizeOf32BitSize + kAtomTypeSize; |
| if (ShouldWriteUserType(box)) { |
| header_size += kBmffUserTypeSize; |
| } |
| if (ShouldWriteVersionFlags(box)) { |
| header_size += kVersionFlagsSize; |
| } |
| if (ShouldWriteSize64Bits(header_size + box.data_size)) { |
| header_size += kSizeOf64BitSize; |
| } |
| return header_size; |
| } |
| |
| absl::StatusOr<int64_t> WriteBmffBoxHeader(const BmffBox& box, |
| riegeli::Writer& destination) { |
| bool success = true; |
| |
| uint64_t header_size = BmffBoxHeaderSize(box); |
| uint64_t box_size = header_size + box.data_size; |
| bool should_write_size_64bits = ShouldWriteSize64Bits(box_size); |
| uint32_t box_size32 = should_write_size_64bits ? 1 : box_size; |
| |
| success &= riegeli::WriteBigEndian<uint32_t>(box_size32, destination); |
| success &= box.type.size() == kAtomTypeSize && destination.Write(box.type); |
| if (should_write_size_64bits) { |
| success &= riegeli::WriteBigEndian<uint64_t>(box_size, destination); |
| } |
| if (ShouldWriteUserType(box)) { |
| success &= box.user_type.size() == kBmffUserTypeSize && |
| destination.Write(box.user_type); |
| } |
| if (ShouldWriteVersionFlags(box)) { |
| success &= destination.WriteByte(box.version); |
| success &= box.flags.size() == (kVersionFlagsSize - 1) && |
| destination.Write(box.flags); |
| } |
| if (!success) { |
| return absl::InternalError("failed to write box header"); |
| } |
| return header_size; |
| } |
| |
| absl::StatusOr<std::vector<BmffBoxHeader>> ReadBmffBoxHeaders( |
| riegeli::Reader& input) { |
| std::vector<BmffBoxHeader> result; |
| if (!input.Seek(0) || input.pos() != 0) { |
| return input.StatusOrAnnotate( |
| absl::DataLossError("failed to seek to start of input")); |
| } |
| auto result_status = |
| IterateOverBmffBoxes(input, [&result](const BmffBoxHeader& box) { |
| result.push_back(std::move(box)); |
| return true; |
| }); |
| if (result_status.ok()) { |
| return std::move(result); |
| } |
| return result_status; |
| } |
| |
| absl::Status IterateOverBmffBoxes(riegeli::Reader& reader, |
| BmffBoxProcessor processor) { |
| if (!reader.SupportsSize() || !reader.Size().has_value()) { |
| return absl::InvalidArgumentError( |
| "manifest store not embedded: reader size cannot be determined"); |
| } |
| if ((*reader.Size() - reader.pos()) == 0) { |
| return absl::OkStatus(); |
| } |
| |
| return IterateOverBmffBoxesInternal(nullptr, reader, *reader.Size(), |
| processor) |
| .status(); |
| } |
| |
| absl::StatusOr<ByteRange> LocateManifestInsertionPoint( |
| riegeli::Reader& reader) { |
| std::optional<ByteRange> result = std::nullopt; |
| std::optional<ByteRange> range = std::nullopt; |
| |
| int64_t c2pa_offset = -1; |
| std::string last_atom_type_processed = ""; |
| auto iterate_status = IterateOverBmffBoxes( |
| reader, |
| [&reader, &range, &c2pa_offset, &last_atom_type_processed, |
| &result](const BmffBoxHeader& box) -> absl::StatusOr<bool> { |
| ABSL_ASSIGN_OR_RETURN(bool overwritable, CanOverwrite(box, reader)); |
| |
| // We only care about the top level boxes, so skip all sub-boxes. |
| reader.Seek(box.start + box.box_size); |
| last_atom_type_processed = box.type; |
| |
| if (!overwritable) { |
| SetInsertionPoint(range, c2pa_offset, result); |
| range = std::nullopt; |
| |
| if (box.type == "mdat" || box.type == "moov") { |
| SetInsertionPoint(ByteRange{.offset = box.start, .length = 0}, |
| c2pa_offset, result); |
| return false; // Terminate loop |
| } |
| return true; // Continue |
| } |
| |
| if (box.type == "uuid") { |
| c2pa_offset = box.start; |
| } |
| |
| if (!range.has_value()) { |
| range = ByteRange{.offset = box.start, .length = 0}; |
| } |
| range->length += box.box_size; |
| |
| return true; // Continue |
| }); |
| ABSL_RETURN_IF_ERROR(iterate_status); |
| if (last_atom_type_processed != "mdat" && |
| last_atom_type_processed != "moov") { |
| return absl::InvalidArgumentError("no mdat/moov box found"); |
| } |
| SetInsertionPoint(range, c2pa_offset, result); |
| return result.value_or(ByteRange{.offset = 0, .length = 0}); |
| } |
| |
| } // namespace credentio |