| // 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 "assertion/assertion_parser.h" | |
| #include <optional> | |
| #include "absl/log/log.h" | |
| #include "absl/status/status.h" | |
| #include "absl/strings/match.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "assertion/parse_assertion.h" | |
| #include "constants/labels.h" | |
| #include "constants/status_codes.h" | |
| #include "google/protobuf/struct.pb.h" | |
| #include "proto/assertion.pb.h" | |
| #include "validator/tracker.h" | |
| namespace credentio { | |
| namespace { | |
| void RecordAssertionParsingFailure(absl::string_view label, absl::Status error, | |
| ValidationTracker& validation_tracker) { | |
| validation_tracker.RecordFailure( | |
| FailureStatusCode::kAssertionCborInvalid, | |
| {.url = | |
| absl::StrCat(kAssertionStoreLabel, kManifestLabelDelimiter, label), | |
| .explanation = absl::StrCat("Failed to parse the assertion with label: ", | |
| label, " and error: ", error.message())}); | |
| } | |
| bool IsBmffBasedHashLabel(absl::string_view label) { | |
| return label == kBmffBasedHashAssertionV2Label || | |
| label == kBmffBasedHashAssertionV3Label || | |
| label == kBmffBasedHashAssertionV2PartLabel || | |
| label == kBmffBasedHashAssertionV3PartLabel; | |
| } | |
| bool IsDataHashLabel(absl::string_view label) { | |
| return label == kDataHashAssertionLabel || | |
| label == kDataHashAssertionPartLabel; | |
| } | |
| bool IsUnsupportedIngredientLabel(absl::string_view label) { | |
| return label == kIngredientAssertionV1Label || | |
| label == kIngredientAssertionV2Label; | |
| } | |
| bool IsBoxesHashLabel(absl::string_view label) { | |
| return label == kBoxesHashAssertionLabel || | |
| label == kBoxesHashAssertionPartLabel; | |
| } | |
| bool IsCollectionDataHashLabel(absl::string_view label) { | |
| return label == kCollectionDataHashAssertionLabel || | |
| label == kCollectionDataHashAssertionPartLabel; | |
| } | |
| } // namespace | |
| absl::string_view StripMultipleInstanceSuffix(absl::string_view label) { | |
| return label.substr(0, label.find("__")); | |
| } | |
| bool AssertionTypeMatcher::Matches(absl::string_view label) const { | |
| return StripMultipleInstanceSuffix(label) == label_; | |
| } | |
| std::optional<Assertion> AssertionParser::ParseCbor( | |
| absl::string_view label, absl::string_view content, | |
| ValidationTracker& validation_tracker) const { | |
| absl::string_view stripped_label = StripMultipleInstanceSuffix(label); | |
| if (IsUnsupportedIngredientLabel(stripped_label)) { | |
| // Unlike other assertion types, it's not safe to ignore an unsupported | |
| // ingredient assertion. | |
| validation_tracker.RecordFailure( | |
| FailureStatusCode::kGoogleUnsupportedSpecVersion, | |
| {.url = | |
| absl::StrCat(kAssertionStoreLabel, kManifestLabelDelimiter, label), | |
| .explanation = "c2pa.ingredient (v1) and c2pa.ingredient.v2 " | |
| "assertions are not supported"}); | |
| return std::nullopt; | |
| } | |
| Assertion assertion; | |
| assertion.set_label(label); | |
| absl::Status status; | |
| if (stripped_label == kActionsAssertionV2Label) { | |
| status = ParseAssertion(content, assertion.mutable_actions()); | |
| } else if (stripped_label == kActionsAssertionV1Label) { | |
| status = ParseAssertion(content, assertion.mutable_actions_v1()); | |
| } else if (IsBmffBasedHashLabel(stripped_label)) { | |
| status = ParseAssertion(content, assertion.mutable_bmff_based_hash()); | |
| } else if (IsDataHashLabel(stripped_label)) { | |
| status = ParseAssertion(content, assertion.mutable_data_hash()); | |
| } else if (stripped_label == kIngredientAssertionV3Label) { | |
| status = ParseAssertion(content, assertion.mutable_ingredient_v3()); | |
| } else if (IsBoxesHashLabel(stripped_label)) { | |
| status = ParseAssertion(content, assertion.mutable_boxes_hash()); | |
| } else if (stripped_label == kMultiAssetHashAssertionLabel) { | |
| status = ParseAssertion(content, assertion.mutable_multi_asset_hash()); | |
| } else if (IsCollectionDataHashLabel(stripped_label)) { | |
| status = ParseAssertion(content, assertion.mutable_collection_data_hash()); | |
| } else if (stripped_label == kAiDisclosureAssertionLabel) { | |
| status = ParseAssertion(content, assertion.mutable_ai_disclosure()); | |
| } else if (stripped_label == kEnvironmentalSustainabilityAssertionLabel) { | |
| status = ParseAssertion(content, | |
| assertion.mutable_environmental_sustainability()); | |
| } else if (stripped_label == kSoftBindingAssertionLabel) { | |
| status = ParseAssertion(content, assertion.mutable_soft_binding()); | |
| } else { | |
| return std::nullopt; | |
| } | |
| if (!status.ok()) { | |
| RecordAssertionParsingFailure(label, status, validation_tracker); | |
| return std::nullopt; | |
| } | |
| return assertion; | |
| } | |
| std::optional<Assertion> AssertionParser::ParseThumbnail( | |
| absl::string_view label, absl::string_view media_type, | |
| absl::string_view data, std::optional<absl::string_view> file_name, | |
| ValidationTracker& validation_tracker) const { | |
| Assertion assertion; | |
| assertion.set_label(label); | |
| if (absl::StartsWith(label, kThumbnailClaimAssertionLabelPrefix)) { | |
| *assertion.mutable_claim_thumbnail() = | |
| ParseThumbnailAssertion(media_type, data, file_name); | |
| return assertion; | |
| } | |
| if (absl::StartsWith(label, kThumbnailIngredientAssertionLabelPrefix)) { | |
| *assertion.mutable_ingredient_thumbnail() = | |
| ParseThumbnailAssertion(media_type, data, file_name); | |
| return assertion; | |
| } | |
| return std::nullopt; | |
| } | |
| std::optional<Assertion> AssertionParser::ParseMetadata( | |
| absl::string_view label, absl::string_view content, | |
| ValidationTracker& validation_tracker) const { | |
| Assertion assertion; | |
| assertion.set_label(label); | |
| absl::string_view stripped_label = StripMultipleInstanceSuffix(label); | |
| if (stripped_label == kRepositoryReceiptAssertionLabel) { | |
| if (auto status = | |
| ParseAssertion(content, assertion.mutable_repository_receipt()); | |
| !status.ok()) { | |
| validation_tracker.RecordFailure( | |
| FailureStatusCode::kAssertionJsonInvalid, | |
| {.url = absl::StrCat(kAssertionStoreLabel, kManifestLabelDelimiter, | |
| label), | |
| .explanation = | |
| absl::StrCat("Failed to parse the assertion with label: ", label, | |
| " and error: ", status.message())}); | |
| return std::nullopt; | |
| } | |
| return assertion; | |
| } | |
| if (!absl::EndsWith(stripped_label, kMetadataAssertionLabelSuffix)) { | |
| return std::nullopt; | |
| } | |
| if (auto status = | |
| ParseStruct(content, assertion.mutable_metadata()->mutable_value()); | |
| !status.ok()) { | |
| validation_tracker.RecordFailure( | |
| FailureStatusCode::kAssertionJsonInvalid, | |
| {.url = | |
| absl::StrCat(kAssertionStoreLabel, kManifestLabelDelimiter, label), | |
| .explanation = | |
| absl::StrCat("Failed to parse the assertion with label: ", label, | |
| " and error: ", status.message())}); | |
| return std::nullopt; | |
| } | |
| if (!assertion.metadata().value().fields().contains("@context")) { | |
| validation_tracker.RecordFailure( | |
| FailureStatusCode::kGeneralError, | |
| {.url = | |
| absl::StrCat(kAssertionStoreLabel, kManifestLabelDelimiter, label), | |
| .explanation = "Metadata doesn't have @context"}); | |
| return std::nullopt; | |
| } | |
| return assertion; | |
| } | |
| } // namespace credentio |