blob: 8f7f7d7f2846cb442b96901a4644beb62f5a1a43 [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/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