blob: 9c8ef058a4a783e2a0fa24a97914d43361345783 [file]
// 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 "claim/manifest_box_assembler.h"
#include <memory>
#include <string>
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/strings/string_view.h"
#include "claim/manifest_box.h"
#include "claim/manifest_store_builder.h"
#include "constants/labels.h"
#include "constants/spec_version.h"
#include "crypto/default/hasher.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "jumbf/test_utils.h"
#include "proto/assertion.pb.h"
#include "proto/bmff_based_hash_assertion.pb.h"
#include "proto/generator.pb.h"
#include "proto/generator_info.pb.h"
#include "proto/ingredient_assertion.pb.h"
#include "proto/ingredient_validation_result.pb.h"
#include "proto/manifest.pb.h"
#include "proto/signature_info.pb.h"
#include "testing/proto_test_utils.h"
#include "uuid/uuid.h"
namespace credentio {
namespace {
using ::absl_testing::StatusIs;
using ::credentio_testing::EqualsProto;
using ::credentio_testing::ParseTextProtoOrDie;
using ::testing::ElementsAre;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::Optional;
using ::testing::Pointwise;
constexpr auto kUuid =
Uuid::FromStringOrDie("03020100-0504-0706-0908-0f0e0d0c0b0a");
constexpr auto kInstanceId =
Uuid::FromStringOrDie("12345678-1234-1234-1234-123456789012");
std::string CreateSimpleManifestStoreBytes() {
std::string cbor_box = jumbf::EncodeCborBox("cbor_box");
std::string signature_box = jumbf::EncodeSuperBox(
kClaimSignatureUuid, kClaimSignatureLabel, {cbor_box});
std::string test_manifest = jumbf::EncodeSuperBox(
kStandardManifestUuid, "urn:c2pa:11111111-1111-1111-1111-111111111111",
{signature_box});
return jumbf::EncodeSuperBox(kManifestStoreUuid, kManifestStoreLabel,
{test_manifest});
}
std::string CreateLegacyManifestStoreBytes() {
std::string cbor_box = jumbf::EncodeCborBox("cbor_box");
std::string signature_box = jumbf::EncodeSuperBox(
kClaimSignatureUuid, kClaimSignatureLabel, {cbor_box});
std::string test_manifest = jumbf::EncodeSuperBox(
kStandardManifestUuid, "urn:uuid:legacy-c2pa", {signature_box});
return jumbf::EncodeSuperBox(kManifestStoreUuid, kManifestStoreLabel,
{test_manifest});
}
class FakeUuidGenerator : public UuidGenerator {
public:
Uuid Generate() const override {
if (first_) {
first_ = false;
return kUuid;
}
return kInstanceId;
}
private:
mutable bool first_ = true;
};
class ManifestAssemblerTest : public testing::Test {
protected:
ManifestAssemblerTest() {
manifest_assembler_ = std::make_unique<StandardManifestBoxAssembler>(
&uuid_generator_, &hash_factory_);
}
// Recreates the assembler targeting the given C2PA spec version.
void SetSpecVersion(SpecVersion spec_version) {
manifest_assembler_ = std::make_unique<StandardManifestBoxAssembler>(
&uuid_generator_, &hash_factory_, spec_version);
}
FakeUuidGenerator uuid_generator_;
Sha256HasherFactory hash_factory_;
std::unique_ptr<ManifestBoxAssembler> manifest_assembler_;
};
TEST_F(ManifestAssemblerTest, NoHardBindingInvalid) {
GenerateManifestStoreRequest request;
EXPECT_THAT(
manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("hard binding")));
}
TEST_F(ManifestAssemblerTest, PopulateBmffBasedHardBinding) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash { alg: "some alg" hash: "some hash" } }
actions { created {} }
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { alg: "some alg" hash: "some hash" }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions { actions { action: "c2pa.created" } }
)pb"),
.ingredient_assertions = {},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_)};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, PopulateActionsAssertionWithAllActionsIncluded) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash { alg: "some alg" hash: "some hash" } }
actions {
created {}
all_actions_included: true
}
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { alg: "some alg" hash: "some hash" }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
all_actions_included: true
actions { action: "c2pa.created" }
}
)pb"),
.ingredient_assertions = {},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_)};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, UsesInstanceIdFromRequest) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash { alg: "some alg" hash: "some hash" } }
actions { created {} }
instance_id: "11111111-1111-1111-1111-111111111111"
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.instance_id = "11111111-1111-1111-1111-111111111111",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { alg: "some alg" hash: "some hash" }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions { actions { action: "c2pa.created" } }
)pb"),
.ingredient_assertions = {},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_),
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, expected.instance_id);
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, PopulateBoxesBasedHardBinding) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding {
box_hash {
alg: "some alg"
boxes { names: "some name" names: "other name" hash: "some hash" }
}
}
actions { created {} }
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.boxes"
boxes_hash {
alg: "some alg"
boxes {
names: "some name"
names: "other name"
hash: "some hash"
pad: ""
pad2: ""
}
}
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions { actions { action: "c2pa.created" } }
)pb"),
.ingredient_assertions = {},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_),
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, PopulateDataBasedHardBinding) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding {
data_hash {
alg: "some alg"
hash: "some hash"
exclusions { start: 1 length: 10 }
}
}
actions { created {} }
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.data"
data_hash {
alg: "some alg"
hash: "some hash"
exclusions { start: 1 length: 10 }
pad: "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
}
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions { actions { action: "c2pa.created" } }
)pb"),
.ingredient_assertions = {},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_),
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, PopulateCollectionDataHashBasedHardBinding) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding {
collection_data_hash {
alg: "some alg"
uris { uri: "some uri" hash: "some hash" type: "some type" }
zip_central_directory_hash: "some hash"
}
}
actions { created {} }
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.collection.data"
collection_data_hash {
uris { uri: "some uri" hash: "some hash" type: "some type" }
alg: "some alg"
zip_central_directory_hash: "some hash"
}
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions { actions { action: "c2pa.created" } }
)pb"),
.ingredient_assertions = {},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_),
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, UnspecifiedInceptionActionInvalid) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
)pb");
EXPECT_THAT(manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("missing inception action")));
}
TEST_F(ManifestAssemblerTest, EmptyAction) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {}
}
ingredients { metadata { title: "ingredient0" } }
)pb");
EXPECT_THAT(
manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("empty action")));
}
TEST_F(ManifestAssemblerTest, PlacedAction) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {
placed {
action_metadata { description: "test placed description" }
ingredient_indexes: 1
ingredient_indexes: 2
}
}
}
ingredients { metadata { title: "ingredient0" } }
ingredients { metadata { title: "ingredient1" } }
ingredients { metadata { title: "ingredient2" } }
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(manifest_box.actions_assertion, EqualsProto(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
actions {
action: "c2pa.placed"
description: "test placed description"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__2"
}
}
}
}
)pb"));
EXPECT_THAT(
manifest_box.ingredient_assertions,
ElementsAre(
EqualsProto(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 { title: "ingredient0" relationship: "parentOf" }
)pb"),
EqualsProto(R"pb(
label: "c2pa.ingredient.v3__1"
ingredient_v3 { title: "ingredient1" relationship: "componentOf" }
)pb"),
EqualsProto(R"pb(
label: "c2pa.ingredient.v3__2"
ingredient_v3 { title: "ingredient2" relationship: "componentOf" }
)pb")));
}
TEST_F(ManifestAssemblerTest, LegacyManifestInIngredients) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {
placed {
action_metadata { description: "test placed description" }
ingredient_indexes: 1
ingredient_indexes: 2
}
}
}
ingredients {
metadata { title: "ingredient0" }
ingredient_validation_results {
manifest_store_bytes: ""
ingredient_validation_results {
active_manifest { successes { code: "test" } }
}
legacy_ingredient_metadata {
is_legacy_ingredient: true
unsupported_manifests: "unsupported manifest"
}
}
}
ingredients {
metadata { title: "ingredient1" }
ingredient_validation_results {
manifest_store_bytes: ""
ingredient_validation_results {
active_manifest { successes { code: "test" } }
}
legacy_ingredient_metadata {
is_legacy_ingredient: true
unsupported_manifests: "unsupported manifest"
}
}
}
ingredients {
metadata { title: "ingredient2" }
ingredient_validation_results {
manifest_store_bytes: ""
ingredient_validation_results {
active_manifest { successes { code: "test" } }
}
}
}
)pb");
std::string ingredient0_manifest_store_bytes =
CreateLegacyManifestStoreBytes();
std::string ingredient1_manifest_store_bytes =
CreateLegacyManifestStoreBytes();
std::string ingredient2_manifest_store_bytes =
CreateSimpleManifestStoreBytes();
request.mutable_ingredients(0)
->mutable_ingredient_validation_results()
->set_manifest_store_bytes(ingredient0_manifest_store_bytes);
request.mutable_ingredients(1)
->mutable_ingredient_validation_results()
->set_manifest_store_bytes(ingredient1_manifest_store_bytes);
request.mutable_ingredients(2)
->mutable_ingredient_validation_results()
->set_manifest_store_bytes(ingredient2_manifest_store_bytes);
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(manifest_box.actions_assertion, EqualsProto(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
actions {
action: "c2pa.placed"
description: "test placed description"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__2"
}
}
}
}
)pb"));
EXPECT_THAT(
manifest_box.ingredient_assertions,
ElementsAre(
EqualsProto(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 { title: "ingredient0" relationship: "parentOf" }
)pb"),
EqualsProto(R"pb(
label: "c2pa.ingredient.v3__1"
ingredient_v3 { title: "ingredient1" relationship: "componentOf" }
)pb"),
EqualsProto(R"pb(
label: "c2pa.ingredient.v3__2"
ingredient_v3 {
title: "ingredient2"
relationship: "componentOf"
validation_results {
active_manifest { successes { code: "test" } }
}
active_manifest {
url: "self#jumbf=/c2pa/urn:c2pa:11111111-1111-1111-1111-111111111111"
hash: "7#\034)n*\250\307>)(\263\336K\024\334R\002\'Q\252^\347\361\000\021T\334\256\320pf"
}
claim_signature {
url: "self#jumbf=/c2pa/urn:c2pa:11111111-1111-1111-1111-111111111111/c2pa.signature"
hash: "\017b6\027\253\306\235Q\350\246\311y\203sr@\256VE\267S\0137>\027#\377+\311U!l"
}
}
)pb")));
}
TEST_F(ManifestAssemblerTest, DigitalSourceTypeSetForUnknownProvenance) {
SetSpecVersion(SpecVersion::kC2pa_2_4);
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions { opened { ingredient_index: 0 } }
ingredients {
metadata { title: "ingredient0" }
ingredient_validation_results {
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
}
}
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(
manifest_box.ingredient_assertions, ElementsAre(EqualsProto(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "ingredient0"
relationship: "parentOf"
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
}
)pb")));
}
TEST_F(ManifestAssemblerTest, DigitalSourceTypeIgnoredBeforeSpec24) {
SetSpecVersion(SpecVersion::kC2pa_2_2);
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions { opened { ingredient_index: 0 } }
ingredients {
metadata { title: "ingredient0" }
ingredient_validation_results {
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
}
}
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(manifest_box.ingredient_assertions, ElementsAre(EqualsProto(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 { title: "ingredient0" relationship: "parentOf" }
)pb")));
}
TEST_F(ManifestAssemblerTest,
DigitalSourceTypeNotSetForIngredientWithoutDigitalSourceType) {
SetSpecVersion(SpecVersion::kC2pa_2_4);
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions { opened { ingredient_index: 0 } }
ingredients {
metadata { title: "ingredient0" }
ingredient_validation_results {}
}
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(manifest_box.ingredient_assertions, ElementsAre(EqualsProto(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 { title: "ingredient0" relationship: "parentOf" }
)pb")));
ASSERT_EQ(manifest_box.ingredient_assertions.size(), 1);
EXPECT_FALSE(manifest_box.ingredient_assertions[0]
.ingredient_v3()
.has_digital_source_type());
}
TEST_F(ManifestAssemblerTest,
DigitalSourceTypeIgnoredIfManifestStoreBytesPresent) {
SetSpecVersion(SpecVersion::kC2pa_2_4);
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions { opened { ingredient_index: 0 } }
ingredients {
metadata { title: "ingredient0" }
ingredient_validation_results {
manifest_store_bytes: ""
ingredient_validation_results {
active_manifest { successes { code: "test" } }
}
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
}
}
)pb");
std::string ingredient0_manifest_store_bytes =
CreateSimpleManifestStoreBytes();
request.mutable_ingredients(0)
->mutable_ingredient_validation_results()
->set_manifest_store_bytes(ingredient0_manifest_store_bytes);
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(
manifest_box.ingredient_assertions, ElementsAre(EqualsProto(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "ingredient0"
relationship: "parentOf"
validation_results { active_manifest { successes { code: "test" } } }
active_manifest {
url: "self#jumbf=/c2pa/urn:c2pa:11111111-1111-1111-1111-111111111111"
hash: "7#\034)n*\250\307>)(\263\336K\024\334R\002\'Q\252^\347\361\000\021T\334\256\320pf"
}
claim_signature {
url: "self#jumbf=/c2pa/urn:c2pa:11111111-1111-1111-1111-111111111111/c2pa.signature"
hash: "\017b6\027\253\306\235Q\350\246\311y\203sr@\256VE\267S\0137>\027#\377+\311U!l"
}
}
)pb")));
}
TEST_F(ManifestAssemblerTest, PlacedActionWithSharedIngredients) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {
placed {
action_metadata { multiple_instances: true }
ingredient_indexes: 1
ingredient_indexes: 2
ingredient_indexes: 1
}
}
actions { placed { ingredient_indexes: 1 } }
}
ingredients { metadata { title: "ingredient0" } }
ingredients { metadata { title: "ingredient1" } }
ingredients { metadata { title: "ingredient2" } }
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(manifest_box.actions_assertion, EqualsProto(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
actions {
action: "c2pa.placed"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__2"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
multiple_instances: true
}
}
actions {
action: "c2pa.placed"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
}
}
}
)pb"));
EXPECT_THAT(
manifest_box.ingredient_assertions,
ElementsAre(
EqualsProto(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 { title: "ingredient0" relationship: "parentOf" }
)pb"),
EqualsProto(R"pb(
label: "c2pa.ingredient.v3__1"
ingredient_v3 { title: "ingredient1" relationship: "componentOf" }
)pb"),
EqualsProto(R"pb(
label: "c2pa.ingredient.v3__2"
ingredient_v3 { title: "ingredient2" relationship: "componentOf" }
)pb")));
}
TEST_F(ManifestAssemblerTest,
PlacedActionWithSharedIngredientsWithSharedRedactions) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {
placed {
action_metadata { multiple_instances: true }
ingredient_indexes: 1
ingredient_indexes: 2
ingredient_indexes: 1
}
}
actions { placed { ingredient_indexes: 1 } }
}
ingredients {
metadata { title: "ingredient0" }
ingredient_validation_results {
redactions { uri: "uri1" reason: "reason1" }
redactions { uri: "uri2" reason: "reason2" }
}
}
ingredients {
metadata { title: "ingredient1" }
ingredient_validation_results {
redactions { uri: "uri2" reason: "reason2" }
}
}
ingredients {
metadata { title: "ingredient2" }
ingredient_validation_results {
redactions { uri: "uri2" reason: "reason2" }
}
}
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(manifest_box.actions_assertion, EqualsProto(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
actions {
action: "c2pa.placed"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__2"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
multiple_instances: true
}
}
actions {
action: "c2pa.placed"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
}
}
actions {
action: "c2pa.redacted"
reason: "reason1"
parameters { redacted_uri: "uri1" }
}
actions {
action: "c2pa.redacted"
reason: "reason2"
parameters { redacted_uri: "uri2" }
}
}
)pb"));
EXPECT_THAT(
manifest_box.ingredient_assertions,
ElementsAre(
EqualsProto(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 { title: "ingredient0" relationship: "parentOf" }
)pb"),
EqualsProto(R"pb(
label: "c2pa.ingredient.v3__1"
ingredient_v3 { title: "ingredient1" relationship: "componentOf" }
)pb"),
EqualsProto(R"pb(
label: "c2pa.ingredient.v3__2"
ingredient_v3 { title: "ingredient2" relationship: "componentOf" }
)pb")));
}
TEST_F(ManifestAssemblerTest, PlacedActionWithoutIngredients) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {
placed { action_metadata { description: "test placed description" } }
}
}
ingredients { metadata { title: "ingredient0" } }
)pb");
EXPECT_THAT(manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("missing ingredients in placed action")));
}
TEST_F(ManifestAssemblerTest, PlacedActionWithInvalidIngredientIndex) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {
placed {
action_metadata { description: "test placed description" }
ingredient_indexes: 1
}
}
}
ingredients { metadata { title: "ingredient0" } }
)pb");
EXPECT_THAT(manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("ingredient index out of range")));
}
TEST_F(ManifestAssemblerTest, RepackagedActionWithoutOpenedIngredient) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
created {}
actions {
repackaged {
action_metadata { description: "test repackaged description" }
}
}
}
)pb");
EXPECT_THAT(manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("requires an `opened` action")));
}
TEST_F(ManifestAssemblerTest, TranscodedActionWithoutOpenedIngredient) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
created {}
actions {
transcoded {
action_metadata { description: "test transcoded description" }
}
}
}
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(manifest_box.actions_assertion, EqualsProto(R"pb(
label: "c2pa.actions.v2"
actions {
actions { action: "c2pa.created" }
actions {
action: "c2pa.transcoded"
description: "test transcoded description"
}
}
)pb"));
}
TEST_F(ManifestAssemblerTest, RepackagedAction) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {
repackaged {
action_metadata { description: "test repackaged description" }
}
}
}
ingredients { metadata { title: "ingredient0" } }
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(manifest_box.actions_assertion, EqualsProto(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
actions {
action: "c2pa.repackaged"
description: "test repackaged description"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
}
)pb"));
EXPECT_THAT(manifest_box.ingredient_assertions, ElementsAre(EqualsProto(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 { title: "ingredient0" relationship: "parentOf" }
)pb")));
}
TEST_F(ManifestAssemblerTest, TranscodedAction) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {
transcoded {
action_metadata { description: "test transcoded description" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
}
ingredients { metadata { title: "ingredient0" } }
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(
manifest_box.actions_assertion, EqualsProto(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
actions {
action: "c2pa.transcoded"
description: "test transcoded description"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
}
)pb"));
EXPECT_THAT(manifest_box.ingredient_assertions, ElementsAre(EqualsProto(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 { title: "ingredient0" relationship: "parentOf" }
)pb")));
}
// Tests simple actions with no ingredient references or other custom fields.
TEST_F(ManifestAssemblerTest, SimpleActions) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
created {}
actions { added_text { action_metadata { description: "added_text" } } }
actions { converted { action_metadata { description: "converted" } } }
actions {
edited_metadata { action_metadata { description: "edited_metadata" } }
}
actions { unknown { action_metadata { description: "unknown" } } }
}
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(manifest_box.actions_assertion, EqualsProto(R"pb(
label: "c2pa.actions.v2"
actions {
actions { action: "c2pa.created" }
actions { action: "c2pa.addedText" description: "added_text" }
actions { action: "c2pa.converted" description: "converted" }
actions {
action: "c2pa.edited.metadata"
description: "edited_metadata"
}
actions { action: "c2pa.unknown" description: "unknown" }
}
)pb"));
EXPECT_THAT(manifest_box.ingredient_assertions, IsEmpty());
}
// Tests actions with digital source type.
TEST_F(ManifestAssemblerTest, DigitalSourceTypeActions) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
created {}
actions {
added_text {
action_metadata { description: "added_text" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
actions {
adjusted_color {
action_metadata { description: "adjusted_color" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
actions {
changed_speed {
action_metadata { description: "changed_speed" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
actions {
cropped {
action_metadata { description: "cropped" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
actions {
deleted {
action_metadata { description: "deleted" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
actions {
drawing {
action_metadata { description: "drawing" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
actions {
dubbed {
action_metadata { description: "dubbed" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
actions {
edited {
action_metadata { description: "edited" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
actions {
enhanced {
action_metadata { description: "enhanced" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
actions {
filtered {
action_metadata { description: "filtered" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
actions {
orientation {
action_metadata { description: "orientation" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
actions {
resized {
action_metadata { description: "resized" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
actions {
trimmed {
action_metadata { description: "trimmed" }
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
}
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(
manifest_box.actions_assertion, EqualsProto(R"pb(
label: "c2pa.actions.v2"
actions {
actions { action: "c2pa.created" }
actions {
action: "c2pa.addedText"
description: "added_text"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
actions {
action: "c2pa.adjustedColor"
description: "adjusted_color"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
actions {
action: "c2pa.changedSpeed"
description: "changed_speed"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
actions {
action: "c2pa.cropped"
description: "cropped"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
actions {
action: "c2pa.deleted"
description: "deleted"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
actions {
action: "c2pa.drawing"
description: "drawing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
actions {
action: "c2pa.dubbed"
description: "dubbed"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
actions {
action: "c2pa.edited"
description: "edited"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
actions {
action: "c2pa.enhanced"
description: "enhanced"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
actions {
action: "c2pa.filtered"
description: "filtered"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
actions {
action: "c2pa.orientation"
description: "orientation"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
actions {
action: "c2pa.resized"
description: "resized"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
actions {
action: "c2pa.trimmed"
description: "trimmed"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
}
}
)pb"));
EXPECT_THAT(manifest_box.ingredient_assertions, IsEmpty());
}
TEST_F(ManifestAssemblerTest, ConflictingIngredientRelationships) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {
placed {
action_metadata { description: "test placed description" }
ingredient_indexes: 0
}
}
}
ingredients { metadata { title: "ingredient0" } }
)pb");
EXPECT_THAT(manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Inconsistent use of ingredient")));
}
TEST_F(ManifestAssemblerTest, HasCreatedAction) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
created {
action_metadata {
software_agent {}
description: "created for testing"
}
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
}
}
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash {}
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
software_agent {}
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
}
}
)pb"),
.ingredient_assertions = {},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_),
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, HasCreatedActionWithInputToIngredients) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
created {
action_metadata {
software_agent {}
description: "created for testing"
}
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
input_ingredient_indexes: 0
input_ingredient_indexes: 1
}
}
ingredients { metadata { title: "test ingredient 1" } }
ingredients { metadata { title: "test ingredient 2" } }
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash {}
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
software_agent {}
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
}
}
}
)pb"),
.ingredient_assertions =
{
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient 1"
relationship: "inputTo"
}
)pb"),
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3__1"
ingredient_v3 {
title: "test ingredient 2"
relationship: "inputTo"
}
)pb"),
},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_),
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, EditedActionWithInputToIngredient) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {
edited {
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
input_ingredient_indexes: 1
}
}
}
ingredients { metadata { title: "test ingredient 0" } }
ingredients { metadata { title: "test ingredient 1" } }
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash {}
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
actions {
action: "c2pa.edited"
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
}
}
}
)pb"),
.ingredient_assertions =
{
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient 0"
relationship: "parentOf"
}
)pb"),
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3__1"
ingredient_v3 {
title: "test ingredient 1"
relationship: "inputTo"
}
)pb"),
},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_),
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, FilteredActionWithInputToIngredient) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {
filtered {
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
input_ingredient_indexes: 1
}
}
}
ingredients { metadata { title: "test ingredient 0" } }
ingredients { metadata { title: "test ingredient 1" } }
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash {}
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
actions {
action: "c2pa.filtered"
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
}
}
}
)pb"),
.ingredient_assertions =
{
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient 0"
relationship: "parentOf"
}
)pb"),
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3__1"
ingredient_v3 {
title: "test ingredient 1"
relationship: "inputTo"
}
)pb"),
},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_),
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, ParentIngredientReferencedAsInputTo) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened { ingredient_index: 0 }
actions {
edited {
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
input_ingredient_indexes: 0
}
}
}
ingredients { metadata { title: "test ingredient 0" } }
)pb");
EXPECT_THAT(manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Inconsistent use of ingredient")));
}
TEST_F(ManifestAssemblerTest, OpenedActionWithoutIngredientIndex) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened {
action_metadata {
software_agent {}
description: "opened for testing"
}
}
}
)pb");
EXPECT_THAT(manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("requires an ingredient index.")));
}
TEST_F(ManifestAssemblerTest, OpenedActionWithInvalidIngredientIndex) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened {
action_metadata {
software_agent {}
description: "opened for testing"
}
ingredient_index: 1
}
}
ingredients { metadata { title: "test ingredient" } }
)pb");
EXPECT_THAT(manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("ingredient index out of range")));
}
TEST_F(ManifestAssemblerTest, OpenedActionWithUnreferencedIngredient) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened {
action_metadata {
software_agent {}
description: "opened for testing"
}
ingredient_index: 1
}
}
ingredients { metadata { title: "test ingredient" } }
ingredients { metadata { title: "test ingredient at index 1" } }
)pb");
EXPECT_THAT(
manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("ingredient is not referenced by any action")));
}
TEST_F(ManifestAssemblerTest, HasSimpleOpenedAction) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened {
action_metadata {
software_agent {}
description: "opened for testing"
}
ingredient_index: 0
}
}
ingredients { metadata { title: "test ingredient" } }
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash {}
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
software_agent {}
description: "opened for testing"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
}
)pb"),
.ingredient_assertions = {ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 { title: "test ingredient" relationship: "parentOf" }
)pb")},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_),
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, HasOpenedAction) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened {
action_metadata {
software_agent {}
description: "opened for testing"
}
ingredient_index: 0
}
}
ingredients {
metadata {
title: "test ingredient"
media_type: "video/mp4"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
}
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash {}
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
software_agent {}
description: "opened for testing"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
}
)pb"),
.ingredient_assertions = {ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
)pb")},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_),
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, HasC2paOpenedAction) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened {
action_metadata {
software_agent {}
description: "opened for testing"
}
ingredient_index: 0
}
}
ingredients {
metadata {
title: "test ingredient"
media_type: "video/mp4"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
ingredient_validation_results {
manifest_store_bytes: ""
ingredient_validation_results {
active_manifest { successes { code: "test" } }
}
}
}
)pb");
std::string ingredient_manifest_store_bytes =
CreateSimpleManifestStoreBytes();
request.mutable_ingredients(0)
->mutable_ingredient_validation_results()
->set_manifest_store_bytes(ingredient_manifest_store_bytes);
ManifestStoreBuilder manifest_store_builder(&hash_factory_);
ABSL_ASSERT_OK(manifest_store_builder.AddIngredientManifestStore(
request.ingredients(0)
.ingredient_validation_results()
.manifest_store_bytes()));
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash {}
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
software_agent {}
description: "opened for testing"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
}
)pb"),
.ingredient_assertions = {ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
active_manifest {
url: "self#jumbf=/c2pa/urn:c2pa:11111111-1111-1111-1111-111111111111"
hash: "7#\034)n*\250\307>)(\263\336K\024\334R\002\'Q\252^\347\361\000\021T\334\256\320pf"
}
claim_signature {
url: "self#jumbf=/c2pa/urn:c2pa:11111111-1111-1111-1111-111111111111/c2pa.signature"
hash: "\017b6\027\253\306\235Q\350\246\311y\203sr@\256VE\267S\0137>\027#\377+\311U!l"
}
validation_results { active_manifest { successes { code: "test" } } }
}
)pb")},
.manifest_store_builder = manifest_store_builder,
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, HasC2paOpenedActionNoManifestStoreBytes) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened {
action_metadata {
software_agent {}
description: "opened for testing"
}
ingredient_index: 0
}
}
ingredients {
metadata {
title: "test ingredient"
media_type: "video/mp4"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
ingredient_validation_results {}
}
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash {}
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
software_agent {}
description: "opened for testing"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
}
)pb"),
.ingredient_assertions = {ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
)pb")},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_),
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
}
TEST_F(ManifestAssemblerTest, HasC2paOpenedActionMissingValidationResults) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened {
action_metadata {
software_agent {}
description: "opened for testing"
}
ingredient_index: 0
}
}
ingredients {
metadata {
title: "test ingredient"
media_type: "video/mp4"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
ingredient_validation_results { manifest_store_bytes: "" }
}
)pb");
std::string ingredient_manifest_store_bytes =
CreateSimpleManifestStoreBytes();
request.mutable_ingredients(0)
->mutable_ingredient_validation_results()
->set_manifest_store_bytes(ingredient_manifest_store_bytes);
EXPECT_THAT(manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("validation results cannot be empty")));
}
TEST_F(ManifestAssemblerTest, MediaTypeFromIngredientValidationResults) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
opened {
action_metadata {
software_agent {}
description: "opened for testing"
}
ingredient_index: 0
}
}
ingredients {
metadata { title: "test ingredient" }
ingredient_validation_results { media_type: "video/mp4" }
}
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_THAT(manifest_box.ingredient_assertions, ElementsAre(EqualsProto(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
}
)pb")));
}
TEST_F(ManifestAssemblerTest, CreatesWithMultiPartAssertion) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding {
bmff_based_hash {}
part_bindings {
bmff_based_hash {}
location { byte_offset: 0 length: 10 }
}
part_bindings {
bmff_based_hash {}
location { byte_offset: 10 length: 10 }
is_optional: true
}
}
actions {
created {
action_metadata {
software_agent {}
description: "created for testing"
}
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
}
}
)pb");
ManifestBox expected = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash {}
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
software_agent {}
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
}
}
)pb"),
.part_hash_assertions = {ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3.part"
bmff_based_hash {}
)pb"),
ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3.part__1"
bmff_based_hash {}
)pb")},
.multi_part_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.multi-asset"
multi_asset_hash {
parts {
location { byte_offset: 0 length: 10 }
hash_assertion {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3.part"
}
is_optional: false
}
parts {
location { byte_offset: 10 length: 10 }
hash_assertion {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3.part__1"
}
is_optional: true
}
pad: "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
pad2: ""
}
)pb"),
.ingredient_assertions = {},
.manifest_store_builder = ManifestStoreBuilder(&hash_factory_),
};
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
EXPECT_EQ(manifest_box.label, expected.label);
EXPECT_EQ(manifest_box.instance_id, kInstanceId.ToString());
EXPECT_THAT(manifest_box.hard_binding_assertion,
EqualsProto(expected.hard_binding_assertion));
EXPECT_THAT(manifest_box.actions_assertion,
EqualsProto(expected.actions_assertion));
EXPECT_THAT(manifest_box.ingredient_assertions,
Pointwise(EqualsProto(), expected.ingredient_assertions));
EXPECT_THAT(manifest_box.part_hash_assertions,
Pointwise(EqualsProto(), expected.part_hash_assertions));
EXPECT_THAT(manifest_box.multi_part_assertion,
Optional(EqualsProto(*expected.multi_part_assertion)));
}
TEST_F(ManifestAssemblerTest, EmptyGatheredAssertionLabelInvalid) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
created {
action_metadata {
software_agent {}
description: "created for testing"
}
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
}
}
gathered_assertions { label: "" data: "{}" content_type: CONTENT_TYPE_JSON }
)pb");
EXPECT_THAT(manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("non-empty label")));
}
TEST_F(ManifestAssemblerTest, InvalidGatheredAssertionContentTypeInvalid) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
created {
action_metadata {
software_agent {}
description: "created for testing"
}
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
}
}
gathered_assertions {
label: "com.example.gathered"
data: "{}"
content_type: CONTENT_TYPE_UNSPECIFIED
}
)pb");
EXPECT_THAT(manifest_assembler_->Assemble(request),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("content type must be specified")));
}
TEST_F(ManifestAssemblerTest, PopulateGatheredAssertions) {
GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb(
hard_binding { bmff_based_hash {} }
actions {
created {
action_metadata {
software_agent {}
description: "created for testing"
}
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
}
}
gathered_assertions {
label: "com.example.gathered.1"
data: "{\"foo\": 1}"
content_type: CONTENT_TYPE_JSON
}
gathered_assertions {
label: "com.example.gathered.2"
data: "{\"bar\": 2}"
content_type: CONTENT_TYPE_JSON
}
)pb");
auto manifest_box_or = manifest_assembler_->Assemble(request);
ABSL_ASSERT_OK(manifest_box_or);
const ManifestBox& manifest_box = *manifest_box_or;
ASSERT_EQ(manifest_box.gathered_assertions.size(), 2);
EXPECT_THAT(manifest_box.gathered_assertions,
Pointwise(EqualsProto(), request.gathered_assertions()));
}
} // namespace
} // namespace credentio