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