blob: bbe8c73d511f0cdd96ca1a004e5c53aa6df0d56a [file] [edit]
// 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 "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
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;
};
absl::StatusOr<Result> ExtractManifest(riegeli::Reader& input,
int64_t end_offset) {
Result result{
.location{.offset = 0, .length = 0},
.manifest = "",
};
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);
absl::Status iteration_status = IterateOverBmffBoxes(
input,
[&input, &eof_offset,
&result](const BmffBoxHeader& header) -> absl::StatusOr<bool> {
// Validate that the end of the current box is within the end offset.
if (header.start + header.box_size > eof_offset) {
return absl::DataLossError("kUnexpectedEof; truncated BMFF box");
}
uint64_t box_end = header.start + header.box_size;
if (header.type == "mdat" || header.type == "moov") {
return false; // Terminate loop
}
if (header.type != "uuid" || header.user_type != kC2paBmffBoxUuid) {
// Skip irrelevant box.
if (!input.Seek(box_end)) {
return input.StatusOrAnnotate(
absl::InternalError("failed to seek to the end of the atom"));
}
return true; // Continue
}
// C2PA box
if (!result.manifest.empty()) {
return absl::NotFoundError("Multiple manifest stores found");
}
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") {
// Ignore non-manifest C2PA boxes.
if (!input.Seek(box_end)) {
return input.StatusOrAnnotate(
absl::InternalError("failed to seek to the end of the atom"));
}
return true; // Continue
}
// Skip the merkle offset.
if (!input.Skip(sizeof(uint64_t))) {
return input.StatusOrAnnotate(
absl::DataLossError("kUnexpectedEof; merkle offset"));
}
if (box_end < input.pos()) {
return input.StatusOrAnnotate(
absl::DataLossError("kUnexpectedEof; payload"));
}
uint64_t payload_size = box_end - input.pos();
if (payload_size > kMaxPayloadSize) {
return absl::InvalidArgumentError(absl::Substitute(
"BMFF C2PA box is too large to extract ($0 > $1)", payload_size,
kMaxPayloadSize));
}
result.location = {.offset = header.start,
.length = box_end - header.start};
if (!input.Read(payload_size, result.manifest)) {
return input.StatusOrAnnotate(
absl::InternalError("failed to read payload"));
}
if (!input.Seek(box_end)) {
return input.StatusOrAnnotate(
absl::InternalError("failed to seek to the end of the atom"));
}
return true; // Continue
});
if (!iteration_status.ok()) {
return absl::NotFoundError(
absl::StrCat("No manifest store found; ", iteration_status.message()));
}
if (result.manifest.empty()) {
return absl::NotFoundError("No manifest store found");
}
return result;
}
} // 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