blob: ccaabaa85f376759444a87152e0dbe05858870cb [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.
//
// RIFF format information:
// https://www.loc.gov/preservation/digital/formats/fdd/fdd000025.shtml
// https://johnloomis.org/cpe102/asgn/asgn1/riff.html
// https://www.tactilemedia.com/info/MCI_Control_Info.html
// C2PA embedding:
// https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_embedding_manifests_into_riff_based_assets
#include "formats/riff/extractor.h"
#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "constants/labels.h"
#include "formats/asset_box.h"
#include "formats/byte_range.h"
#include "formats/riff/constants.h"
#include "formats/riff/reader.h"
#include "jumbf/utils.h"
#include "riegeli/bytes/reader.h"
namespace credentio {
namespace {
absl::StatusOr<std::vector<AssetBox>> ExtractBoxesInternal(
riegeli::Reader& reader, int64_t end_offset) {
std::vector<AssetBox> result;
bool first = true;
std::vector<uint64_t> container_ends;
auto status = IterateOverRiffChunks(
reader,
[&result, &first, &container_ends,
&reader](const RiffChunk& chunk) -> absl::StatusOr<bool> {
while (!container_ends.empty() &&
chunk.offset >= container_ends.back()) {
container_ends.pop_back();
}
int depth = container_ends.size();
if (depth == 0) {
if (first) {
if (chunk.id != kRiffChunkIdRiff) {
return absl::InvalidArgumentError("RIFF chunk not found");
}
first = false;
}
result.push_back({
.identifier = chunk.id,
.byte_range = {.offset = chunk.offset, .length = 12},
});
if (chunk.HasSubchunks()) {
container_ends.push_back(chunk.offset + chunk.length);
}
return true;
}
if (depth == 1) {
if (chunk.id == kRiffChunkIdC2pa && IsRiffChunkTooLarge(chunk)) {
return absl::InvalidArgumentError(
"RIFF C2PA chunk is too large to extract");
}
result.push_back({
.identifier = chunk.id,
.byte_range = {.offset = chunk.offset, .length = chunk.length},
});
if (chunk.HasSubchunks()) {
reader.Seek(chunk.offset + chunk.length);
}
return true;
}
return absl::InternalError("Unexpected depth in RIFF parser");
},
end_offset);
ABSL_RETURN_IF_ERROR(status);
return result;
}
// Returns the C2PA subchunk within the RIFF chunk, if any.
absl::StatusOr<std::optional<RiffChunk>> FindC2paChunk(riegeli::Reader& reader,
int64_t end_offset) {
std::optional<RiffChunk> c2pa_chunk;
RiffChunk riff_chunk;
bool first = true;
auto status = IterateOverRiffChunks(
reader,
[&c2pa_chunk, &first,
&riff_chunk](const RiffChunk& chunk) -> absl::StatusOr<bool> {
if (first) {
if (chunk.id != kRiffChunkIdRiff) {
return absl::InvalidArgumentError(
"Input does not start with RIFF chunk");
}
riff_chunk = chunk;
first = false;
return true;
}
if (chunk.offset >= riff_chunk.offset + riff_chunk.length) {
return false; // Stop iterating, we finished the first RIFF chunk
}
if (chunk.id == kRiffChunkIdC2pa) {
if (IsRiffChunkTooLarge(chunk)) {
return absl::InvalidArgumentError(
"RIFF C2PA chunk is too large to extract");
}
c2pa_chunk = chunk;
return false; // Found it, stop iterating
}
return true;
},
end_offset);
ABSL_RETURN_IF_ERROR(status);
if (first) {
return absl::NotFoundError("No manifest store found");
}
return c2pa_chunk;
}
} // namespace
absl::StatusOr<std::string> RiffExtractor::ExtractManifestStore(
riegeli::Reader& input) const {
ABSL_ASSIGN_OR_RETURN(std::optional<RiffChunk> c2pa_chunk,
FindC2paChunk(input, /*end_offset=*/-1));
if (!c2pa_chunk.has_value()) {
return absl::NotFoundError("No manifest store found");
}
int64_t offset = c2pa_chunk->data_offset;
int64_t length = c2pa_chunk->data_length;
if (!input.Seek(offset) || input.pos() != offset) {
return absl::InvalidArgumentError(
"The input could not be reset to the manifest store location.");
}
std::string result;
if (!input.Read(length, result)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read manifest store"));
}
return std::move(result);
}
absl::StatusOr<std::optional<ByteRange>>
RiffExtractor::ExtractManifestStoreLocation(riegeli::Reader& input,
ExtractOptions options) const {
ABSL_ASSIGN_OR_RETURN(std::optional<RiffChunk> c2pa_chunk,
FindC2paChunk(input, options.end_offset));
if (!c2pa_chunk.has_value()) {
if (options.requires_c2pa) {
return absl::NotFoundError("No manifest store found");
}
return std::nullopt;
}
return ByteRange{
.offset = c2pa_chunk->offset,
.length = c2pa_chunk->length,
};
}
absl::StatusOr<std::vector<AssetBox>> RiffExtractor::ExtractBoxes(
riegeli::Reader& input, ExtractOptions options) const {
ABSL_ASSIGN_OR_RETURN(std::vector<AssetBox> boxes,
ExtractBoxesInternal(input, options.end_offset));
if (!options.requires_c2pa) {
return std::move(boxes);
}
int64_t c2pa_count = 0;
for (const auto& box : boxes) {
if (box.identifier == kRiffChunkIdC2pa) {
c2pa_count++;
if (c2pa_count > 1) {
return absl::NotFoundError("Multiple manifest stores found");
}
}
}
if (c2pa_count == 0) {
return absl::NotFoundError("No manifest store found");
}
return std::move(boxes);
}
bool RiffExtractor::MightBeC2paManifestStore(absl::string_view payload) const {
return jumbf::HasDescriptionBoxMatching(payload, kManifestStoreUuid,
kMinimumJumbfDescriptionToggles,
kManifestStoreLabel)
.value_or(false);
}
} // namespace credentio