| // 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 <utility> | |
| #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 | |
| constexpr uint64_t kNullTerminatorSize = 1; | |
| constexpr uint64_t kMerkleOffsetSize = sizeof(uint64_t); | |
| 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; | |
| }; | |
| // The first ftyp, moov and mdat boxes, and the C2PA manifest box, found among | |
| // the top-level boxes of a BMFF file. | |
| struct BmffC2paBoxLocations { | |
| std::optional<BmffBoxHeader> ftyp_box; | |
| std::optional<BmffBoxHeader> moov_box; | |
| std::optional<BmffBoxHeader> mdat_box; | |
| std::optional<BmffBoxHeader> manifest; | |
| }; | |
| absl::StatusOr<std::string> ReadC2paBoxPayload(riegeli::Reader& input, | |
| const BmffBoxHeader& box, | |
| absl::string_view purpose) { | |
| const uint64_t box_end = box.start + box.box_size; | |
| const uint64_t payload_start = box.start + box.header_size + | |
| box.version_and_flags_size + purpose.size() + | |
| kNullTerminatorSize + kMerkleOffsetSize; | |
| // Guard against boxes too small to hold the purpose and merkle offset, which | |
| // would otherwise underflow `payload_size` below. | |
| if (payload_start > box_end) { | |
| return absl::NotFoundError( | |
| "No manifest store found; kUnexpectedEof; C2PA box too small for " | |
| "merkle offset"); | |
| } | |
| if (!input.Seek(payload_start)) { | |
| return input.StatusOrAnnotate( | |
| absl::NotFoundError("No manifest store found; kUnexpectedEof; failed " | |
| "to seek to C2PA payload")); | |
| } | |
| const uint64_t payload_size = box_end - payload_start; | |
| if (payload_size > kMaxPayloadSize) { | |
| return absl::NotFoundError( | |
| absl::Substitute("No manifest store found; BMFF C2PA box is too large " | |
| "to extract ($0 > $1)", | |
| payload_size, kMaxPayloadSize)); | |
| } | |
| std::string payload; | |
| if (!input.Read(payload_size, payload)) { | |
| return input.StatusOrAnnotate(absl::NotFoundError( | |
| "No manifest store found; failed to read C2PA box payload")); | |
| } | |
| return payload; | |
| } | |
| absl::Status RecordHeadersForC2pa(riegeli::Reader& input, | |
| const BmffBoxHeader& header, | |
| BmffC2paBoxLocations& c2pa_boxes) { | |
| if (header.type == "ftyp") { | |
| c2pa_boxes.ftyp_box = header; | |
| return absl::OkStatus(); | |
| } | |
| if (header.type == "mdat") { | |
| if (!c2pa_boxes.mdat_box.has_value()) { | |
| c2pa_boxes.mdat_box = header; | |
| } | |
| return absl::OkStatus(); | |
| } | |
| if (header.type == "moov") { | |
| if (!c2pa_boxes.moov_box.has_value()) { | |
| c2pa_boxes.moov_box = header; | |
| } | |
| return absl::OkStatus(); | |
| } | |
| if (header.type == "uuid" && header.user_type == kC2paBmffBoxUuid) { | |
| 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") { | |
| if (c2pa_boxes.manifest.has_value()) { | |
| return absl::NotFoundError("multiple manifest boxes found"); | |
| } | |
| c2pa_boxes.manifest = header; | |
| return absl::OkStatus(); | |
| } | |
| if (purpose == "original" || purpose == "update") { | |
| return absl::InvalidArgumentError( | |
| absl::Substitute("unsupported C2PA metadata purpose ($0)", purpose)); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| absl::StatusOr<Result> ValidateLocationAndExtractManifest( | |
| riegeli::Reader& input, const BmffC2paBoxLocations& c2pa_boxes) { | |
| if (!c2pa_boxes.manifest.has_value()) { | |
| return absl::NotFoundError("No manifest store found"); | |
| } | |
| // Box must be after ftyp. | |
| if (c2pa_boxes.ftyp_box.has_value() && | |
| c2pa_boxes.ftyp_box->start > c2pa_boxes.manifest->start) { | |
| return absl::NotFoundError( | |
| "No manifest store found; manifest resides before ftyp"); | |
| } | |
| // Box must be before moov. | |
| if (c2pa_boxes.moov_box.has_value() && | |
| c2pa_boxes.moov_box->start < c2pa_boxes.manifest->start) { | |
| return absl::NotFoundError( | |
| "No manifest store found; manifest resides after moov"); | |
| } | |
| // Box must be before mdat. | |
| if (c2pa_boxes.mdat_box.has_value() && | |
| c2pa_boxes.mdat_box->start < c2pa_boxes.manifest->start) { | |
| return absl::NotFoundError( | |
| "No manifest store found; manifest resides after mdat"); | |
| } | |
| ABSL_ASSIGN_OR_RETURN( | |
| auto manifest, | |
| ReadC2paBoxPayload(input, *c2pa_boxes.manifest, "manifest")); | |
| if (manifest.empty()) { | |
| return absl::NotFoundError( | |
| "No manifest store found; manifest store is empty"); | |
| } | |
| return Result{.location = {.offset = c2pa_boxes.manifest->start, | |
| .length = c2pa_boxes.manifest->box_size}, | |
| .manifest = std::move(manifest)}; | |
| } | |
| absl::StatusOr<Result> ExtractManifest(riegeli::Reader& input, | |
| int64_t end_offset) { | |
| 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); | |
| BmffC2paBoxLocations c2pa_boxes; | |
| auto status = IterateOverBmffBoxes( | |
| input, | |
| [&input, eof_offset, | |
| &c2pa_boxes](const BmffBoxHeader& header) -> absl::StatusOr<bool> { | |
| uint64_t box_end = header.start + header.box_size; | |
| if (box_end > eof_offset) { | |
| return absl::DataLossError("kUnexpectedEof; truncated BMFF box"); | |
| } | |
| ABSL_RETURN_IF_ERROR(RecordHeadersForC2pa(input, header, c2pa_boxes)); | |
| if (box_end == eof_offset) { | |
| return false; // Terminate loop | |
| } | |
| if (!input.Seek(box_end)) { | |
| return input.StatusOrAnnotate( | |
| absl::InternalError("failed to seek to the end of the atom")); | |
| } | |
| return true; // Continue | |
| }); | |
| if (!status.ok()) { | |
| return absl::NotFoundError( | |
| absl::StrCat("No manifest store found; ", status.message())); | |
| } | |
| return ValidateLocationAndExtractManifest(input, c2pa_boxes); | |
| } | |
| } // 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 |