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