blob: f21b17c1d1f67e0ef1da88d7b8254afa8f03204c [file] [edit]
// 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