blob: 63925d1025aeb37a5025e41405804eedb491c5fa [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/extractor.h"
#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/strings/substitute.h"
#include "constants/labels.h"
#include "formats/bmff/box_header.h"
#include "formats/byte_range.h"
#include "jumbf/utils.h"
#include "riegeli/bytes/reader.h"
#include "utils/riegeli.h"
namespace credentio {
namespace {
constexpr absl::string_view kZeroFlags("\x00\x00\x00", 3);
constexpr uint64_t kMaxPayloadSize = 1024 * 1024 * 10; // 10 MiB
constexpr uint64_t kNullTerminatorSize = 1;
constexpr uint64_t kMerkleOffsetSize = sizeof(uint64_t);
absl::StatusOr<std::string> ReadPurpose(riegeli::Reader& contents) {
std::string purpose;
if (!ReadNullTerminatedString(contents, 20, purpose)) {
return contents.StatusOrAnnotate(absl::DataLossError("kUnexpectedEof"));
}
return purpose;
}
struct Result {
ByteRange location;
std::string manifest;
};
// The first ftyp, moov and mdat boxes, and the C2PA manifest box, found among
// the top-level boxes of a BMFF file.
struct BmffC2paBoxLocations {
std::optional<BmffBoxHeader> ftyp_box;
std::optional<BmffBoxHeader> moov_box;
std::optional<BmffBoxHeader> mdat_box;
std::optional<BmffBoxHeader> manifest;
};
absl::StatusOr<std::string> ReadC2paBoxPayload(riegeli::Reader& input,
const BmffBoxHeader& box,
absl::string_view purpose) {
const uint64_t box_end = box.start + box.box_size;
const uint64_t payload_start = box.start + box.header_size +
box.version_and_flags_size + purpose.size() +
kNullTerminatorSize + kMerkleOffsetSize;
// Guard against boxes too small to hold the purpose and merkle offset, which
// would otherwise underflow `payload_size` below.
if (payload_start > box_end) {
return absl::NotFoundError(
"No manifest store found; kUnexpectedEof; C2PA box too small for "
"merkle offset");
}
if (!input.Seek(payload_start)) {
return input.StatusOrAnnotate(
absl::NotFoundError("No manifest store found; kUnexpectedEof; failed "
"to seek to C2PA payload"));
}
const uint64_t payload_size = box_end - payload_start;
if (payload_size > kMaxPayloadSize) {
return absl::NotFoundError(
absl::Substitute("No manifest store found; BMFF C2PA box is too large "
"to extract ($0 > $1)",
payload_size, kMaxPayloadSize));
}
std::string payload;
if (!input.Read(payload_size, payload)) {
return input.StatusOrAnnotate(absl::NotFoundError(
"No manifest store found; failed to read C2PA box payload"));
}
return payload;
}
absl::Status RecordHeadersForC2pa(riegeli::Reader& input,
const BmffBoxHeader& header,
BmffC2paBoxLocations& c2pa_boxes) {
if (header.type == "ftyp") {
c2pa_boxes.ftyp_box = header;
return absl::OkStatus();
}
if (header.type == "mdat") {
if (!c2pa_boxes.mdat_box.has_value()) {
c2pa_boxes.mdat_box = header;
}
return absl::OkStatus();
}
if (header.type == "moov") {
if (!c2pa_boxes.moov_box.has_value()) {
c2pa_boxes.moov_box = header;
}
return absl::OkStatus();
}
if (header.type == "uuid" && header.user_type == kC2paBmffBoxUuid) {
if (header.version != 0) {
return absl::InvalidArgumentError(absl::Substitute(
"unsupported C2PA box version ($0)", header.version));
}
if (header.flags != kZeroFlags) {
return absl::InvalidArgumentError(
absl::Substitute("unsupported C2PA box flags ($0)",
absl::BytesToHexString(header.flags)));
}
ABSL_ASSIGN_OR_RETURN(auto purpose, ReadPurpose(input));
if (purpose == "manifest") {
if (c2pa_boxes.manifest.has_value()) {
return absl::NotFoundError("multiple manifest boxes found");
}
c2pa_boxes.manifest = header;
return absl::OkStatus();
}
if (purpose == "original" || purpose == "update") {
return absl::InvalidArgumentError(
absl::Substitute("unsupported C2PA metadata purpose ($0)", purpose));
}
return absl::OkStatus();
}
return absl::OkStatus();
}
absl::StatusOr<Result> ValidateLocationAndExtractManifest(
riegeli::Reader& input, const BmffC2paBoxLocations& c2pa_boxes) {
if (!c2pa_boxes.manifest.has_value()) {
return absl::NotFoundError("No manifest store found");
}
// Box must be after ftyp.
if (c2pa_boxes.ftyp_box.has_value() &&
c2pa_boxes.ftyp_box->start > c2pa_boxes.manifest->start) {
return absl::NotFoundError(
"No manifest store found; manifest resides before ftyp");
}
// Box must be before moov.
if (c2pa_boxes.moov_box.has_value() &&
c2pa_boxes.moov_box->start < c2pa_boxes.manifest->start) {
return absl::NotFoundError(
"No manifest store found; manifest resides after moov");
}
// Box must be before mdat.
if (c2pa_boxes.mdat_box.has_value() &&
c2pa_boxes.mdat_box->start < c2pa_boxes.manifest->start) {
return absl::NotFoundError(
"No manifest store found; manifest resides after mdat");
}
ABSL_ASSIGN_OR_RETURN(
auto manifest,
ReadC2paBoxPayload(input, *c2pa_boxes.manifest, "manifest"));
if (manifest.empty()) {
return absl::NotFoundError(
"No manifest store found; manifest store is empty");
}
return Result{.location = {.offset = c2pa_boxes.manifest->start,
.length = c2pa_boxes.manifest->box_size},
.manifest = std::move(manifest)};
}
absl::StatusOr<Result> ExtractManifest(riegeli::Reader& input,
int64_t end_offset) {
if (!input.SupportsSize() || !input.Size().has_value()) {
return absl::InvalidArgumentError(
"manifest store not embedded: reader size cannot be determined");
}
uint64_t eof_offset =
end_offset < 0 ? *input.Size() : static_cast<uint64_t>(end_offset);
BmffC2paBoxLocations c2pa_boxes;
auto status = IterateOverBmffBoxes(
input,
[&input, eof_offset,
&c2pa_boxes](const BmffBoxHeader& header) -> absl::StatusOr<bool> {
uint64_t box_end = header.start + header.box_size;
if (box_end > eof_offset) {
return absl::DataLossError("kUnexpectedEof; truncated BMFF box");
}
ABSL_RETURN_IF_ERROR(RecordHeadersForC2pa(input, header, c2pa_boxes));
if (box_end == eof_offset) {
return false; // Terminate loop
}
if (!input.Seek(box_end)) {
return input.StatusOrAnnotate(
absl::InternalError("failed to seek to the end of the atom"));
}
return true; // Continue
});
if (!status.ok()) {
return absl::NotFoundError(
absl::StrCat("No manifest store found; ", status.message()));
}
return ValidateLocationAndExtractManifest(input, c2pa_boxes);
}
} // namespace
absl::StatusOr<std::string> BmffExtractor::ExtractManifestStore(
riegeli::Reader& input) const {
ABSL_ASSIGN_OR_RETURN(auto result, ExtractManifest(input, /*end_offset=*/-1));
return result.manifest;
}
absl::StatusOr<std::optional<ByteRange>>
BmffExtractor::ExtractManifestStoreLocation(riegeli::Reader& input,
ExtractOptions options) const {
auto result = ExtractManifest(input, options.end_offset);
if (!result.ok()) {
if (options.requires_c2pa) {
return result.status();
}
return std::nullopt;
}
return (*result).location;
}
bool BmffExtractor::MightBeC2paManifestStore(absl::string_view payload) const {
return jumbf::HasDescriptionBoxMatching(payload, kManifestStoreUuid,
kMinimumJumbfDescriptionToggles,
kManifestStoreLabel)
.value_or(false);
}
} // namespace credentio