|
14 | 14 | #include <gtest/gtest.h> |
15 | 15 | #include <nlohmann/json.hpp> |
16 | 16 | #include <algorithm> |
| 17 | +#include <fstream> |
17 | 18 | #include <functional> |
18 | 19 | #include <iostream> |
19 | 20 | #include <memory> |
@@ -5858,3 +5859,171 @@ TEST_F(BuilderTest, CreateIntentViaContext) |
5858 | 5859 | } |
5859 | 5860 | ASSERT_TRUE(found_created) << "Expected c2pa.created action in active manifest"; |
5860 | 5861 | } |
| 5862 | + |
| 5863 | +TEST_F(BuilderTest, ArchiveIngredientRoundTripAndReuse) |
| 5864 | +{ |
| 5865 | + auto context = c2pa::Context(); |
| 5866 | + auto source_path = c2pa_test::get_fixture_path("A.jpg"); |
| 5867 | + |
| 5868 | + // Shared instance_id links the parentOf ingredient to a c2pa.opened action. |
| 5869 | + // Thos will make the archive valid |
| 5870 | + std::string instance_id = "xmp:iid:archive-roundtrip-0001"; |
| 5871 | + json manifest_json = { |
| 5872 | + {"claim_generator_info", json::array({{{"name", "c2pa-test"}, {"version", "1.0"}}})}, |
| 5873 | + {"assertions", json::array({ |
| 5874 | + { |
| 5875 | + {"label", "c2pa.actions"}, |
| 5876 | + {"data", { |
| 5877 | + {"actions", json::array({ |
| 5878 | + { |
| 5879 | + {"action", "c2pa.opened"}, |
| 5880 | + {"parameters", { |
| 5881 | + {"ingredientIds", json::array({instance_id})} |
| 5882 | + }} |
| 5883 | + } |
| 5884 | + })} |
| 5885 | + }} |
| 5886 | + } |
| 5887 | + })} |
| 5888 | + }; |
| 5889 | + |
| 5890 | + // 1. Builder1: add parentOf ingredient linked to c2pa.opened, no signing. |
| 5891 | + auto builder1 = c2pa::Builder(context, manifest_json.dump()); |
| 5892 | + json ingredient = { |
| 5893 | + {"title", "A.jpg"}, |
| 5894 | + {"relationship", "parentOf"}, |
| 5895 | + {"instance_id", instance_id} |
| 5896 | + }; |
| 5897 | + builder1.add_ingredient(ingredient.dump(), source_path); |
| 5898 | + |
| 5899 | + // 2. Archive to .c2pa file. |
| 5900 | + auto archive_path = get_temp_path("roundtrip-ingredient.c2pa"); |
| 5901 | + ASSERT_NO_THROW(builder1.to_archive(archive_path)); |
| 5902 | + ASSERT_TRUE(fs::exists(archive_path)); |
| 5903 | + ASSERT_GT(fs::file_size(archive_path), 0u); |
| 5904 | + |
| 5905 | + // 3. Read archive back; manifest must parse and carry the ingredient. |
| 5906 | + std::ifstream archive_in(archive_path, std::ios::binary); |
| 5907 | + c2pa::Reader archive_reader(context, "application/c2pa", archive_in); |
| 5908 | + std::string archive_json; |
| 5909 | + ASSERT_NO_THROW(archive_json = archive_reader.json()); |
| 5910 | + |
| 5911 | + auto parsed = json::parse(archive_json); |
| 5912 | + ASSERT_TRUE(parsed.contains("active_manifest")); |
| 5913 | + std::string active = parsed["active_manifest"]; |
| 5914 | + auto ingredients = parsed["manifests"][active]["ingredients"]; |
| 5915 | + ASSERT_EQ(ingredients.size(), 1u); |
| 5916 | + EXPECT_EQ(ingredients[0]["title"], "A.jpg"); |
| 5917 | + EXPECT_EQ(ingredients[0]["relationship"], "parentOf"); |
| 5918 | + |
| 5919 | + // 4. Builder2: fresh manifest (training.json) + add the archive as an ingredient. |
| 5920 | + auto builder2_manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); |
| 5921 | + auto builder2 = c2pa::Builder(context, builder2_manifest); |
| 5922 | + json ingredient_override = { |
| 5923 | + {"title", "A.jpg"}, |
| 5924 | + {"relationship", "componentOf"} |
| 5925 | + }; |
| 5926 | + ASSERT_NO_THROW(builder2.add_ingredient(ingredient_override.dump(), archive_path)); |
| 5927 | + |
| 5928 | + // 5. Sign builder2 and validate signed output. |
| 5929 | + auto signer = c2pa_test::create_test_signer(); |
| 5930 | + auto output_path = get_temp_path("archive_reused_output.jpg"); |
| 5931 | + std::vector<unsigned char> manifest_data; |
| 5932 | + ASSERT_NO_THROW(manifest_data = builder2.sign(source_path, output_path, signer)); |
| 5933 | + ASSERT_FALSE(manifest_data.empty()); |
| 5934 | + |
| 5935 | + auto out_reader = c2pa::Reader(context, output_path); |
| 5936 | + std::string out_json; |
| 5937 | + ASSERT_NO_THROW(out_json = out_reader.json()); |
| 5938 | + auto out_parsed = json::parse(out_json); |
| 5939 | + std::string out_active = out_parsed["active_manifest"]; |
| 5940 | + auto out_ingredients = out_parsed["manifests"][out_active]["ingredients"]; |
| 5941 | + ASSERT_EQ(out_ingredients.size(), 1u); |
| 5942 | + EXPECT_EQ(out_ingredients[0]["title"], "A.jpg"); |
| 5943 | + EXPECT_EQ(out_ingredients[0]["relationship"], "componentOf"); |
| 5944 | +} |
| 5945 | + |
| 5946 | +TEST_F(BuilderTest, ArchiveIngredientWithProvenanceRoundTripAndReuse) |
| 5947 | +{ |
| 5948 | + auto context = c2pa::Context(); |
| 5949 | + auto source_path = c2pa_test::get_fixture_path("A.jpg"); |
| 5950 | + auto ingredient_path = c2pa_test::get_fixture_path("C.jpg"); |
| 5951 | + |
| 5952 | + // Shared instance_id links the parentOf ingredient to a c2pa.opened action. |
| 5953 | + std::string instance_id = "xmp:iid:archive-roundtrip-provenance-0001"; |
| 5954 | + |
| 5955 | + json manifest_json = { |
| 5956 | + {"claim_generator_info", json::array({{{"name", "c2pa-test"}, {"version", "1.0"}}})}, |
| 5957 | + {"assertions", json::array({ |
| 5958 | + { |
| 5959 | + {"label", "c2pa.actions"}, |
| 5960 | + {"data", { |
| 5961 | + {"actions", json::array({ |
| 5962 | + { |
| 5963 | + {"action", "c2pa.opened"}, |
| 5964 | + {"parameters", { |
| 5965 | + {"ingredientIds", json::array({instance_id})} |
| 5966 | + }} |
| 5967 | + } |
| 5968 | + })} |
| 5969 | + }} |
| 5970 | + } |
| 5971 | + })} |
| 5972 | + }; |
| 5973 | + |
| 5974 | + // 1. Builder1: add parentOf ingredient (C.jpg is C2PA-signed, has provenance). |
| 5975 | + auto builder1 = c2pa::Builder(context, manifest_json.dump()); |
| 5976 | + json ingredient = { |
| 5977 | + {"title", "C.jpg"}, |
| 5978 | + {"relationship", "parentOf"}, |
| 5979 | + {"instance_id", instance_id} |
| 5980 | + }; |
| 5981 | + builder1.add_ingredient(ingredient.dump(), ingredient_path); |
| 5982 | + |
| 5983 | + // 2. Archive to .c2pa file. |
| 5984 | + auto archive_path = get_temp_path("roundtrip-provenance-ingredient.c2pa"); |
| 5985 | + ASSERT_NO_THROW(builder1.to_archive(archive_path)); |
| 5986 | + ASSERT_TRUE(fs::exists(archive_path)); |
| 5987 | + ASSERT_GT(fs::file_size(archive_path), 0u); |
| 5988 | + |
| 5989 | + // 3. Read archive back; manifest must parse and carry the ingredient. |
| 5990 | + std::ifstream archive_in(archive_path, std::ios::binary); |
| 5991 | + c2pa::Reader archive_reader(context, "application/c2pa", archive_in); |
| 5992 | + std::string archive_json; |
| 5993 | + ASSERT_NO_THROW(archive_json = archive_reader.json()); |
| 5994 | + std::cout << archive_json << std::endl; |
| 5995 | + |
| 5996 | + auto parsed = json::parse(archive_json); |
| 5997 | + ASSERT_TRUE(parsed.contains("active_manifest")); |
| 5998 | + std::string active = parsed["active_manifest"]; |
| 5999 | + auto ingredients = parsed["manifests"][active]["ingredients"]; |
| 6000 | + ASSERT_EQ(ingredients.size(), 1u); |
| 6001 | + EXPECT_EQ(ingredients[0]["title"], "C.jpg"); |
| 6002 | + EXPECT_EQ(ingredients[0]["relationship"], "parentOf"); |
| 6003 | + |
| 6004 | + // 4. Builder2: fresh manifest + add the archive as an ingredient. |
| 6005 | + auto builder2_manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); |
| 6006 | + auto builder2 = c2pa::Builder(context, builder2_manifest); |
| 6007 | + json ingredient_override = { |
| 6008 | + {"title", "C.jpg"}, |
| 6009 | + {"relationship", "componentOf"} |
| 6010 | + }; |
| 6011 | + ASSERT_NO_THROW(builder2.add_ingredient(ingredient_override.dump(), archive_path)); |
| 6012 | + |
| 6013 | + // 5. Sign builder2 and validate signed output. |
| 6014 | + auto signer = c2pa_test::create_test_signer(); |
| 6015 | + auto output_path = get_temp_path("archive_provenance_reused_output.jpg"); |
| 6016 | + std::vector<unsigned char> manifest_data; |
| 6017 | + ASSERT_NO_THROW(manifest_data = builder2.sign(source_path, output_path, signer)); |
| 6018 | + ASSERT_FALSE(manifest_data.empty()); |
| 6019 | + |
| 6020 | + auto out_reader = c2pa::Reader(context, output_path); |
| 6021 | + std::string out_json; |
| 6022 | + ASSERT_NO_THROW(out_json = out_reader.json()); |
| 6023 | + auto out_parsed = json::parse(out_json); |
| 6024 | + std::string out_active = out_parsed["active_manifest"]; |
| 6025 | + auto out_ingredients = out_parsed["manifests"][out_active]["ingredients"]; |
| 6026 | + ASSERT_EQ(out_ingredients.size(), 1u); |
| 6027 | + EXPECT_EQ(out_ingredients[0]["title"], "C.jpg"); |
| 6028 | + EXPECT_EQ(out_ingredients[0]["relationship"], "componentOf"); |
| 6029 | +} |
0 commit comments