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