| // 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 |