| // 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 "utils/crjson.h" |
| |
| #include <cstdint> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/status/status_matchers.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/escaping.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "constants/labels.h" |
| #include "cppbor/cppbor.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "jumbf/constants.h" |
| #include "jumbf/test_utils.h" |
| #include "nlohmann/json.hpp" |
| #include "proto/ingredient_assertion.pb.h" |
| #include "proto/ingredient_validation_result.pb.h" |
| #include "proto/manifest.pb.h" |
| #include "proto/validation_result.pb.h" |
| #include "proto/validation_status.pb.h" |
| |
| namespace credentio { |
| namespace { |
| |
| using ::absl_testing::IsOk; |
| |
| TEST(ConvertManifestStoreToCrJsonTest, ByteStringsAreBase64Encoded) { |
| // 1. Construct a CBOR map containing a byte string. |
| cppbor::Map map; |
| map.add("hash_value", cppbor::Bstr("raw_bytes_here")); |
| std::vector<uint8_t> encoded_cbor = map.encode(); |
| absl::string_view cbor_view( |
| reinterpret_cast<const char*>(encoded_cbor.data()), encoded_cbor.size()); |
| |
| // 2. Wrap it in a CborBox. |
| std::string cbor_box = jumbf::EncodeCborBox(cbor_view); |
| |
| // 3. Wrap that in a SuperBox (representing an assertion). |
| // Use a standard allowlisted assertion label, e.g., kDataHashAssertionLabel. |
| std::string assertion_box = jumbf::EncodeSuperBox( |
| jumbf::kCborBoxTypeUuid, kDataHashAssertionLabel, {cbor_box}, |
| /*requestable=*/true); |
| |
| // 4. Wrap that in an assertion store SuperBox. |
| std::string assertion_store_box = jumbf::EncodeSuperBox( |
| kAssertionStoreUuid, kAssertionStoreLabel, {assertion_box}); |
| |
| // 5. Wrap that in a manifest SuperBox. |
| std::string manifest_box = jumbf::EncodeSuperBox( |
| kStandardManifestUuid, "test_manifest", {assertion_store_box}); |
| |
| // 6. Wrap that in a manifest store SuperBox. |
| std::string manifest_store_box = jumbf::EncodeSuperBox( |
| kManifestStoreUuid, kManifestStoreLabel, {manifest_box}); |
| |
| // 7. Call ConvertManifestStoreToCrJsonWithoutValidationResults. |
| absl::StatusOr<nlohmann::json> j = |
| ConvertToCrJsonWithoutValidationResults(manifest_store_box); |
| ASSERT_THAT(j, IsOk()); |
| |
| // 8. Verify that the output JSON contains the base64 encoded byte string. |
| // The expected format is "b64'<base64_data>'". |
| std::string expected_b64 = |
| absl::StrCat("b64'", absl::Base64Escape("raw_bytes_here"), "'"); |
| ASSERT_TRUE(j->contains("manifests")); |
| ASSERT_GT((*j)["manifests"].size(), 0); |
| const auto& manifest = (*j)["manifests"][0]; |
| ASSERT_TRUE(manifest.contains("assertions")); |
| const auto& assertions = manifest["assertions"]; |
| ASSERT_TRUE(assertions.contains(kDataHashAssertionLabel)); |
| const auto& assertion_json = assertions[kDataHashAssertionLabel]; |
| |
| EXPECT_EQ(assertion_json["hash_value"], expected_b64); |
| } |
| |
| TEST(ConvertManifestStoreToCrJsonTest, UnrecognizedAssertionIsIncluded) { |
| // 1. Construct a CBOR map for a dummy assertion. |
| cppbor::Map map; |
| map.add("foo", "bar"); |
| std::vector<uint8_t> encoded_cbor = map.encode(); |
| absl::string_view cbor_view( |
| reinterpret_cast<const char*>(encoded_cbor.data()), encoded_cbor.size()); |
| |
| // 2. Wrap it in a CborBox. |
| std::string cbor_box = jumbf::EncodeCborBox(cbor_view); |
| |
| // 3. Wrap that in a SuperBox with an unrecognized label. |
| std::string assertion_box = jumbf::EncodeSuperBox( |
| jumbf::kCborBoxTypeUuid, "custom.unrecognized.assertion", {cbor_box}, |
| /*requestable=*/true); |
| |
| // 4. Wrap that in an assertion store SuperBox. |
| std::string assertion_store_box = jumbf::EncodeSuperBox( |
| kAssertionStoreUuid, kAssertionStoreLabel, {assertion_box}); |
| |
| // 5. Wrap that in a manifest SuperBox. |
| std::string manifest_box = jumbf::EncodeSuperBox( |
| kStandardManifestUuid, "test_manifest", {assertion_store_box}); |
| |
| // 6. Wrap that in a manifest store SuperBox. |
| std::string manifest_store_box = jumbf::EncodeSuperBox( |
| kManifestStoreUuid, kManifestStoreLabel, {manifest_box}); |
| |
| // 7. Call ConvertManifestStoreToCrJsonWithoutValidationResults. |
| absl::StatusOr<nlohmann::json> j = |
| ConvertToCrJsonWithoutValidationResults(manifest_store_box); |
| ASSERT_THAT(j, IsOk()); |
| |
| // 8. Verify that the output JSON contains the unrecognized assertion with |
| // empty object. |
| ASSERT_TRUE(j->contains("manifests")); |
| ASSERT_GT((*j)["manifests"].size(), 0); |
| const auto& manifest = (*j)["manifests"][0]; |
| ASSERT_TRUE(manifest.contains("assertions")); |
| const auto& assertions = manifest["assertions"]; |
| ASSERT_TRUE(assertions.contains("custom.unrecognized.assertion")); |
| EXPECT_TRUE(assertions["custom.unrecognized.assertion"].is_object()); |
| EXPECT_TRUE(assertions["custom.unrecognized.assertion"].empty()); |
| } |
| |
| TEST(ConvertManifestStoreToCrJsonTest, HandlesNonSuperBoxInAssertionStore) { |
| // 1. Construct a CBOR box directly. This is not a SuperBox. |
| std::string cbor_box = jumbf::EncodeCborBox("test"); |
| |
| // 2. Wrap that in an assertion store SuperBox. This is the error condition. |
| std::string assertion_store_box = jumbf::EncodeSuperBox( |
| kAssertionStoreUuid, kAssertionStoreLabel, {cbor_box}); |
| |
| // 3. Wrap that in a manifest SuperBox. |
| std::string manifest_box = jumbf::EncodeSuperBox( |
| kStandardManifestUuid, "test_manifest", {assertion_store_box}); |
| |
| // 4. Wrap that in a manifest store SuperBox. |
| std::string manifest_store_box = jumbf::EncodeSuperBox( |
| kManifestStoreUuid, kManifestStoreLabel, {manifest_box}); |
| |
| // 5. Call ConvertManifestStoreToCrJsonWithoutValidationResults. |
| absl::StatusOr<nlohmann::json> j = |
| ConvertToCrJsonWithoutValidationResults(manifest_store_box); |
| ASSERT_THAT(j, IsOk()); |
| |
| // 6. Verify that the output JSON contains the error message. |
| ASSERT_TRUE(j->contains("manifests")); |
| ASSERT_GT((*j)["manifests"].size(), 0); |
| const auto& manifest = (*j)["manifests"][0]; |
| ASSERT_TRUE(manifest.contains("assertions")); |
| const auto& assertions = manifest["assertions"]; |
| // The error should be at the top level of the "assertions" object because |
| // the structure is invalid before even getting to a specific assertion label. |
| ASSERT_TRUE(assertions.contains("_error")); |
| EXPECT_EQ(assertions["_error"], |
| "Assertion store contains non-SuperBox content"); |
| } |
| |
| TEST(ConvertManifestStoreToCrJsonTest, ClaimV2IsConverted) { |
| // 1. Construct claim_generator_info map. |
| cppbor::Map generator_info; |
| generator_info.add("name", "Test Generator"); |
| generator_info.add("version", "1.0"); |
| |
| // 2. Construct created_assertions array. |
| cppbor::Array created_assertions; |
| cppbor::Map assertion_link; |
| assertion_link.add("url", "self#jumbf=/c2pa/assertions/custom.assertion"); |
| created_assertions.add(std::move(assertion_link)); |
| |
| // 3. Construct the claim map. |
| cppbor::Map claim_map; |
| claim_map.add("instanceID", "test_instance_id"); |
| claim_map.add("claim_generator_info", std::move(generator_info)); |
| claim_map.add("signature", "self#jumbf=/c2pa/signature"); |
| claim_map.add("created_assertions", std::move(created_assertions)); |
| claim_map.add("dc:title", "Test Title"); |
| |
| std::vector<uint8_t> encoded_claim = claim_map.encode(); |
| absl::string_view claim_view( |
| reinterpret_cast<const char*>(encoded_claim.data()), |
| encoded_claim.size()); |
| |
| // 4. Wrap in CborBox. |
| std::string cbor_box = jumbf::EncodeCborBox(claim_view); |
| |
| // 5. Wrap in Claim V2 SuperBox. |
| std::string claim_box = jumbf::EncodeSuperBox(jumbf::kCborBoxTypeUuid, |
| "c2pa.claim.v2", {cbor_box}); |
| |
| // 6. Wrap in manifest SuperBox. |
| std::string manifest_box = jumbf::EncodeSuperBox( |
| kStandardManifestUuid, "test_manifest", {claim_box}); |
| |
| // 7. Wrap in manifest store SuperBox. |
| std::string manifest_store_box = jumbf::EncodeSuperBox( |
| kManifestStoreUuid, kManifestStoreLabel, {manifest_box}); |
| |
| // 8. Call ConvertManifestStoreToCrJsonWithoutValidationResults. |
| absl::StatusOr<nlohmann::json> j = |
| ConvertToCrJsonWithoutValidationResults(manifest_store_box); |
| ASSERT_THAT(j, IsOk()); |
| |
| // 9. Verify claim.v2 fields in output JSON. |
| ASSERT_TRUE(j->contains("manifests")); |
| ASSERT_GT((*j)["manifests"].size(), 0); |
| const auto& manifest = (*j)["manifests"][0]; |
| ASSERT_TRUE(manifest.contains("claim.v2")); |
| const auto& claim_json = manifest["claim.v2"]; |
| |
| EXPECT_EQ(claim_json["instanceID"], "test_instance_id"); |
| EXPECT_EQ(claim_json["dc:title"], "Test Title"); |
| EXPECT_EQ(claim_json["signature"], "self#jumbf=/c2pa/signature"); |
| |
| ASSERT_TRUE(claim_json.contains("claim_generator_info")); |
| EXPECT_EQ(claim_json["claim_generator_info"]["name"], "Test Generator"); |
| EXPECT_EQ(claim_json["claim_generator_info"]["version"], "1.0"); |
| |
| ASSERT_TRUE(claim_json.contains("created_assertions")); |
| ASSERT_GT(claim_json["created_assertions"].size(), 0); |
| EXPECT_EQ(claim_json["created_assertions"][0]["url"], |
| "self#jumbf=/c2pa/assertions/custom.assertion"); |
| } |
| |
| TEST(ConvertManifestStoreToCrJsonTest, ClaimV2WithMetadataIsConverted) { |
| // 1. Construct claim_generator_info map. |
| cppbor::Map generator_info; |
| generator_info.add("name", "Test Generator"); |
| generator_info.add("version", "1.0"); |
| |
| // 2. Construct created_assertions array. |
| cppbor::Array created_assertions; |
| cppbor::Map assertion_link; |
| assertion_link.add("url", "self#jumbf=/c2pa/assertions/custom.assertion"); |
| created_assertions.add(std::move(assertion_link)); |
| |
| // 3. Construct metadata map. |
| cppbor::Map metadata_map; |
| metadata_map.add("dateTime", "2023-05-21T00:00:00Z"); |
| |
| // rating |
| cppbor::Array rating_array; |
| cppbor::Map rating_map; |
| rating_map.add("value", "suitable-for-all"); |
| rating_map.add("code", "G"); |
| rating_map.add("explanation", "General Audiences"); |
| rating_array.add(std::move(rating_map)); |
| metadata_map.add("rating", std::move(rating_array)); |
| |
| // dataSource |
| cppbor::Map datasource_map; |
| datasource_map.add("type", "camera"); |
| datasource_map.add("details", "Captured by hardware"); |
| metadata_map.add("dataSource", std::move(datasource_map)); |
| |
| // reference |
| cppbor::Map reference_map; |
| reference_map.add("url", "self#jumbf=/c2pa/assertions/custom.assertion"); |
| metadata_map.add("reference", std::move(reference_map)); |
| |
| // localizations |
| cppbor::Array localizations_array; |
| cppbor::Map localization_map; |
| localization_map.add("en", "English"); |
| localizations_array.add(std::move(localization_map)); |
| metadata_map.add("localizations", std::move(localizations_array)); |
| |
| // regionOfInterest |
| cppbor::Map region_map; |
| region_map.add("foo", "bar"); |
| metadata_map.add("regionOfInterest", std::move(region_map)); |
| |
| // 4. Construct the claim map. |
| cppbor::Map claim_map; |
| claim_map.add("instanceID", "test_instance_id"); |
| claim_map.add("claim_generator_info", std::move(generator_info)); |
| claim_map.add("signature", "self#jumbf=/c2pa/signature"); |
| claim_map.add("created_assertions", std::move(created_assertions)); |
| claim_map.add("dc:title", "Test Title"); |
| claim_map.add("metadata", std::move(metadata_map)); |
| |
| std::vector<uint8_t> encoded_claim = claim_map.encode(); |
| absl::string_view claim_view( |
| reinterpret_cast<const char*>(encoded_claim.data()), |
| encoded_claim.size()); |
| |
| // 5. Wrap in CborBox. |
| std::string cbor_box = jumbf::EncodeCborBox(claim_view); |
| |
| // 6. Wrap in Claim V2 SuperBox. |
| std::string claim_box = jumbf::EncodeSuperBox(jumbf::kCborBoxTypeUuid, |
| "c2pa.claim.v2", {cbor_box}); |
| |
| // 7. Wrap in manifest SuperBox. |
| std::string manifest_box = jumbf::EncodeSuperBox( |
| kStandardManifestUuid, "test_manifest", {claim_box}); |
| |
| // 8. Wrap in manifest store SuperBox. |
| std::string manifest_store_box = jumbf::EncodeSuperBox( |
| kManifestStoreUuid, kManifestStoreLabel, {manifest_box}); |
| |
| // 9. Call ConvertManifestStoreToCrJsonWithoutValidationResults. |
| absl::StatusOr<nlohmann::json> j = |
| ConvertToCrJsonWithoutValidationResults(manifest_store_box); |
| ASSERT_THAT(j, IsOk()); |
| |
| // 10. Verify metadata fields in output JSON. |
| ASSERT_TRUE(j->contains("manifests")); |
| ASSERT_GT((*j)["manifests"].size(), 0); |
| const auto& manifest = (*j)["manifests"][0]; |
| ASSERT_TRUE(manifest.contains("claim.v2")); |
| const auto& claim_json = manifest["claim.v2"]; |
| |
| ASSERT_TRUE(claim_json.contains("metadata")); |
| const auto& metadata_json = claim_json["metadata"]; |
| |
| EXPECT_EQ(metadata_json["dateTime"], "2023-05-21T00:00:00Z"); |
| |
| ASSERT_TRUE(metadata_json.contains("rating")); |
| ASSERT_GT(metadata_json["rating"].size(), 0); |
| EXPECT_EQ(metadata_json["rating"][0]["value"], "suitable-for-all"); |
| EXPECT_EQ(metadata_json["rating"][0]["code"], "G"); |
| EXPECT_EQ(metadata_json["rating"][0]["explanation"], "General Audiences"); |
| |
| ASSERT_TRUE(metadata_json.contains("dataSource")); |
| EXPECT_EQ(metadata_json["dataSource"]["type"], "camera"); |
| EXPECT_EQ(metadata_json["dataSource"]["details"], "Captured by hardware"); |
| |
| ASSERT_TRUE(metadata_json.contains("reference")); |
| EXPECT_EQ(metadata_json["reference"]["url"], |
| "self#jumbf=/c2pa/assertions/custom.assertion"); |
| |
| // Localizations and RegionOfInterest are currently converted to empty objects |
| // but we still verify they exist. |
| ASSERT_TRUE(metadata_json.contains("localizations")); |
| EXPECT_TRUE(metadata_json["localizations"].is_array()); |
| ASSERT_TRUE(metadata_json.contains("regionOfInterest")); |
| EXPECT_TRUE(metadata_json["regionOfInterest"].is_object()); |
| } |
| |
| } // namespace |
| } // namespace credentio |