From 6b5a30f3ce57f38bc1fab6eb6aabe568c2a0b4bf Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 4 Jun 2026 11:10:14 -0700 Subject: [PATCH 1/6] fix: API updates --- CMakeLists.txt | 2 +- README.md | 25 ++++ compile_commands.json | 1 + docs/selective-manifests.md | 2 +- include/c2pa.hpp | 33 ----- src/c2pa_core.cpp | 80 ------------ tests/builder.test.cpp | 236 +++++------------------------------- tests/c-app-test/test.c | 37 +----- tests/read_file.test.cpp | 73 ----------- 9 files changed, 64 insertions(+), 425 deletions(-) create mode 120000 compile_commands.json delete mode 100644 tests/read_file.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bb8b6fe..4962bacd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ cmake_minimum_required(VERSION 3.27) # This is the current version of this C++ project -project(c2pa-c VERSION 0.23.13) +project(c2pa-c VERSION 0.23.14) # Set the version of the c2pa_rs library used set(C2PA_VERSION "0.85.2") diff --git a/README.md b/README.md index 790eca75..bfc49a04 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,31 @@ Build the [unit tests](https://github.com/contentauth/c2pa-cpp/tree/main/tests) make test ``` +#### Running the sanitizer build on macOS + +`make test-san` builds the tests with AddressSanitizer/UBSan. On recent macOS versions the +AddressSanitizer runtime shipped with Xcode's AppleClang can abort at process start with: + +``` +AddressSanitizer: CHECK failed: sanitizer_malloc_mac.inc:189 "((!asan_init_is_running)) != (0)" +``` + +This is an incompatibility between the older AppleClang sanitizer runtime and the host +malloc; it is not specific to this project. Setting `MallocNanoZone=0` or preloading the ASan +runtime via `DYLD_INSERT_LIBRARIES` does **not** resolve it. The fix is to build the +sanitizer target with a newer LLVM/Clang (for example, Homebrew's `llvm`): + +```sh +brew install llvm +cmake -S . -B build/debug -G Ninja -DCMAKE_BUILD_TYPE=Debug -DENABLE_SANITIZERS=ON \ + -DCMAKE_C_COMPILER="$(brew --prefix llvm)/bin/clang" \ + -DCMAKE_CXX_COMPILER="$(brew --prefix llvm)/bin/clang++" +cmake --build build/debug +ctest --test-dir build/debug --output-on-failure +``` + +The Rust `c2pa_c` library does not need to be sanitizer-instrumented for this to work. + ### Building API documentation API documentation generated by Doxygen is automatically built on each PR. diff --git a/compile_commands.json b/compile_commands.json new file mode 120000 index 00000000..7c1ac711 --- /dev/null +++ b/compile_commands.json @@ -0,0 +1 @@ +build/debug/compile_commands.json \ No newline at end of file diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index ff674b8b..8cd7e227 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -419,7 +419,7 @@ for (auto& assertion : manifest["assertions"]) { | --- | --- | --- | | **Who controls it** | Caller (any string) | Caller (any string, or from XMP metadata) | | **Priority for linking** | Primary: checked first | Fallback: used when label is absent/empty | -| **When to use** | JSON-defined manifests where the caller controls the ingredient definition | Programmatic workflows using `read_ingredient_file()` or XMP-based IDs | +| **When to use** | JSON-defined manifests where the caller controls the ingredient definition | Programmatic workflows using `Builder::add_ingredient()` or XMP-based IDs | | **Survives signing** | SDK may reassign the actual assertion label | Unchanged | | **Stable across rebuilds** | The caller controls the build-time value; the post-signing label may change | Yes, always the same set value | diff --git a/include/c2pa.hpp b/include/c2pa.hpp index 53755447..d5740d1f 100644 --- a/include/c2pa.hpp +++ b/include/c2pa.hpp @@ -541,39 +541,6 @@ namespace c2pa [[deprecated("Use Context::from_json() or Context::from_settings() instead")]] void C2PA_CPP_API load_settings(const std::string& data, const std::string& format); - /// @brief Read a file and return the manifest JSON. - /// @param source_path The path to the file to read. - /// @param data_dir Optional directory to store binary resources. - /// @return Optional string containing the manifest JSON if a manifest was found. - /// @throws C2paException for errors encountered by the C2PA library. - /// @deprecated Use Reader object instead. - [[deprecated("Use Reader object instead")]] - std::optional C2PA_CPP_API read_file(const std::filesystem::path &source_path, const std::optional data_dir = std::nullopt); - - /// @brief Read a file and return an ingredient JSON. - /// @param source_path The path to the file to read. - /// @param data_dir The directory to store binary resources. - /// @return String containing the ingredient JSON. - /// @throws C2paException for errors encountered by the C2PA library. - /// @deprecated Use Reader and Builder.add_ingredient instead. - [[deprecated("Use Reader and Builder.add_ingredient")]] - std::string C2PA_CPP_API read_ingredient_file(const std::filesystem::path &source_path, const std::filesystem::path &data_dir); - - /// @brief Add a manifest and sign a file. - /// @param source_path The path to the asset to be signed. - /// @param dest_path The path to write the signed file to. - /// @param manifest The manifest JSON to add to the file. - /// @param signer_info The signer info to use for signing. - /// @param data_dir Optional directory to store binary resources. - /// @throws C2paException for errors encountered by the C2PA library. - /// @deprecated Use Builder.sign instead. - [[deprecated("Use Builder.sign instead")]] - void C2PA_CPP_API sign_file(const std::filesystem::path &source_path, - const std::filesystem::path &dest_path, - const char *manifest, - SignerInfo *signer_info, - const std::optional data_dir = std::nullopt); - /// @defgroup StreamWrappers Stream wrappers for C2PA C API /// @brief C++ stream types that adapt stream types to C2paStream. /// diff --git a/src/c2pa_core.cpp b/src/c2pa_core.cpp index e379c2a0..aaaf0df7 100644 --- a/src/c2pa_core.cpp +++ b/src/c2pa_core.cpp @@ -63,84 +63,4 @@ namespace c2pa } } - /// Reads a file and returns the manifest json as a C2pa::String. - /// @param source_path the path to the file to read. - /// @param data_dir the directory to store binary resources (optional). - /// @return a std::string containing the manifest json if a manifest was found. - /// @throws a C2pa::C2paException for errors encountered by the C2PA library. - [[deprecated("Use stream APIs instead: Reader to read data, combined with Builder to manage ingredients")]] - std::optional read_file(const std::filesystem::path &source_path, const std::optional data_dir) - { - const char* dir_ptr = nullptr; - std::string dir_str; - if (data_dir.has_value()) { - auto u = data_dir.value().u8string(); - dir_str = std::string(u.begin(), u.end()); - dir_ptr = dir_str.c_str(); - } - - auto src_u8 = source_path.u8string(); - std::string src_str(src_u8.begin(), src_u8.end()); - char *result = c2pa_read_file(src_str.c_str(), dir_ptr); - - if (result == nullptr) - { - auto C2paException = c2pa::C2paException(); - if (detail::error_indicates_manifest_not_found(C2paException.what())) - { - return std::nullopt; - } - throw c2pa::C2paException(); - } - std::string str(result); - c2pa_free(result); - return str; - } - - /// Reads a file and returns an ingredient JSON as a C2pa::String. - /// @param source_path the path to the file to read. - /// @param data_dir the directory to store binary resources. - /// @return a std::string containing the ingredient json. - /// @throws a C2pa::C2paException for errors encountered by the C2PA library. - [[deprecated("Use stream APIs instead: add_ingredient on the Builder")]] - std::string read_ingredient_file(const std::filesystem::path &source_path, const std::filesystem::path &data_dir) - { - auto src_u8 = source_path.u8string(); - auto dir_u8 = data_dir.u8string(); - return detail::c_string_to_string( - c2pa_read_ingredient_file(std::string(src_u8.begin(), src_u8.end()).c_str(), - std::string(dir_u8.begin(), dir_u8.end()).c_str())); - } - - /// Adds the manifest and signs a file. - // source_path: path to the asset to be signed - // dest_path: the path to write the signed file to - // manifest: the manifest json to add to the file - // signer_info: the signer info to use for signing - // data_dir: the directory to store binary resources (optional) - // Throws a C2pa::C2paException for errors encountered by the C2PA library - [[deprecated("Use stream APIs instead: sign on Builder")]] - void sign_file(const std::filesystem::path &source_path, - const std::filesystem::path &dest_path, - const char *manifest, - c2pa::SignerInfo *signer_info, - const std::optional data_dir) - { - auto src_u8 = source_path.u8string(); - auto dst_u8 = dest_path.u8string(); - std::string src_str(src_u8.begin(), src_u8.end()); - std::string dst_str(dst_u8.begin(), dst_u8.end()); - std::string dir_str; - if (data_dir.has_value()) { - auto u = data_dir.value().u8string(); - dir_str = std::string(u.begin(), u.end()); - } - - char *result = c2pa_sign_file(src_str.c_str(), dst_str.c_str(), manifest, signer_info, dir_str.c_str()); - if (result == nullptr) - { - throw c2pa::C2paException(); - } - c2pa_free(result); - } } // namespace c2pa diff --git a/tests/builder.test.cpp b/tests/builder.test.cpp index de74d8b6..95ef03cf 100644 --- a/tests/builder.test.cpp +++ b/tests/builder.test.cpp @@ -1974,105 +1974,6 @@ TEST_F(BuilderTest, SignWithoutTimestamping) ASSERT_FALSE(active_manifest["signature_info"].contains("time")); } -TEST_F(BuilderTest, ReadIngredientFile) -{ - fs::path current_dir = fs::path(__FILE__).parent_path(); - - // Construct the path to the test fixture - fs::path source_path = current_dir / "../tests/fixtures/A.jpg"; - - // Get temp directory for ingredient data - fs::path temp_dir = get_temp_dir("read_ingredient_a"); - - // Test that the function can read ingredient file successfully - std::string result; - ASSERT_NO_THROW(result = c2pa::read_ingredient_file(source_path, temp_dir)); - - // Verify that the result is not empty - ASSERT_FALSE(result.empty()); - - // Expected JSON structure: - // The result should contain at least the following structure: - // { - // "title": "A.jpg", - // "format": "image/jpeg", - // "thumbnail": { - // ... more goes here (identifier and hash) - // ... but we don't check these in this test - // }, - // "relationship": "componentOf" - // } - - ASSERT_TRUE(result.find("\"title\"") != std::string::npos); - ASSERT_TRUE(result.find("\"A.jpg\"") != std::string::npos); - - ASSERT_TRUE(result.find("\"format\"") != std::string::npos); - ASSERT_TRUE(result.find("\"image/jpeg\"") != std::string::npos); - - ASSERT_TRUE(result.find("\"thumbnail\"") != std::string::npos); - - ASSERT_TRUE(result.find("\"relationship\"") != std::string::npos); - ASSERT_TRUE(result.find("\"componentOf\"") != std::string::npos); -} - -TEST_F(BuilderTest, ReadIngredientFileWhoHasAManifestStore) -{ - fs::path current_dir = fs::path(__FILE__).parent_path(); - - // Construct the path to the test fixture - // C has a manifest store attached - fs::path source_path = current_dir / "../tests/fixtures/C.jpg"; - - // Get temp directory for ingredient data - fs::path temp_dir = get_temp_dir("read_ingredient_c"); - - // Test that the function can read ingredient file successfully - std::string result; - ASSERT_NO_THROW(result = c2pa::read_ingredient_file(source_path, temp_dir)); - - // The expected JSON structure for an ingredient with a manifest - // is extended, as there are additional fields. - // So, the result should contain at least the following structure: - // { - // "title": "C.jpg", - // "format": "image/jpeg", - // "thumbnail": { - // "format": "image/jpeg", - // ... more goes here (identifier and hash) - // ... but we don't check these in this test - // }, - // "relationship": "componentOf", - // "active_manifest": "contentauth:urn:uuid:c85a2b90-f1a0-4aa4-b17f-f938b475804e", - // "validation_results": { ... more goes here ... } - // "manifest_data": { - // "format": "application/c2pa", - // "identifier": not checked in the test, value changes after multiple - // runs during debugs to be unique because we reuse the - // same dir holding resources - // } - // } - - ASSERT_TRUE(result.find("\"title\"") != std::string::npos); - ASSERT_TRUE(result.find("\"C.jpg\"") != std::string::npos); - - ASSERT_TRUE(result.find("\"format\"") != std::string::npos); - ASSERT_TRUE(result.find("\"image/jpeg\"") != std::string::npos); - - ASSERT_TRUE(result.find("\"thumbnail\"") != std::string::npos); - - ASSERT_TRUE(result.find("\"relationship\"") != std::string::npos); - ASSERT_TRUE(result.find("\"componentOf\"") != std::string::npos); - - // Additional fields because the ingredient has a manifest store attached - ASSERT_TRUE(result.find("\"active_manifest\"") != std::string::npos); - ASSERT_TRUE(result.find("\"contentauth:urn:uuid:c85a2b90-f1a0-4aa4-b17f-f938b475804e\"") != std::string::npos); - - ASSERT_TRUE(result.find("\"validation_results\"") != std::string::npos); - - ASSERT_TRUE(result.find("\"manifest_data\"") != std::string::npos); - ASSERT_TRUE(result.find("\"application/c2pa\"") != std::string::npos); -} - TEST_F(BuilderTest, AddIngredientAsResourceToBuilder) { fs::path current_dir = fs::path(__FILE__).parent_path(); @@ -2080,33 +1981,14 @@ TEST_F(BuilderTest, AddIngredientAsResourceToBuilder) // Construct the path to the test fixture fs::path ingredient_source_path = current_dir / "../tests/fixtures/A.jpg"; - std::string ingredient_source_path_str = ingredient_source_path.string(); - - fs::path temp_dir = get_temp_dir("ingredient_as_resource"); - - // Get the needed JSON for the ingredient from the ingredient file using `read_ingredient_file` - std::string result; - // The data_dir is the location where binary resources will be stored - // eg. thumbnails - result = c2pa::read_ingredient_file(ingredient_source_path, temp_dir); - - // Parse ingredient JSON and extract the identifier - json ingredient_json = json::parse(result); - std::string identifier = ingredient_json["thumbnail"]["identifier"]; // Create the builder using a manifest JSON auto manifest = c2pa_test::read_text_file(manifest_path); + auto builder = c2pa::Builder(manifest); - // Parse the manifest and add ingredients array - json manifest_json = json::parse(manifest); - manifest_json["ingredients"] = json::array({ingredient_json}); - - auto builder = c2pa::Builder(manifest_json.dump()); - - // Add a resource: a thumbnail for the ingredient - // for the thumbnail path, we use what was put in the data_dir by the read_ingredient_file call. - fs::path ingredient_thumbnail_path = temp_dir / identifier; - builder.add_resource(identifier, ingredient_thumbnail_path); + // Add the ingredient from its source file. The Builder reads the file, + // generates the thumbnail resource, and adds the ingredient to the manifest. + builder.add_ingredient("{}", ingredient_source_path); // Create a signer fs::path certs_path = current_dir / "../tests/fixtures/es256_certs.pem"; @@ -2371,39 +2253,14 @@ TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePath) // Construct the path to the test fixture fs::path ingredient_source_path = current_dir / "../tests/fixtures/A.jpg"; - std::string ingredient_source_path_str = ingredient_source_path.string(); - - // Use temp dir for ingredient data (data dir) - fs::path temp_dir = get_temp_dir("base_ingredient_as_resource"); - - // Get the needed JSON for the ingredient - std::string result; - // The data_dir is the location where binary resources will be stored - // eg. thumbnails - result = c2pa::read_ingredient_file(ingredient_source_path, temp_dir); // Create the builder using a manifest JSON auto manifest = c2pa_test::read_text_file(manifest_path); + auto builder = c2pa::Builder(manifest); - // Add ingredients array to the manifest JSON, since our demo manifest doesn't have it, - // and we are adding ingredients more manually than through the Builder.add_ingredient call. - - // Parse the JSON and add ingredients array - std::string modified_manifest = manifest; - // Find the last closing brace and insert ingredients array before it - size_t last_brace = modified_manifest.find_last_of('}'); - if (last_brace != std::string::npos) { - std::string ingredients_array = ",\n \"ingredients\": [\n " + result + "\n ]"; - modified_manifest.insert(last_brace, ingredients_array); - } - - auto builder = c2pa::Builder(modified_manifest); - - // a Builder can load resources from a base path - // eg. ingredients from a data directory. - // Here, we can reuse the data directory from the read_ingredient_file call, - // so the builder properly loads the ingredient data using that directory. - builder.set_base_path(temp_dir.string()); + // Add the ingredient from its source file. The Builder reads the file and + // embeds the ingredient resources directly (no separate data dir / base path). + builder.add_ingredient("{}", ingredient_source_path); // Create a signer fs::path certs_path = current_dir / "../tests/fixtures/es256_certs.pem"; @@ -2446,13 +2303,8 @@ TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePathPlacedActionThreadLocalSe // set settings to not auto-add a placed action (thread-local) c2pa::load_settings("{\"builder\": { \"actions\": {\"auto_placed_action\": {\"enabled\": false}}}}", "json"); - // Get the needed JSON for the ingredient - std::string result; - result = c2pa::read_ingredient_file(ingredient_source_path, temp_dir); - - // Parse ingredient JSON and extract the instance_id - json ingredient_json = json::parse(result); - std::string instance_id = ingredient_json["instance_id"]; + // The instance_id we assign to the ingredient and reference from the placed action. + std::string instance_id = "test:iid:939a4c48-0dff-44ec-8f95-61f52b11618f"; // Create the manifest JSON structure with the placed action json manifest_json = { @@ -2490,17 +2342,19 @@ TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePathPlacedActionThreadLocalSe }} }} } - })}, - {"ingredients", json::array({ingredient_json})} + })} }; // Now we can create a Builder with the manifest auto builder = c2pa::Builder(manifest_json.dump()); - // A Builder can load resources from a base path eg. ingredients from a data directory. - // Here, we reuse the data directory from the read_ingredient_file call, - // so the builder properly loads the ingredient data using that directory. - builder.set_base_path(temp_dir.string()); + // Add the ingredient with the matching instance_id so the placed action links to it. + // The Builder reads the source file and embeds the ingredient resources directly. + json ingredient_obj = { + {"instance_id", instance_id}, + {"relationship", "componentOf"} + }; + builder.add_ingredient(ingredient_obj.dump(), ingredient_source_path); // Create a signer fs::path certs_path = current_dir / "../tests/fixtures/es256_certs.pem"; @@ -2544,13 +2398,8 @@ TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePathWithManifestContainingPla // Create context with auto_placed_action disabled via JSON settings auto context = c2pa::Context("{\"builder\": { \"actions\": {\"auto_placed_action\": {\"enabled\": false}}}}"); - // Get the needed JSON for the ingredient - std::string result; - result = c2pa::read_ingredient_file(ingredient_source_path, temp_dir); - - // Parse ingredient JSON and extract the instance_id - json ingredient_json = json::parse(result); - std::string instance_id = ingredient_json["instance_id"]; + // The instance_id we assign to the ingredient and reference from the placed action. + std::string instance_id = "test:iid:939a4c48-0dff-44ec-8f95-61f52b11618f"; // Create the manifest JSON structure with the placed action json manifest_json = { @@ -2588,15 +2437,19 @@ TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePathWithManifestContainingPla }} }} } - })}, - {"ingredients", json::array({ingredient_json})} + })} }; // Create a Builder with the context and manifest auto builder = c2pa::Builder(context, manifest_json.dump()); - // Set base path so builder can load ingredient resources - builder.set_base_path(temp_dir.string()); + // Add the ingredient with the matching instance_id so the placed action links to it. + // The Builder reads the source file and embeds the ingredient resources directly. + json ingredient_obj = { + {"instance_id", instance_id}, + {"relationship", "componentOf"} + }; + builder.add_ingredient(ingredient_obj.dump(), ingredient_source_path); // Create a signer fs::path certs_path = current_dir / "../tests/fixtures/es256_certs.pem"; @@ -2627,39 +2480,14 @@ TEST_F(BuilderTest, AddIngredientWithProvenanceDataToBuilderUsingBasePath) // Construct the path to the test fixture fs::path ingredient_source_path = current_dir / "../tests/fixtures/C.jpg"; - std::string ingredient_source_path_str = ingredient_source_path.string(); - - // Use temp data dir - fs::path temp_dir = get_temp_dir("ingredient_with_provenance_as_resource"); - - // Get the needed JSON for the ingredient - std::string result; - // The data_dir is the location where binary resources will be stored - // eg. thumbnails, but also additional c2pa data - result = c2pa::read_ingredient_file(ingredient_source_path, temp_dir); // Create the builder using a manifest JSON auto manifest = c2pa_test::read_text_file(manifest_path); + auto builder = c2pa::Builder(manifest); - // Add ingredients array to the manifest JSON, since our demo manifest doesn't have it, - // and we are adding ingredients more manually than through the Builder.add_ingredient call. - - // Parse the JSON and add ingredients array - std::string modified_manifest = manifest; - // Find the last closing brace and insert ingredients array before it - size_t last_brace = modified_manifest.find_last_of('}'); - if (last_brace != std::string::npos) { - std::string ingredients_array = ",\n \"ingredients\": [\n " + result + "\n ]"; - modified_manifest.insert(last_brace, ingredients_array); - } - - auto builder = c2pa::Builder(modified_manifest); - - // a Builder can load resources from a base path - // eg. ingredients from a data directory. - // Here, we reuse the data directory from the read_ingredient_file call, - // so the builder properly loads the ingredient data using that directory. - builder.set_base_path(temp_dir.string()); + // Add the ingredient from its source file. C.jpg has a manifest store, so the + // Builder reads its provenance/manifest data and embeds the ingredient resources. + builder.add_ingredient("{}", ingredient_source_path); // Create a signer fs::path certs_path = current_dir / "../tests/fixtures/es256_certs.pem"; diff --git a/tests/c-app-test/test.c b/tests/c-app-test/test.c index 4e31f805..e9edbd5f 100644 --- a/tests/c-app-test/test.c +++ b/tests/c-app-test/test.c @@ -23,15 +23,6 @@ int main(void) char *version = c2pa_version(); assert_contains("version", version, "c2pa-c-ffi/0."); - char *result1 = c2pa_read_file("tests/fixtures/C.jpg", NULL); - assert_str_not_null("c2pa_read_file_no_data_dir", result1); - - char *result = c2pa_read_file("tests/fixtures/C.jpg", "build/tmp"); - assert_str_not_null("c2pa_read_file", result); - - result = c2pa_read_ingredient_file("tests/fixtures/C.jpg", "build/ingredient"); - assert_str_not_null("c2pa_ingredient_from_file", result); - C2paStream *input_stream = open_file_stream("tests/fixtures/C.jpg", "rb"); assert_not_null("open_file_stream", input_stream); @@ -71,35 +62,15 @@ int main(void) char *manifest = load_file("tests/fixtures/training.json"); - // create a sign_info struct (using positional initialization to avoid designated initializers) - - C2paSignerInfo sign_info = {"es256", certs, private_key, "http://timestamp.digicert.com"}; - - // Remove the file if it exists - remove("build/tmp/earth.jpg"); - result = c2pa_sign_file("tests/fixtures/C.jpg", "build/tmp/earth.jpg", manifest, &sign_info, "tests/fixtures"); - // c2pa_sign_file returns JSON manifest from the Reader on success, NULL on error - assert_not_null("c2pa_sign_file_ok", result); - if (result != NULL) - c2pa_free(result); - - remove("build/tmp/earth2.jpg"); - result = c2pa_sign_file("tests/fixtures/foo.jpg", "build/tmp/earth2.jpg", manifest, &sign_info, "tests/fixtures"); - assert_null("c2pa_sign_file_not_found", result, "FileNotFound"); - - remove("build/tmp/earth1.pem"); - result = c2pa_sign_file("tests/fixtures/es256_certs.pem", "build/tmp/earth1.pem", manifest, &sign_info, "tests/fixtures"); - assert_null("c2pa_sign_file_not_supported", result, "NotSupported"); - C2paBuilder *builder = c2pa_builder_from_json(manifest); assert_not_null("c2pa_builder_from_json", builder); - C2paStream *archive = open_file_stream("build/tmp/archive.zip", "wb"); + C2paStream *archive = open_file_stream("build/archive.zip", "wb"); int arch_result = c2pa_builder_to_archive(builder, archive); assert_int("c2pa_builder_to_archive", arch_result); close_file_stream(archive); - C2paStream *archive2 = open_file_stream("build/tmp/archive.zip", "rb"); + C2paStream *archive2 = open_file_stream("build/archive.zip", "rb"); C2paBuilder *builder2 = c2pa_builder_from_archive(archive2); assert_not_null("c2pa_builder_from_archive", builder2); close_file_stream(archive2); @@ -108,9 +79,9 @@ int main(void) assert_not_null("c2pa_signer_create", signer); C2paStream *source = open_file_stream("tests/fixtures/C.jpg", "rb"); - remove("build/tmp/earth4.jpg"); + remove("build/earth4.jpg"); // stream needs to be w+b because we'll write, rewind, read - C2paStream *dest = open_file_stream("build/tmp/earth4.jpg", "w+b"); + C2paStream *dest = open_file_stream("build/earth4.jpg", "w+b"); const unsigned char *manifest_bytes = NULL; // todo: test passing NULL instead of a pointer int64_t result2 = c2pa_builder_sign(builder2, "image/jpeg", source, dest, signer, &manifest_bytes); diff --git a/tests/read_file.test.cpp b/tests/read_file.test.cpp deleted file mode 100644 index eea01783..00000000 --- a/tests/read_file.test.cpp +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2024 Adobe. All rights reserved. -// This file is licensed to you under the Apache License, -// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) -// or the MIT license (http://opensource.org/licenses/MIT), -// at your option. -// Unless required by applicable law or agreed to in writing, -// this software is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or -// implied. See the LICENSE-MIT and LICENSE-APACHE files for the -// specific language governing permissions and limitations under -// each license. - -#include -#include -#include -#include - -using nlohmann::json; -namespace fs = std::filesystem; - -TEST(ReadFile, ReadFileWithNoManifestReturnsEmptyOptional) { - fs::path current_dir = fs::path(__FILE__).parent_path(); - fs::path test_file = current_dir / "../tests/fixtures/A.jpg"; - auto result = c2pa::read_file(test_file); - ASSERT_FALSE(result.has_value()); -}; - -class ReadFileWithManifestTests - : public ::testing::TestWithParam { -public: - static void test_read_file_with_manifest(const std::string& filename) { - fs::path current_dir = fs::path(__FILE__).parent_path(); - fs::path test_file = current_dir / "../tests/fixtures" / filename; - auto result = c2pa::read_file(test_file); - ASSERT_TRUE(result.has_value()); - - // parse result with json - auto json = json::parse(result.value()); - EXPECT_TRUE(json.contains("manifests")); - EXPECT_TRUE(json.contains("active_manifest")); - } -}; - -INSTANTIATE_TEST_SUITE_P(ReadFileWithManifestTests, ReadFileWithManifestTests, - ::testing::Values( - // Files with manifests - "C.jpg", - "video1.mp4", - "C.dng", - "sample1_signed.wav")); - -TEST_P(ReadFileWithManifestTests, ReadFileWithManifestReturnsSomeValue) { - auto filename = GetParam(); - test_read_file_with_manifest(filename); -} - -TEST(ReadFile, ReadFileWithDataDirReturnsSomeValue) -{ - fs::path current_dir = fs::path(__FILE__).parent_path(); - fs::path test_file = current_dir / "../tests/fixtures/C.jpg"; - auto result = c2pa::read_file(test_file, current_dir / "../build/read_file"); - ASSERT_TRUE(result.has_value()); - - // parse result with json - auto json = json::parse(result.value()); - - EXPECT_TRUE(json.contains("manifests")); - EXPECT_TRUE(json.contains("active_manifest")); - - // build/read_file should exist and contain a manifest.json file - EXPECT_TRUE(fs::exists(current_dir / "../build/read_file")); - EXPECT_TRUE(fs::exists(current_dir / "../build/read_file/manifest_store.json")); -}; From 627c6838651381f9a3a7e6f992abc96678810a77 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:28:34 -0700 Subject: [PATCH 2/6] Delete compile_commands.json --- compile_commands.json | 1 - 1 file changed, 1 deletion(-) delete mode 120000 compile_commands.json diff --git a/compile_commands.json b/compile_commands.json deleted file mode 120000 index 7c1ac711..00000000 --- a/compile_commands.json +++ /dev/null @@ -1 +0,0 @@ -build/debug/compile_commands.json \ No newline at end of file From 1a65cec167fe4327ea53c9448c47d1bc203dbc93 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 4 Jun 2026 11:43:56 -0700 Subject: [PATCH 3/6] fix: Remove unused APIs --- README.md | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index bfc49a04..4424e565 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ If you want to view the documentation in GitHub, see: - Using [working stores and archvies](docs/working-stores.md) - Selectively constructing manifests by [filtering actions and ingredients](docs/selective-manifests.md) - Using the [embeddable API for low-level control over embedding manifests](docs/embeddable-api.md) -- [Frequently-asked questions (FAQs)](docs/faqs.md) +- [Frequently-asked questions (FAQs)](docs/faqs.md) - [Release notes](docs/release-notes.md) @@ -64,7 +64,7 @@ Building the library holding the C++ SDK requires [GNU make](https://www.gnu.org Enter this command to build the SDK: -``` +```sh make release ``` @@ -129,30 +129,50 @@ Build the [unit tests](https://github.com/contentauth/c2pa-cpp/tree/main/tests) make test ``` -#### Running the sanitizer build on macOS +The Rust `c2pa_c` library does not need to be sanitizer-instrumented for this to work. -`make test-san` builds the tests with AddressSanitizer/UBSan. On recent macOS versions the -AddressSanitizer runtime shipped with Xcode's AppleClang can abort at process start with: +### Troubleshooting -``` +#### Sanitizer test builds fail on macOS + +`make test-san` builds the tests with AddressSanitizer/UBSan. On some recent macOS versions the +AddressSanitizer runtime shipped with Xcode's AppleClang can abort at process start with a message like: + +```sh AddressSanitizer: CHECK failed: sanitizer_malloc_mac.inc:189 "((!asan_init_is_running)) != (0)" ``` -This is an incompatibility between the older AppleClang sanitizer runtime and the host -malloc; it is not specific to this project. Setting `MallocNanoZone=0` or preloading the ASan -runtime via `DYLD_INSERT_LIBRARIES` does **not** resolve it. The fix is to build the -sanitizer target with a newer LLVM/Clang (for example, Homebrew's `llvm`): +This is an incompatibility between an older AppleClang sanitizer runtime and the host +malloc. The fix is to build the sanitizer target with a newer LLVM/Clang (for example, Homebrew's `llvm`). + +To do so, install (or update) an `llvm` version. For instance, using homebrew:: ```sh brew install llvm +``` + +Then, build and run the tests with the sanitizers activated. First, configure the build: + +```sh cmake -S . -B build/debug -G Ninja -DCMAKE_BUILD_TYPE=Debug -DENABLE_SANITIZERS=ON \ -DCMAKE_C_COMPILER="$(brew --prefix llvm)/bin/clang" \ -DCMAKE_CXX_COMPILER="$(brew --prefix llvm)/bin/clang++" +``` + +Note that the llvm compiler from `brew --prefix llvm` must properly resolve for this command to work. + +Then run the build scripts: + +```sh cmake --build build/debug +``` + +And finally run the test executable: + +```sh ctest --test-dir build/debug --output-on-failure ``` -The Rust `c2pa_c` library does not need to be sanitizer-instrumented for this to work. ### Building API documentation @@ -166,14 +186,14 @@ To generate API docs locally, these are the main files: Install Doxygen if needed: -``` +```sh macOS: brew install doxygen Ubuntu/Debian: sudo apt-get install doxygen ``` To generate docs, enter the command: -``` +```sh ./scripts/generate_api_docs.sh ``` From 5ee4b278c397711577ffee8392cdc9d592b79f9f Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Tue, 9 Jun 2026 09:34:48 -0700 Subject: [PATCH 4/6] fix: Update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 39c3d0a3..0ce856c9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ cmake_install.cmake CMakeOutput.log CMakeError.log CMakeLists.txt.user +compile_commands.json # Doxygen docs /api-docs/_build From 90748aab14db285a2a0f0e9891d03e68ae110196 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Tue, 9 Jun 2026 10:33:55 -0700 Subject: [PATCH 5/6] fix: Small memory fix --- src/c2pa_builder.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/c2pa_builder.cpp b/src/c2pa_builder.cpp index 358f7fe4..58f86674 100644 --- a/src/c2pa_builder.cpp +++ b/src/c2pa_builder.cpp @@ -117,6 +117,7 @@ namespace c2pa } builder = c2pa_builder_with_archive(base, c_archive.c_stream); + base = nullptr; if (builder == nullptr) { throw C2paException(); } From f19168a8ce9422459e83271b935d7aa294247540 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:44:13 -0700 Subject: [PATCH 6/6] fix: Memory handling updates + undefined behavior for exceptions (#233) * fix: memchecks * fix: Code cleanup * fix: cmake build type defaults * fix: Undo cmakelsit changes --- include/c2pa.hpp | 9 ++-- src/c2pa_builder.cpp | 4 +- src/c2pa_context.cpp | 6 ++- src/c2pa_internal.hpp | 115 ++++++++++++++++++++++++++--------------- src/c2pa_reader.cpp | 35 ++++++++----- src/c2pa_settings.cpp | 1 + src/c2pa_signer.cpp | 8 +++ tests/builder.test.cpp | 48 +++++++++++++++++ tests/reader.test.cpp | 18 +++++++ 9 files changed, 182 insertions(+), 62 deletions(-) diff --git a/include/c2pa.hpp b/include/c2pa.hpp index 91fbfe5e..f78ed6f2 100644 --- a/include/c2pa.hpp +++ b/include/c2pa.hpp @@ -567,7 +567,8 @@ namespace c2pa explicit CppIStream(IStream &istream) { static_assert(std::is_base_of::value, "Stream must be derived from std::istream"); - c_stream = c2pa_create_stream(reinterpret_cast(&istream), reader, seeker, writer, flusher); + // Upcast to std::istream* before type erasure; the callbacks cast the context back to std::istream* + c_stream = c2pa_create_stream(reinterpret_cast(static_cast(&istream)), reader, seeker, writer, flusher); if (c_stream == nullptr) { throw C2paException("Failed to create input stream wrapper: is stream open and valid?"); } @@ -628,7 +629,8 @@ namespace c2pa template explicit CppOStream(OStream &ostream) { static_assert(std::is_base_of::value, "Stream must be derived from std::ostream"); - c_stream = c2pa_create_stream(reinterpret_cast(&ostream), reader, seeker, writer, flusher); + // Upcast to std::ostream* before type erasure; the callbacks cast the context back to std::ostream* + c_stream = c2pa_create_stream(reinterpret_cast(static_cast(&ostream)), reader, seeker, writer, flusher); if (c_stream == nullptr) { throw C2paException("Failed to create output stream wrapper: is stream open and valid?"); } @@ -687,7 +689,8 @@ namespace c2pa template CppIOStream(IOStream &iostream) { static_assert(std::is_base_of::value, "Stream must be derived from std::iostream"); - c_stream = c2pa_create_stream(reinterpret_cast(&iostream), reader, seeker, writer, flusher); + // Upcast to std::iostream* before type erasure; the callbacks cast the context back to std::iostream* + c_stream = c2pa_create_stream(reinterpret_cast(static_cast(&iostream)), reader, seeker, writer, flusher); if (c_stream == nullptr) { throw C2paException("Failed to create I/O stream wrapper: is stream open and valid?"); } diff --git a/src/c2pa_builder.cpp b/src/c2pa_builder.cpp index 58f86674..e68dc9c0 100644 --- a/src/c2pa_builder.cpp +++ b/src/c2pa_builder.cpp @@ -51,8 +51,8 @@ namespace c2pa init_from_context(context); // Apply the manifest definition to the Builder. - // Note: c2pa_builder_with_definition always consumes the builder pointer, - // so the original pointer is invalid after this call regardless of success/error. + // Note: c2pa_builder_with_definition consumes the builder pointer on success + // and on operation failure. C2paBuilder* updated = c2pa_builder_with_definition(builder, manifest_json.c_str()); builder = nullptr; if (updated == nullptr) { diff --git a/src/c2pa_context.cpp b/src/c2pa_context.cpp index bcecb7c0..53374953 100644 --- a/src/c2pa_context.cpp +++ b/src/c2pa_context.cpp @@ -183,7 +183,11 @@ namespace c2pa if (!raw) { throw C2paException("Signer is not valid"); } - c2pa_context_builder_set_signer(context_builder, raw); + // On error the signer may not have been consumed by the C API, + // surface an error + if (c2pa_context_builder_set_signer(context_builder, raw) != 0) { + throw C2paException(); + } return *this; } diff --git a/src/c2pa_internal.hpp b/src/c2pa_internal.hpp index e1fe8ab2..1cf16e06 100644 --- a/src/c2pa_internal.hpp +++ b/src/c2pa_internal.hpp @@ -46,9 +46,16 @@ inline std::vector c_mime_types_to_vector(const char* const* mime_t std::vector result; if (mime_types == nullptr) { return result; } - result.reserve(count); - for(uintptr_t i = 0; i < count; i++) { - result.emplace_back(mime_types[i]); + try { + result.reserve(count); + for(uintptr_t i = 0; i < count; i++) { + if (mime_types[i] != nullptr) { + result.emplace_back(mime_types[i]); + } + } + } catch (...) { + c2pa_free_string_array(mime_types, count); + throw; } c2pa_free_string_array(mime_types, count); @@ -107,29 +114,37 @@ struct StreamSeekTraits { }; /// Seeker impl. +/// Exceptions must not unwind into Rust/C, so any throw +/// is converted to an IoError return. template intptr_t stream_seeker(StreamContext* context, intptr_t offset, C2paSeekMode whence) { - auto* stream = reinterpret_cast(context); - if (!is_stream_usable(stream)) { - return stream_error_return(StreamError::IoError); - } - const std::ios_base::seekdir dir = whence_to_seekdir(whence); - stream->clear(); - StreamSeekTraits::seek(stream, offset, dir); - if (stream->fail()) { - return stream_error_return(StreamError::InvalidArgument); - } - if (stream->bad()) { - return stream_error_return(StreamError::IoError); - } - const int64_t pos = StreamSeekTraits::tell(stream); - if (pos < 0) { + try { + auto* stream = reinterpret_cast(context); + if (!is_stream_usable(stream)) { + return stream_error_return(StreamError::IoError); + } + const std::ios_base::seekdir dir = whence_to_seekdir(whence); + stream->clear(); + StreamSeekTraits::seek(stream, offset, dir); + if (stream->fail()) { + return stream_error_return(StreamError::InvalidArgument); + } + if (stream->bad()) { + return stream_error_return(StreamError::IoError); + } + const int64_t pos = StreamSeekTraits::tell(stream); + if (pos < 0) { + return stream_error_return(StreamError::IoError); + } + return static_cast(pos); + } catch (...) { return stream_error_return(StreamError::IoError); } - return static_cast(pos); } /// Reader impl. +/// Exceptions must not unwind into Rust/C, so any throw +/// is converted to an IoError return. template intptr_t stream_reader(StreamContext* context, uint8_t* buffer, intptr_t size) { if (!context || !buffer) { @@ -141,37 +156,47 @@ intptr_t stream_reader(StreamContext* context, uint8_t* buffer, intptr_t size) { if (size == 0) { return 0; } - auto* stream = reinterpret_cast(context); - if (!is_stream_usable(stream)) { - return stream_error_return(StreamError::IoError); - } - stream->read(reinterpret_cast(buffer), size); - if (stream->fail()) { - if (!stream->eof()) { - return stream_error_return(StreamError::InvalidArgument); + try { + auto* stream = reinterpret_cast(context); + if (!is_stream_usable(stream)) { + return stream_error_return(StreamError::IoError); } - } - if (stream->bad()) { + stream->read(reinterpret_cast(buffer), size); + if (stream->fail()) { + if (!stream->eof()) { + return stream_error_return(StreamError::InvalidArgument); + } + } + if (stream->bad()) { + return stream_error_return(StreamError::IoError); + } + return static_cast(stream->gcount()); + } catch (...) { return stream_error_return(StreamError::IoError); } - return static_cast(stream->gcount()); } /// Get stream from context, used by writer and flusher. +/// Exceptions must not unwind into Rust/C, so any throw +/// is converted to an IoError return. template intptr_t stream_op(StreamContext* context, Op op) { - auto* stream = reinterpret_cast(context); - if (!is_stream_usable(stream)) { - return stream_error_return(StreamError::IoError); - } - const intptr_t result = op(stream); - if (stream->fail()) { - return stream_error_return(StreamError::InvalidArgument); - } - if (stream->bad()) { + try { + auto* stream = reinterpret_cast(context); + if (!is_stream_usable(stream)) { + return stream_error_return(StreamError::IoError); + } + const intptr_t result = op(stream); + if (stream->fail()) { + return stream_error_return(StreamError::InvalidArgument); + } + if (stream->bad()) { + return stream_error_return(StreamError::IoError); + } + return result; + } catch (...) { return stream_error_return(StreamError::IoError); } - return result; } /// Writer impl. @@ -236,12 +261,18 @@ inline std::string c_string_to_string(T* c_result) { /// @return Vector containing the bytes (throws if null or negative size) /// @details This helper extracts the pattern of checking C API results, /// copying to a vector, and freeing the C-allocated memory. -/// The C API contract is: if result < 0 or data == nullptr, the operation failed. +/// The C API contract is: if result < 0, the operation failed. A null +/// data pointer with size == 0 is a valid empty result (the C API +/// returns null for empty byte arrays). inline std::vector to_byte_vector(const unsigned char* data, int64_t size) { - if (size < 0 || data == nullptr) { + if (size < 0 || (data == nullptr && size > 0)) { c2pa_free(data); // May be null or allocated, c2pa_free handles both throw C2paException(); } + if (size == 0) { + c2pa_free(data); + return {}; + } auto result = std::vector(data, data + size); c2pa_free(data); diff --git a/src/c2pa_reader.cpp b/src/c2pa_reader.cpp index 909e61e7..a2301a00 100644 --- a/src/c2pa_reader.cpp +++ b/src/c2pa_reader.cpp @@ -46,15 +46,16 @@ namespace c2pa throw C2paException("Invalid Context provider IContextProvider"); } + // Create the stream wrapper before the reader handle + cpp_stream = std::make_unique(stream); + c2pa_reader = c2pa_reader_from_context(context.c_context()); if (c2pa_reader == nullptr) { throw C2paException("Failed to create reader from context"); } - cpp_stream = std::make_unique(stream); // Update reader with stream. - // Note: c2pa_reader_with_stream always consumes the reader pointer, - // so the original pointer is invalid after this call regardless of success/error. + // Note: c2pa_reader_with_stream consumes the reader pointer. C2paReader* updated = c2pa_reader_with_stream(c2pa_reader, format.c_str(), cpp_stream->c_stream); c2pa_reader = nullptr; if (updated == nullptr) { @@ -71,15 +72,10 @@ namespace c2pa throw C2paException("Invalid Context provider IContextProvider"); } - c2pa_reader = c2pa_reader_from_context(context.c_context()); - if (c2pa_reader == nullptr) { - throw C2paException("Failed to create reader from context"); - } - - // Create owned stream that will live as long as the Reader + // Create the streams before the reader handle. + // Create owned stream that will live as long as the Reader. owned_stream = std::make_unique(source_path, std::ios::binary); if (!owned_stream->is_open()) { - c2pa_free(c2pa_reader); throw std::system_error(errno, std::system_category(), "Failed to open file: " + source_path.string()); } @@ -87,7 +83,13 @@ namespace c2pa // CppIStream stores reference to owned_stream, which lives as long as Reader cpp_stream = std::make_unique(*owned_stream); - // Note: c2pa_reader_with_stream always consumes the reader pointer. + + c2pa_reader = c2pa_reader_from_context(context.c_context()); + if (c2pa_reader == nullptr) { + throw C2paException("Failed to create reader from context"); + } + + // Note: c2pa_reader_with_stream consumes the reader pointer. C2paReader* updated = c2pa_reader_with_stream(c2pa_reader, extension.c_str(), cpp_stream->c_stream); c2pa_reader = nullptr; if (updated == nullptr) { @@ -175,15 +177,20 @@ namespace c2pa [[nodiscard]] std::optional Reader::remote_url() const { auto url = c2pa_reader_remote_url(c2pa_reader); if (url == nullptr) { return std::nullopt; } - std::string url_str(url); // The C2PA library returns a `const char*` that needs to be released. // The underlying `char*` is mutable; however, to indicate the value // shouldn't be modified, it's returned as a const char*. // // TODO: Revisit after determining how we want c2pa-rs to handle // strings that shouldn't be modified by our bindings. - c2pa_free(url); - return url_str; + try { + std::string url_str(url); + c2pa_free(url); + return url_str; + } catch (...) { + c2pa_free(url); + throw; + } } int64_t Reader::get_resource(const std::string &uri, const std::filesystem::path &path) diff --git a/src/c2pa_settings.cpp b/src/c2pa_settings.cpp index be989ec1..abdec65e 100644 --- a/src/c2pa_settings.cpp +++ b/src/c2pa_settings.cpp @@ -34,6 +34,7 @@ namespace c2pa } if (c2pa_settings_update_from_string(settings_ptr, data.c_str(), format.c_str()) != 0) { c2pa_free(settings_ptr); + settings_ptr = nullptr; throw C2paException(); } } diff --git a/src/c2pa_signer.cpp b/src/c2pa_signer.cpp index 8172f537..9e6954e2 100644 --- a/src/c2pa_signer.cpp +++ b/src/c2pa_signer.cpp @@ -71,12 +71,20 @@ namespace c2pa { // Pass the C++ callback as a context to our static callback wrapper. signer = c2pa_signer_create((const void *)callback, &signer_passthrough, alg, sign_cert.c_str(), validate_tsa_uri(tsa_uri)); + if (signer == nullptr) + { + throw C2paException(); + } } Signer::Signer(const std::string &alg, const std::string &sign_cert, const std::string &private_key, const std::optional &tsa_uri) { auto info = C2paSignerInfo { alg.c_str(), sign_cert.c_str(), private_key.c_str(), validate_tsa_uri(tsa_uri) }; signer = c2pa_signer_from_info(&info); + if (signer == nullptr) + { + throw C2paException(); + } } Signer::~Signer() diff --git a/tests/builder.test.cpp b/tests/builder.test.cpp index 95ef03cf..d61d9db0 100644 --- a/tests/builder.test.cpp +++ b/tests/builder.test.cpp @@ -6005,3 +6005,51 @@ TEST_F(BuilderTest, ArchiveIngredientWithProvenanceRoundTripAndReuse) EXPECT_EQ(out_ingredients[0]["title"], "C.jpg"); EXPECT_EQ(out_ingredients[0]["relationship"], "componentOf"); } + +TEST(SignerTest, InvalidCredentialsThrowFromConstructor) { + EXPECT_THROW( + c2pa::Signer("Es256", "not a certificate", "not a private key"), + c2pa::C2paException); +} + +TEST_F(BuilderTest, SignSourceStreamWithExceptions) { + auto image_path = c2pa_test::get_fixture_path("A.jpg"); + auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + + auto context = std::make_shared(); + auto builder = c2pa::Builder(context, manifest); + auto signer = c2pa_test::create_test_signer(); + + std::ifstream source(image_path, std::ios::binary); + ASSERT_TRUE(source.is_open()); + source.exceptions(std::ios::failbit | std::ios::badbit); + + std::stringstream memory_buffer(std::ios::in | std::ios::out | std::ios::binary); + std::iostream& dest = memory_buffer; + + try { + auto manifest_data = builder.sign("image/jpeg", source, dest, signer); + EXPECT_FALSE(manifest_data.empty()); + } catch (const c2pa::C2paException&) { + // An error result is acceptable; crossing the FFI with an exception is not. + } +} + +TEST_F(BuilderTest, ArchiveToFstreamBackedCppOStream) { + auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + + auto context = std::make_shared(); + auto builder = c2pa::Builder(context, manifest); + + auto archive_path = get_temp_path("archive_fstream_cppostream.bin"); + std::fstream dest(archive_path, + std::ios_base::binary | std::ios_base::trunc | + std::ios_base::in | std::ios_base::out); + ASSERT_TRUE(dest.is_open()); + + c2pa::CppOStream c_dest(dest); + ASSERT_EQ(c2pa_builder_to_archive(builder.c2pa_builder(), c_dest.c_stream), 0); + dest.flush(); + + EXPECT_GT(std::filesystem::file_size(archive_path), 0u); +} diff --git a/tests/reader.test.cpp b/tests/reader.test.cpp index 68827ee4..9ab8a4ee 100644 --- a/tests/reader.test.cpp +++ b/tests/reader.test.cpp @@ -718,3 +718,21 @@ TEST_F(ReaderTest, ReadCrJsonSpecialChars) auto crjson = reader.crjson(); EXPECT_FALSE(crjson.empty()); } + +TEST(Reader, StreamWithExceptions) { + fs::path current_dir = fs::path(__FILE__).parent_path(); + fs::path test_file = current_dir / "../tests/fixtures/C.jpg"; + + std::ifstream stream(test_file, std::ios::binary); + ASSERT_TRUE(stream.is_open()); + stream.exceptions(std::ios::failbit | std::ios::badbit); + + auto context = std::make_shared(); + try { + auto reader = c2pa::Reader(context, "image/jpeg", stream); + auto json_result = reader.json(); + EXPECT_FALSE(json_result.empty()); + } catch (const c2pa::C2paException&) { + // An error result is acceptable; crossing the FFI with an exception is not. + } +}