| // 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 "bindings/validator.h" | |
| #include <algorithm> | |
| #include <cstdint> | |
| #include <iterator> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <variant> | |
| #include <vector> | |
| #include "absl/base/nullability.h" | |
| #include "absl/container/flat_hash_map.h" | |
| #include "absl/functional/function_ref.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/strings/substitute.h" | |
| #include "bindings/bmff_hash_hard_binding_validator.h" | |
| #include "bindings/boxes_hash_hard_binding_validator.h" | |
| #include "bindings/collection_data_hash_hard_binding_validator.h" | |
| #include "bindings/data_hash_hard_binding_validator.h" | |
| #include "constants/status_codes.h" | |
| #include "formats/asset_box.h" | |
| #include "formats/bmff/assessor.h" | |
| #include "formats/bmff/box_header.h" | |
| #include "formats/byte_range.h" | |
| #include "formats/format.h" | |
| #include "formats/registry.h" | |
| #include "jumbf/uri.h" | |
| #include "proto/assertion.pb.h" | |
| #include "proto/bmff_based_hash_assertion.pb.h" | |
| #include "proto/hashed_uri.pb.h" | |
| #include "proto/manifest.pb.h" | |
| #include "proto/multi_asset_hash_assertion.pb.h" | |
| #include "proto/validation_result.pb.h" | |
| #include "proto/validation_status.pb.h" | |
| #include "riegeli/bytes/reader.h" | |
| #include "utils/dual_status_tracker.h" | |
| #include "utils/status_tracker.h" | |
| #include "utils/two_stage_status_tracker.h" | |
| #include "utils/uri.h" | |
| #include "validator/result.h" | |
| #include "validator/validation_result_internal.h" | |
| namespace credentio { | |
| namespace { | |
| struct OffsetLocator { | |
| int64_t offset; | |
| int64_t length; | |
| }; | |
| struct BmffLocator { | |
| std::string path; | |
| }; | |
| absl::StatusOr<std::variant<OffsetLocator, BmffLocator>> GetLocator( | |
| const MultiAssetPartLocation& part) { | |
| if (!part.has_byte_offset() && !part.has_length() && !part.has_bmff_box()) { | |
| return absl::InvalidArgumentError("no location specified"); | |
| } | |
| if (part.has_bmff_box() && (part.has_byte_offset() || part.has_length())) { | |
| return absl::InvalidArgumentError( | |
| "bmff box cannot be used with byte range locator"); | |
| } | |
| if (part.has_bmff_box()) { | |
| return BmffLocator{.path = std::string(part.bmff_box())}; | |
| } | |
| if (!part.has_byte_offset() || !part.has_length()) { | |
| return absl::InvalidArgumentError( | |
| "byte range locator must have both offset and length"); | |
| } | |
| return OffsetLocator{.offset = part.byte_offset(), .length = part.length()}; | |
| } | |
| std::unique_ptr<PartialValidationResultProto> CopyManifestLabels( | |
| const PartialValidationResultProto& source) { | |
| auto target = std::make_unique<PartialValidationResultProto>(); | |
| target->set_hard_binding_uri(source.hard_binding_uri()); | |
| target->set_multi_asset_hash_uri(source.multi_asset_hash_uri()); | |
| target->mutable_active_manifest()->set_label( | |
| source.active_manifest().label()); | |
| for (const Manifest& manifest : source.ingredient_manifests()) { | |
| target->add_ingredient_manifests()->set_label(manifest.label()); | |
| } | |
| return target; | |
| } | |
| absl::StatusOr<std::unique_ptr<ValidationResultProto>> | |
| CompleteValidationResults( | |
| std::unique_ptr<PartialValidationResultProto> target, | |
| std::unique_ptr<PartialValidationResultProto> source) { | |
| target->mutable_active_manifest()->mutable_validation()->MergeFrom( | |
| std::move(*source->mutable_active_manifest()->mutable_validation())); | |
| absl::flat_hash_map<absl::string_view, ValidationStatusSet> source_results; | |
| for (const Manifest& source_manifest : source->ingredient_manifests()) { | |
| source_results[source_manifest.label()] = | |
| std::move(source_manifest.validation()); | |
| } | |
| for (Manifest& target_manifest : *target->mutable_ingredient_manifests()) { | |
| if (source_results.contains(target_manifest.label())) { | |
| target_manifest.mutable_validation()->MergeFrom( | |
| std::move(source_results[target_manifest.label()])); | |
| } | |
| } | |
| return MakeFullValidationResult(std::move(target)); | |
| } | |
| // Validates the parts of a multi-asset hash assertion. This ensures that the | |
| // parts do not overlap and that the total size of the required parts does not | |
| // exceed the size of the asset. Only problems accessing the files return as | |
| // non-OK status, other errors are recorded in the status tracker. | |
| absl::Status ValidateParts(const MultiAssetHashAssertion& assertion, | |
| absl::string_view assertion_uri, int64_t asset_size, | |
| StatusTracker& tracker) { | |
| int64_t next_expected_offset = 0; | |
| int64_t minimum_required_size = 0; | |
| for (const auto& part : assertion.parts()) { | |
| absl::StatusOr<std::variant<OffsetLocator, BmffLocator>> locator = | |
| GetLocator(part.location()); | |
| if (!locator.ok()) { | |
| tracker.RecordFailure( | |
| FailureStatusCode::kAssertionMultiAssetHashMalformed, | |
| {.url = assertion_uri}); | |
| return absl::OkStatus(); | |
| } | |
| if (std::holds_alternative<BmffLocator>(*locator)) { | |
| // Contains a BMFF locator, cannot ensure bytes included based on offsets. | |
| return absl::OkStatus(); | |
| } | |
| OffsetLocator location = std::get<OffsetLocator>(*locator); | |
| if (location.offset != next_expected_offset) { | |
| tracker.RecordFailure( | |
| FailureStatusCode::kAssertionMultiAssetHashMalformed, | |
| {.url = assertion_uri}); | |
| return absl::OkStatus(); | |
| } | |
| next_expected_offset = location.offset + location.length; | |
| if (!part.is_optional()) { | |
| minimum_required_size += location.length; | |
| } | |
| } | |
| if (minimum_required_size > asset_size) { | |
| tracker.RecordFailure( | |
| FailureStatusCode::kAssertionMultiAssetHashMissingPart, | |
| {.url = assertion_uri}); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| } // namespace | |
| absl::StatusOr<ContentBindingValidator::Structure> | |
| ContentBindingValidator::ExtractAssetStructure(riegeli::Reader& contents, | |
| const Format* format, | |
| int64_t end_offset) const { | |
| ContentBindingValidator::Structure structure{ | |
| .asset = ByteRange{.offset = contents.pos(), | |
| .length = end_offset - contents.pos()}}; | |
| absl::StatusOr<const Format*> inferred_format; | |
| if (format == nullptr) { | |
| inferred_format = format_registry_->GetFormat(contents); | |
| if (!inferred_format.ok()) { | |
| if (structure.asset.offset == 0) { | |
| // Invalid format on the first asset therefore unsupported format. | |
| return inferred_format.status(); | |
| } | |
| return structure; | |
| } | |
| format = *inferred_format; | |
| } | |
| absl::StatusOr<std::vector<AssetBox>> boxes = | |
| format->extractor()->ExtractBoxes(contents, {.requires_c2pa = false}); | |
| if (boxes.ok() && !boxes->empty()) { | |
| ABSL_ASSIGN_OR_RETURN( | |
| int64_t end_of_asset_offset, | |
| PopulateStructureFromBoxes(*std::move(boxes), structure)); | |
| // Only assets which contain boxes can support multiple assets (by finding | |
| // the c2pa.after box). Navigate to the start of the next potential asset | |
| // and continue to extract the structure. | |
| if (!contents.Seek(end_of_asset_offset) || | |
| contents.pos() != end_of_asset_offset) { | |
| return contents.StatusOrAnnotate( | |
| absl::InternalError("failed to seek to end of asset")); | |
| } | |
| return structure; | |
| } | |
| // The determined format does not support multiple assets at this point, | |
| // attempt to find the manifest store location and treat the remainder of | |
| // the file as the last asset. | |
| absl::StatusOr<std::optional<ByteRange>> manifest_store_location = | |
| format->extractor()->ExtractManifestStoreLocation( | |
| contents, {.requires_c2pa = false, .end_offset = end_offset}); | |
| if (manifest_store_location.ok() && manifest_store_location->has_value()) { | |
| structure.manifest_store_location = *std::move(manifest_store_location); | |
| } | |
| return structure; | |
| } | |
| absl::StatusOr<int64_t> ContentBindingValidator::PopulateStructureFromBoxes( | |
| std::vector<AssetBox> boxes, | |
| ContentBindingValidator::Structure& structure) const { | |
| if (boxes.empty()) { | |
| return absl::InvalidArgumentError("no boxes found"); | |
| } | |
| structure.boxes = std::move(boxes); | |
| const AssetBox* last_box = nullptr; | |
| for (const AssetBox& box : structure.boxes) { | |
| last_box = &box; | |
| if (box.identifier == "C2PA") { | |
| structure.manifest_store_location = ByteRange{ | |
| .offset = box.byte_range.offset, .length = box.byte_range.length}; | |
| } | |
| } | |
| if (last_box != nullptr && last_box->identifier == "c2pa.after") { | |
| structure.asset.length = | |
| last_box->byte_range.offset - structure.asset.offset; | |
| } | |
| return structure.asset.offset + structure.asset.length; | |
| } | |
| absl::StatusOr<std::vector<ContentBindingValidator::Structure>> | |
| ContentBindingValidator::GetStructure(riegeli::Reader& contents, | |
| const Format& format) const { | |
| std::vector<ContentBindingValidator::Structure> structures; | |
| if (!contents.SupportsSize() || !contents.Size().has_value()) { | |
| return absl::InvalidArgumentError("asset does not support size"); | |
| } | |
| if (contents.Size() == 0) { | |
| return absl::InvalidArgumentError("asset has no contents"); | |
| } | |
| if (!contents.Seek(0) || contents.pos() != 0) { | |
| return contents.StatusOrAnnotate( | |
| absl::InternalError("failed to seek to start of asset")); | |
| } | |
| uint64_t end_offset = *contents.Size(); | |
| bool at_end_of_file = false; | |
| while (!at_end_of_file) { | |
| // Format is always inferred for non-first parts. | |
| const Format* part_format = structures.empty() ? &format : nullptr; | |
| ABSL_ASSIGN_OR_RETURN( | |
| ContentBindingValidator::Structure structure, | |
| ExtractAssetStructure(contents, part_format, end_offset)); | |
| at_end_of_file = | |
| (structure.asset.offset + structure.asset.length) == end_offset; | |
| structures.push_back(std::move(structure)); | |
| } | |
| return structures; | |
| } | |
| absl::StatusOr<std::unique_ptr<ValidationResultProto>> | |
| ContentBindingValidator::Validate(riegeli::Reader& contents, | |
| const Format& format, | |
| std::unique_ptr<PartialValidationResultProto> | |
| partial_validation_result) const { | |
| std::unique_ptr<PartialValidationResultProto> result = | |
| CopyManifestLabels(*partial_validation_result); | |
| ABSL_ASSIGN_OR_RETURN( | |
| std::unique_ptr<DualStatusTracker> tracker, | |
| DualStatusTracker::FromPartialValidationResult(&*result)); | |
| bool assertion_in_ingredient_manifest = tracker->WritesToIngredientManifest(); | |
| TwoStageStatusTracker staging_tracker(*tracker); | |
| ABSL_ASSIGN_OR_RETURN( | |
| std::vector<ContentBindingValidator::Structure> asset_structure, | |
| GetStructure(contents, format)); | |
| auto assertion_fetcher = [&](absl::string_view uri) { | |
| return GetAssertion(partial_validation_result.get(), uri); | |
| }; | |
| // Getting the structure causes the Tell to be at the end of the file, reset. | |
| if (!contents.Seek(0) || contents.pos() != 0) { | |
| return contents.StatusOrAnnotate( | |
| absl::InternalError("failed to seek to start of asset")); | |
| } | |
| ABSL_RETURN_IF_ERROR(ValidateAssertion( | |
| contents, assertion_fetcher, | |
| partial_validation_result->hard_binding_uri(), asset_structure, | |
| /*requires_c2pa=*/true, staging_tracker, | |
| assertion_in_ingredient_manifest)); | |
| if (!staging_tracker.HasFailuresInStage1() || | |
| partial_validation_result->multi_asset_hash_uri().empty()) { | |
| // Success or no multi-asset hard binding, finished. | |
| staging_tracker.WriteStage1(); | |
| return CompleteValidationResults(std::move(partial_validation_result), | |
| std::move(result)); | |
| } | |
| staging_tracker.MoveStage1ToStage2(); | |
| // Validate the multi-asset hard binding assertion. | |
| if (!contents.Seek(0) || contents.pos() != 0) { | |
| return contents.StatusOrAnnotate( | |
| absl::InternalError("failed to seek to start of asset")); | |
| } | |
| ABSL_RETURN_IF_ERROR(ValidateAssertion( | |
| contents, assertion_fetcher, | |
| partial_validation_result->multi_asset_hash_uri(), asset_structure, | |
| /*requires_c2pa=*/true, staging_tracker, | |
| assertion_in_ingredient_manifest)); | |
| if (staging_tracker.HasFailuresInStage1()) { | |
| // Failed, add all the staged codes to the result. | |
| staging_tracker.WriteStage2(); | |
| } | |
| staging_tracker.WriteStage1(); | |
| return CompleteValidationResults(std::move(partial_validation_result), | |
| std::move(result)); | |
| } | |
| absl::Status ContentBindingValidator::ValidateAssertion( | |
| riegeli::Reader& contents, AssertionFetcherRef assertion_fetcher, | |
| absl::string_view assertion_uri, | |
| std::vector<ContentBindingValidator::Structure> asset_structure, | |
| bool requires_c2pa, TwoStageStatusTracker& tracker, | |
| bool assertion_in_ingredient_manifest, int64_t end_offset) const { | |
| const Assertion* absl_nullable assertion = assertion_fetcher(assertion_uri); | |
| if (assertion == nullptr) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("missing assertion: ", assertion_uri)); | |
| } | |
| switch (assertion->assertion_case()) { | |
| case Assertion::kBoxesHash: | |
| if (asset_structure.empty()) { | |
| return absl::InvalidArgumentError( | |
| "asset structure cannot be determined"); | |
| } | |
| return ValidateBoxesHash(contents, assertion_uri, assertion->boxes_hash(), | |
| asset_structure, requires_c2pa, tracker, | |
| end_offset); | |
| case Assertion::kDataHash: | |
| return ValidateDataHash( | |
| contents, assertion_uri, assertion->data_hash(), | |
| asset_structure.empty() | |
| ? std::nullopt | |
| : asset_structure.front().manifest_store_location, | |
| requires_c2pa, tracker, assertion_in_ingredient_manifest, end_offset); | |
| case Assertion::kCollectionDataHash: | |
| return ValidateCollectionDataHash(contents, assertion_uri, | |
| assertion->collection_data_hash(), | |
| requires_c2pa, tracker); | |
| case Assertion::kBmffBasedHash: | |
| return ValidateBmffHash(contents, assertion_uri, | |
| assertion->bmff_based_hash(), requires_c2pa, | |
| tracker); | |
| case Assertion::kMultiAssetHash: | |
| return ValidateMultiAssetHash( | |
| contents, assertion_uri, assertion->multi_asset_hash(), | |
| asset_structure, requires_c2pa, assertion_fetcher, tracker, | |
| assertion_in_ingredient_manifest); | |
| default: | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("assertion is not a hard binding assertion: ", | |
| assertion->assertion_case())); | |
| } | |
| } | |
| absl::Status ContentBindingValidator::ValidateDataHash( | |
| riegeli::Reader& contents, absl::string_view assertion_uri, | |
| const DataHashAssertion& assertion, | |
| std::optional<ByteRange> manifest_store_location, bool requires_c2pa, | |
| StatusTracker& tracker, bool assertion_in_ingredient_manifest, | |
| int64_t end_offset) const { | |
| DataHashHardBindingValidator().Validate( | |
| contents, manifest_store_location, assertion, assertion_uri, tracker, | |
| assertion_in_ingredient_manifest, contents.pos(), end_offset); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status ContentBindingValidator::ValidateCollectionDataHash( | |
| riegeli::Reader& contents, absl::string_view assertion_uri, | |
| const CollectionDataHashAssertion& assertion, bool requires_c2pa, | |
| StatusTracker& tracker) const { | |
| CollectionDataHashHardBindingValidator().Validate(contents, assertion, | |
| assertion_uri, tracker); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status ContentBindingValidator::ValidateBoxesHash( | |
| riegeli::Reader& contents, absl::string_view assertion_uri, | |
| const BoxesHashAssertion& assertion, | |
| std::vector<ContentBindingValidator::Structure> remaining_structure, | |
| bool requires_c2pa, StatusTracker& tracker, int64_t end_offset) const { | |
| if (assertion.boxes().empty()) { | |
| // No boxes in assertion. | |
| tracker.RecordFailure(FailureStatusCode::kAssertionBoxesHashMalformed, | |
| {.url = assertion_uri}); | |
| return absl::OkStatus(); | |
| } | |
| for (const auto& box : assertion.boxes()) { | |
| if (box.names().empty()) { | |
| // Boxes Assertion has no names. | |
| tracker.RecordFailure(FailureStatusCode::kAssertionBoxesHashMalformed, | |
| {.url = assertion_uri}); | |
| return absl::OkStatus(); | |
| } | |
| } | |
| bool assertion_has_c2pa_after = | |
| *assertion.boxes().rbegin()->names().rbegin() == "c2pa.after"; | |
| if (remaining_structure.empty()) { | |
| // No Asset Data. | |
| tracker.RecordFailure(FailureStatusCode::kAssertionBoxesHashMismatch, | |
| {.url = assertion_uri}); | |
| return absl::OkStatus(); | |
| } | |
| std::vector<AssetBox> asset_boxes = remaining_structure.front().boxes; | |
| if (asset_boxes.empty()) { | |
| // No boxes in asset. | |
| tracker.RecordFailure(FailureStatusCode::kAssertionBoxesHashMismatch, | |
| {.url = assertion_uri}); | |
| return absl::OkStatus(); | |
| } | |
| bool extraction_has_c2pa_after = | |
| asset_boxes.back().identifier == "c2pa.after"; | |
| if (!assertion_has_c2pa_after && extraction_has_c2pa_after) { | |
| int64_t part_idx = 1; | |
| while (part_idx < remaining_structure.size() && | |
| !remaining_structure[part_idx].boxes.empty()) { | |
| // Remove the c2pa.after box. | |
| asset_boxes.pop_back(); | |
| // Add the boxes from the next asset. | |
| Structure& next_asset = remaining_structure[part_idx]; | |
| asset_boxes.reserve(asset_boxes.size() + next_asset.boxes.size()); | |
| asset_boxes.insert(asset_boxes.end(), next_asset.boxes.begin(), | |
| next_asset.boxes.end()); | |
| // Increment the index to the next asset. | |
| ++part_idx; | |
| } | |
| } | |
| BoxesHashHardBindingValidator().Validate(contents, asset_boxes, assertion, | |
| assertion_uri, tracker); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status ContentBindingValidator::ValidateBmffHash( | |
| riegeli::Reader& contents, absl::string_view assertion_uri, | |
| const BmffBasedHashAssertion& assertion, bool requires_c2pa, | |
| StatusTracker& tracker) const { | |
| BmffHashHardBindingValidator().Validate(contents, assertion, assertion_uri, | |
| tracker); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status ContentBindingValidator::ValidateMultiAssetHash( | |
| riegeli::Reader& contents, absl::string_view assertion_uri, | |
| const MultiAssetHashAssertion& assertion, | |
| std::vector<ContentBindingValidator::Structure> asset_structure, | |
| bool requires_c2pa, AssertionFetcherRef assertion_fetcher, | |
| TwoStageStatusTracker& tracker, | |
| bool assertion_in_ingredient_manifest) const { | |
| bool recorded_multi_asset_failure = false; | |
| int64_t max_offset_validated = 0; | |
| if (!contents.SupportsSize() || !contents.Size().has_value()) { | |
| return absl::InvalidArgumentError( | |
| "multi-asset hash requires an asset with a known size"); | |
| } | |
| uint64_t asset_size = *contents.Size(); | |
| // multi-asset hash should drop all `c2pa.after` boxes | |
| for (auto& structure : asset_structure) { | |
| if (!structure.boxes.empty()) { | |
| if (structure.boxes.back().identifier == "c2pa.after") { | |
| structure.boxes.pop_back(); | |
| } | |
| } | |
| } | |
| ABSL_RETURN_IF_ERROR( | |
| ValidateParts(assertion, assertion_uri, asset_size, tracker)); | |
| if (tracker.HasFailuresInStage1()) { | |
| // No need to validate if we already have failures. | |
| return absl::OkStatus(); | |
| } | |
| TwoStageStatusTracker multi_asset_tracker( | |
| *static_cast<StatusTracker*>(&tracker)); | |
| for (int part_index = 0; part_index < assertion.parts_size(); ++part_index) { | |
| const auto& part = assertion.parts(part_index); | |
| ABSL_ASSIGN_OR_RETURN(auto locator, GetLocator(part.location())); | |
| std::optional<int64_t> offset = std::nullopt; | |
| std::optional<int64_t> length = std::nullopt; | |
| if (std::holds_alternative<OffsetLocator>(locator)) { | |
| OffsetLocator location = std::get<OffsetLocator>(locator); | |
| offset = location.offset; | |
| length = location.length; | |
| } else { | |
| BmffLocator location = std::get<BmffLocator>(locator); | |
| if (!contents.Seek(0) || contents.pos() != 0) { | |
| return contents.StatusOrAnnotate( | |
| absl::InternalError("failed to seek to start of asset")); | |
| } | |
| ABSL_ASSIGN_OR_RETURN(bool is_supported, | |
| BmffAssessor().IsSupported(contents)); | |
| if (!is_supported) { | |
| return absl::InvalidArgumentError( | |
| "Uses a BMFF locator, but file is not a BMFF file"); | |
| } | |
| ABSL_RETURN_IF_ERROR(IterateOverBmffBoxes( | |
| contents, [&location, &offset, &length](const BmffBoxHeader& header) { | |
| if (header.xpath == location.path) { | |
| offset = header.start; | |
| length = header.box_size; | |
| return false; // Terminate loop | |
| } | |
| return true; // Continue | |
| })); | |
| if (!offset.has_value()) { | |
| // Did not find the location | |
| multi_asset_tracker.RecordFailure( | |
| FailureStatusCode::kAssertionMultiAssetHashMalformed, | |
| {.url = assertion_uri}); | |
| recorded_multi_asset_failure = true; | |
| } | |
| } | |
| if (!offset.has_value() || !length.has_value()) { | |
| multi_asset_tracker.RecordFailure( | |
| FailureStatusCode::kAssertionMultiAssetHashMissingPart, | |
| {.url = assertion_uri, | |
| .explanation = | |
| absl::Substitute("missing location for part $0 of $1", | |
| part_index + 1, assertion.parts_size())}); | |
| recorded_multi_asset_failure = true; | |
| if (part.is_optional()) { | |
| multi_asset_tracker.MoveStage1ToStage2(); | |
| } else { | |
| multi_asset_tracker.WriteStage1(); | |
| } | |
| requires_c2pa = false; | |
| continue; | |
| } | |
| if (!contents.Seek(*offset) || contents.pos() != *offset || | |
| (*offset + *length) > asset_size) { | |
| multi_asset_tracker.RecordFailure( | |
| FailureStatusCode::kAssertionMultiAssetHashMissingPart, | |
| {.url = assertion_uri, | |
| .explanation = | |
| absl::Substitute("missing data for part $0 of $1", | |
| part_index + 1, assertion.parts_size())}); | |
| if (part.is_optional()) { | |
| multi_asset_tracker.MoveStage1ToStage2(); | |
| } else { | |
| multi_asset_tracker.WriteStage1(); | |
| } | |
| requires_c2pa = false; | |
| continue; | |
| } | |
| ABSL_ASSIGN_OR_RETURN(absl::string_view manifest_label, | |
| GetManifestLabelFromAbsoluteUri(assertion_uri)); | |
| ABSL_ASSIGN_OR_RETURN(std::string part_hash_assertion_path, | |
| jumbf::UriResolver::GetAbsolutePathFromUri( | |
| part.hash_assertion().url(), | |
| absl::StrCat("/c2pa/", manifest_label))); | |
| std::string part_hash_assertion_uri = | |
| absl::StrCat("self#jumbf=", part_hash_assertion_path); | |
| std::vector<ContentBindingValidator::Structure> part_structure = | |
| (asset_structure.size() - 1) < part_index | |
| ? std::vector<ContentBindingValidator::Structure>{} | |
| : std::vector<ContentBindingValidator::Structure>( | |
| asset_structure.begin() + part_index, asset_structure.end()); | |
| ABSL_RETURN_IF_ERROR( | |
| ValidateAssertion(contents, assertion_fetcher, part_hash_assertion_uri, | |
| part_structure, requires_c2pa, multi_asset_tracker, | |
| assertion_in_ingredient_manifest, *offset + *length)); | |
| if (part.is_optional()) { | |
| if (multi_asset_tracker.HasFailuresInStage1()) { | |
| // Optional, but failed, move to stage 2. | |
| multi_asset_tracker.MoveStage1ToStage2(); | |
| } else { | |
| // Optional, but succeeded, write. | |
| multi_asset_tracker.WriteStage1(); | |
| max_offset_validated = | |
| std::max(max_offset_validated, *offset + *length); | |
| } | |
| } else { | |
| if (multi_asset_tracker.HasFailuresInStage1()) { | |
| // Encountered a failure, but not optional, write all status codes. | |
| multi_asset_tracker.WriteStage2(); | |
| } | |
| multi_asset_tracker.WriteStage1(); | |
| max_offset_validated = std::max(max_offset_validated, *offset + *length); | |
| } | |
| // We only need to ensure C2PA exists on the first part. | |
| requires_c2pa = false; | |
| } | |
| if (!recorded_multi_asset_failure && max_offset_validated != asset_size) { | |
| // Less than the whole file was validated, if the whole file was read it's | |
| // a mismatch, otherwise it's malformed. | |
| multi_asset_tracker.RecordFailure( | |
| contents.pos() == asset_size | |
| ? FailureStatusCode::kAssertionMultiAssetHashMismatch | |
| : FailureStatusCode::kAssertionMultiAssetHashMalformed, | |
| {.url = assertion_uri}); | |
| recorded_multi_asset_failure = true; | |
| } | |
| // Write all status codes to the tracker. | |
| multi_asset_tracker.WriteStage1(); | |
| if (tracker.HasFailuresInStage1()) { | |
| // Failures were added during validation, write all stage two codes. | |
| multi_asset_tracker.WriteStage2(); | |
| if (!recorded_multi_asset_failure) { | |
| tracker.RecordFailure(FailureStatusCode::kAssertionMultiAssetHashMismatch, | |
| {.url = assertion_uri}); | |
| } | |
| } else { | |
| tracker.RecordSuccess(SuccessStatusCode::kAssertionMultiAssetHashMatch, | |
| {.url = assertion_uri}); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| } // namespace credentio |