diff --git a/docs/context-settings.md b/docs/context-settings.md index 249e3cb..bf6e767 100644 --- a/docs/context-settings.md +++ b/docs/context-settings.md @@ -10,13 +10,13 @@ See also: ## Quick start -The simplest way to configure the SDK is to create a `Context` with inline JSON and pass it to `Reader` or `Builder`: +The simplest way to configure the SDK is to create a `Context` wrapped in a `shared_ptr` and pass it to `Reader` or `Builder`: ```cpp #include "c2pa.hpp" // Create a Context with settings -c2pa::Context context(R"({ +auto context = std::make_shared(R"({ "version": 1, "builder": { "claim_generator_info": {"name": "My App", "version": "1.0"}, @@ -35,7 +35,7 @@ c2pa::Builder builder(context, manifest_json); For default SDK configuration, just create an empty `Context`: ```cpp -c2pa::Context context; // Uses SDK defaults +auto context = std::make_shared(); // Uses SDK defaults c2pa::Reader reader(context, "image.jpg"); ``` @@ -66,18 +66,18 @@ c2pa::Reader reader(context, "image.jpg"); ### Context lifecycle - **Non-copyable, moveable**: `Context` can be moved but not copied. After moving, `is_valid()` returns `false` on the source -- **Used at construction**: `Reader` and `Builder` copy configuration from the context at construction time. The `Context` doesn't need to outlive them -- **Reusable**: Use the same `Context` to create multiple readers and builders +- **Pass as `shared_ptr`**: `Reader` and `Builder` retain a shared reference to the context, keeping it alive for their lifetime. This is required when using progress callbacks — without it, the callback can fire after the context is destroyed, causing a crash +- **Reusable**: Use the same `shared_ptr` to create multiple readers and builders ```cpp -c2pa::Context context(settings); +auto context = std::make_shared(settings); // All three use the same configuration c2pa::Builder builder1(context, manifest1); c2pa::Builder builder2(context, manifest2); c2pa::Reader reader(context, "image.jpg"); -// Context can go out of scope, readers/builders still work +// context shared_ptr can go out of scope — reader/builder each hold a reference ``` ### Settings format @@ -179,15 +179,15 @@ Use `ContextBuilder::with_progress_callback` to attach a callback before buildin std::atomic phase_count{0}; -auto context = c2pa::Context::ContextBuilder() +auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_progress_callback([&](c2pa::ProgressPhase phase, uint32_t step, uint32_t total) { ++phase_count; // Return true to continue, false to cancel. return true; }) - .create_context(); + .create_context()); -// Use the context normally — the callback fires automatically. +// Pass as shared_ptr so the context stays alive while the callback can fire. c2pa::Builder builder(context, manifest_json); builder.sign("source.jpg", "output.jpg", signer); ``` @@ -212,16 +212,16 @@ You may call `Context::cancel()` from another thread while the same `Context` re ```cpp #include -auto context = c2pa::Context::ContextBuilder() +auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_progress_callback([](c2pa::ProgressPhase, uint32_t, uint32_t) { return true; // Don't cancel from the callback — use cancel() instead. }) - .create_context(); + .create_context()); // Kick off a cancel after 500 ms from a background thread. -std::thread cancel_thread([&context]() { +std::thread cancel_thread([context]() { std::this_thread::sleep_for(std::chrono::milliseconds(500)); - context.cancel(); + context->cancel(); }); try { @@ -278,14 +278,14 @@ Reading → VerifyingManifest → VerifyingSignature → VerifyingAssetHash → `with_progress_callback` chains with other `ContextBuilder` methods: ```cpp -auto context = c2pa::Context::ContextBuilder() +auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_settings(settings) .with_signer(std::move(signer)) .with_progress_callback([](c2pa::ProgressPhase phase, uint32_t step, uint32_t total) { // Update a UI progress bar, log phases, etc. return true; }) - .create_context(); + .create_context()); ``` ## Common configuration patterns @@ -370,11 +370,11 @@ c2pa::Builder prod_builder(prod_context, manifest); ### Temporary contexts -Since configuration is copied at construction, you can use temporary contexts: +You can wrap a temporary `Context` in a `shared_ptr` inline: ```cpp c2pa::Reader reader( - c2pa::Context(R"({"verify": {"remote_manifest_fetch": false}})"), + std::make_shared(R"({"verify": {"remote_manifest_fetch": false}})"), "image.jpg" ); ``` @@ -384,12 +384,12 @@ c2pa::Reader reader( `Reader` uses `Context` to control validation, trust configuration, network access, and performance. > [!IMPORTANT] -> `Context` is used only at construction. `Reader` copies the configuration it needs internally, so the `Context` doesn't need to outlive the `Reader`. +> Pass `Context` as a `shared_ptr`. `Reader` retains a shared reference, keeping the context alive for its lifetime. This is required when using progress callbacks. ### Reading from a file ```cpp -c2pa::Context context(R"({ +auto context = std::make_shared(R"({ "version": 1, "verify": { "remote_manifest_fetch": false, @@ -414,10 +414,10 @@ std::cout << reader.json() << std::endl; `Builder` uses `Context` to control manifest creation, signing, thumbnails, and more. > [!IMPORTANT] -> The `Context` is used only when constructing the `Builder`. It copies the configuration internally, so the `Context` doesn't need to outlive the `Builder`. +> Pass `Context` as a `shared_ptr`. `Builder` retains a shared reference, keeping the context alive for its lifetime. This is required when using progress callbacks. ```cpp -c2pa::Context context(R"({ +auto context = std::make_shared(R"({ "version": 1, "builder": { "claim_generator_info": {"name": "My App", "version": "1.0"}, @@ -515,7 +515,7 @@ MIICEzCCAcWgAwIBAgIUW4fUnS38162x10PCnB8qFsrQuZgwBQYDK2VwMHcxCzAJ ... -----END CERTIFICATE-----)"; -c2pa::Context context(R"({ +auto context = std::make_shared(R"({ "version": 1, "trust": { "user_anchors": ")" + test_root_ca + R"(" @@ -540,7 +540,7 @@ settings.update(R"({ } })"); -c2pa::Context context(settings); +auto context = std::make_shared(settings); c2pa::Reader reader(context, "signed_asset.jpg"); ``` @@ -567,9 +567,9 @@ For the PEM string (for example in `user_anchors` in above example): Load in your application: ```cpp -auto context = c2pa::Context::ContextBuilder() +auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_json_settings_file("dev_trust_config.json") - .create_context(); + .create_context()); c2pa::Reader reader(context, "signed_asset.jpg"); ``` @@ -624,7 +624,7 @@ The following table lists the key properties (all default to `true`): Disable network-dependent features: ```cpp -c2pa::Context context(R"({ +auto context = std::make_shared(R"({ "version": 1, "verify": { "remote_manifest_fetch": false, @@ -659,7 +659,7 @@ c2pa::Context dev_context(dev_settings); Enable all validation features for certification or compliance testing: ```cpp -c2pa::Context context(R"({ +auto context = std::make_shared(R"({ "version": 1, "verify": { "strict_v1_validation": true, @@ -733,7 +733,7 @@ See [ClaimGeneratorInfoSettings in the SDK object reference](https://opensource. **Example:** ```cpp -c2pa::Context context(R"({ +auto context = std::make_shared(R"({ "version": 1, "builder": { "claim_generator_info": { @@ -809,7 +809,7 @@ std::string config = R"({ } })"; -c2pa::Context context(config); +auto context = std::make_shared(config); c2pa::Builder builder(context, manifest_json); builder.sign(source_path, dest_path); // Uses signer from context ``` @@ -886,9 +886,11 @@ The SDK introduced Context-based APIs to replace constructors and functions that | Deprecated API | Replacement | |---|---| | `load_settings(data, format)` | [`Context` constructors or `ContextBuilder`](#replacing-load_settings) | -| `Reader(format, stream)` | [`Reader(context, format, stream)`](#adding-a-context-parameter-to-reader-and-builder) | -| `Reader(source_path)` | [`Reader(context, source_path)`](#adding-a-context-parameter-to-reader-and-builder) | -| `Builder(manifest_json)` | [`Builder(context, manifest_json)`](#adding-a-context-parameter-to-reader-and-builder) | +| `Reader(format, stream)` | [`Reader(shared_ptr, format, stream)`](#adding-a-context-parameter-to-reader-and-builder) | +| `Reader(source_path)` | [`Reader(shared_ptr, source_path)`](#adding-a-context-parameter-to-reader-and-builder) | +| `Builder(manifest_json)` | [`Builder(shared_ptr, manifest_json)`](#adding-a-context-parameter-to-reader-and-builder) | +| `Reader(IContextProvider&, ...)` | [`Reader(shared_ptr, ...)`](#using-shared_ptr-instead-of-reference-for-reader-and-builder) | +| `Builder(IContextProvider&, ...)` | [`Builder(shared_ptr, ...)`](#using-shared_ptr-instead-of-reference-for-reader-and-builder) | | `Builder::sign(..., ostream, ...)` | [`Builder::sign(..., iostream, ...)`](#using-iostream-instead-of-ostream-in-buildersign) | ### Replacing load_settings @@ -927,7 +929,7 @@ The following constructors are deprecated because they rely on thread-local sett - `Reader(const std::filesystem::path& source_path)` - `Builder(const std::string& manifest_json)` -The migration path for each is to create a `Context` and pass it as the first argument. +The migration path is to create a `shared_ptr` and pass it as the first argument. **Deprecated:** @@ -937,24 +939,44 @@ c2pa::Reader reader("image.jpg"); c2pa::Builder builder(manifest_json); ``` -**With context API:** +**With shared_ptr context API:** ```cpp -c2pa::Context context; // or Context(settings) or Context(json_string) +auto context = std::make_shared(); // or Context(settings) or Context(json) c2pa::Reader reader(context, "image/jpeg", stream); c2pa::Reader reader(context, "image.jpg"); c2pa::Builder builder(context, manifest_json); ``` -If you need default SDK behavior and have no custom settings, `c2pa::Context context;` with no arguments is sufficient. +### Using shared_ptr instead of reference for Reader and Builder -#### About IContextProvider +The `IContextProvider&` reference overloads are deprecated because they do not extend the lifetime of the context. If the context is destroyed while a `Reader` or `Builder` has a progress callback registered, the callback fires against freed memory, causing a crash. -The deprecation warnings reference `IContextProvider` in their suggested fix (e.g., "Use Reader(IContextProvider& context, ...)"). `IContextProvider` is the interface that `Reader` and `Builder` constructors accept. `Context` is the SDK's built-in implementation of this interface. +**Deprecated:** -When the deprecation warning says "Use Reader(IContextProvider& context, ...)", passing a `Context` object satisfies that parameter. +```cpp +c2pa::Context context; +c2pa::Reader reader(context, "image.jpg"); // reference — context not kept alive +c2pa::Builder builder(context, manifest_json); // reference — context not kept alive +c2pa::Reader::from_asset(context, "image.jpg"); // reference — context not kept alive +``` -External libraries can also implement `IContextProvider` to provide their own context objects (for example, wrapping a platform-specific configuration system). The interface is minimal: any class that can produce a valid `C2paContext*` pointer and report its validity can serve as a context provider. This becomes relevant when building integrations that need to manage context lifetime or initialization differently than the SDK's `Context` class does. +**With shared_ptr:** + +```cpp +auto context = std::make_shared(); +c2pa::Reader reader(context, "image.jpg"); +c2pa::Builder builder(context, manifest_json); +c2pa::Reader::from_asset(context, "image.jpg"); +``` + +The `shared_ptr` overloads accept any `shared_ptr`, so custom `IContextProvider` implementations work the same way, wrap them in a `shared_ptr` before passing. + +#### About IContextProvider + +`IContextProvider` is the interface that `Reader` and `Builder` constructors accept. `Context` is the SDK's built-in implementation. The deprecation warnings reference it in the suggested replacement (e.g., `"Use Reader(std::shared_ptr, ...)`"). + +External libraries can implement `IContextProvider` to supply their own context objects. The interface requires a valid `C2paContext*` pointer and an `is_valid()` check. Wrap your implementation in a `shared_ptr` when passing to `Reader` or `Builder`. ### Builder::sign overloads @@ -982,10 +1004,10 @@ If a signer is configured in the `Context` (through settings JSON or `ContextBui ```cpp c2pa::Signer signer("es256", certs, key, tsa_url); -auto context = c2pa::Context::ContextBuilder() +auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_json(settings_json) .with_signer(std::move(signer)) // signer is consumed here - .create_context(); + .create_context()); c2pa::Builder builder(context, manifest_json); diff --git a/include/c2pa.hpp b/include/c2pa.hpp index f890a4c..46767a9 100644 --- a/include/c2pa.hpp +++ b/include/c2pa.hpp @@ -728,6 +728,11 @@ namespace c2pa C2paReader *c2pa_reader; std::unique_ptr owned_stream; // Owns file stream when created from path std::unique_ptr cpp_stream; // Wraps stream for C API; destroyed before owned_stream + std::shared_ptr context_ref; + + void init_from_context(IContextProvider& context, const std::string &format, std::istream &stream); + void init_from_context(IContextProvider& context, const std::filesystem::path &source_path); + Reader() : c2pa_reader(nullptr) {} public: /// @brief Create a Reader from a context and stream. @@ -736,6 +741,12 @@ namespace c2pa /// @param stream The input stream to read from. /// @throws C2paException if context.is_valid() returns false, /// or for other errors encountered by the C2PA library. + /// @deprecated Use Reader(std::shared_ptr, format, stream) instead. + /// The reference overload does not extend the lifetime of the context, which can + /// be problematic when progress callbacks fire after the context is destroyed. + [[deprecated("Use Reader(std::shared_ptr, format, stream) instead. " + "The reference overload does not extend the lifetime of the context, which can " + "be problematic when progress callbacks fire after the context is destroyed.")]] Reader(IContextProvider& context, const std::string &format, std::istream &stream); /// @brief Create a Reader from a context and file path. @@ -744,8 +755,31 @@ namespace c2pa /// @throws C2paException if context.is_valid() returns false, /// or for other errors encountered by the C2PA library. /// @note Prefer using the streaming APIs if possible. + /// @deprecated Use Reader(std::shared_ptr, source_path) instead. + /// The reference overload does not extend the lifetime of the context, which can + /// be problematic when progress callbacks fire after the context is destroyed. + [[deprecated("Use Reader(std::shared_ptr, source_path) instead. " + "The reference overload does not extend the lifetime of the context, which can " + "be problematic when progress callbacks fire after the context is destroyed.")]] Reader(IContextProvider& context, const std::filesystem::path &source_path); + /// @brief Create a Reader from a shared context and stream. + /// @details The Reader retains a shared reference to the context, keeping it + /// alive for the lifetime of the Reader. + /// @param context Shared context provider. + /// @param format The mime format of the stream. + /// @param stream The input stream to read from. + /// @throws C2paException if context is null or context->is_valid() returns false. + Reader(std::shared_ptr context, const std::string &format, std::istream &stream); + + /// @brief Create a Reader from a shared context and file path. + /// @details The Reader retains a shared reference to the context, keeping it + /// alive for the lifetime of the Reader. + /// @param context Shared context provider. + /// @param source_path The path to the file to read. + /// @throws C2paException if context is null or context->is_valid() returns false. + Reader(std::shared_ptr context, const std::filesystem::path &source_path); + /// @brief Create a Reader from a stream (will use global settings if any loaded). /// @details The validation_status field in the JSON contains validation results. /// @param format The mime format of the stream. @@ -767,13 +801,39 @@ namespace c2pa /// @return A Reader if JUMBF (c2pa/manifest) data is present; std::nullopt if none. /// @throws C2paException for errors other than a missing manifest (e.g. invalid asset). /// @throws std::system_error if the file cannot be opened. + /// @deprecated Use from_asset(std::shared_ptr, source_path) instead. + /// The reference overload does not extend the lifetime of the context, which can + /// cause a use-after-free crash when progress callbacks fire after the context is + /// destroyed. + [[deprecated("Use from_asset(std::shared_ptr, source_path) instead. " + "The reference overload does not extend the lifetime of the context, which can " + "be problematic when progress callbacks fire after the context is destroyed.")]] static std::optional from_asset(IContextProvider& context, const std::filesystem::path &source_path); /// @brief Try to create a Reader from a context and stream when the asset may lack C2PA data. /// @return A Reader if JUMBF (c2pa/manifest) data is present; std::nullopt if none. /// @throws C2paException for errors other than a missing manifest. + /// @deprecated Use from_asset(std::shared_ptr, format, stream) instead. + /// The reference overload does not extend the lifetime of the context, which can + /// cause a use-after-free crash when progress callbacks fire after the context is + /// destroyed. + [[deprecated("Use from_asset(std::shared_ptr, format, stream) instead. " + "The reference overload does not extend the lifetime of the context, which can " + "be problematic when progress callbacks fire after the context is destroyed.")]] static std::optional from_asset(IContextProvider& context, const std::string &format, std::istream &stream); + /// @brief Try to open a Reader from a shared context and file path when the asset may lack C2PA data. + /// @details The Reader retains a shared reference to the context if C2PA data is found. + /// @return A Reader if JUMBF (c2pa/manifest) data is present; std::nullopt if none. + /// @throws C2paException for errors other than a missing manifest. + static std::optional from_asset(std::shared_ptr context, const std::filesystem::path &source_path); + + /// @brief Try to create a Reader from a shared context and stream when the asset may lack C2PA data. + /// @details The Reader retains a shared reference to the context if C2PA data is found. + /// @return A Reader if JUMBF (c2pa/manifest) data is present; std::nullopt if none. + /// @throws C2paException for errors other than a missing manifest. + static std::optional from_asset(std::shared_ptr context, const std::string &format, std::istream &stream); + // Non-copyable Reader(const Reader&) = delete; @@ -782,7 +842,8 @@ namespace c2pa Reader(Reader&& other) noexcept : c2pa_reader(std::exchange(other.c2pa_reader, nullptr)), owned_stream(std::move(other.owned_stream)), - cpp_stream(std::move(other.cpp_stream)) { + cpp_stream(std::move(other.cpp_stream)), + context_ref(std::move(other.context_ref)) { } Reader& operator=(Reader&& other) noexcept { @@ -791,6 +852,7 @@ namespace c2pa c2pa_reader = std::exchange(other.c2pa_reader, nullptr); owned_stream = std::move(other.owned_stream); cpp_stream = std::move(other.cpp_stream); + context_ref = std::move(other.context_ref); } return *this; } @@ -939,12 +1001,22 @@ namespace c2pa { private: C2paBuilder *builder; + std::shared_ptr context_ref; + + void init_from_context(IContextProvider& context); + void init_from_context(IContextProvider& context, const std::string &manifest_json); public: /// @brief Create a Builder from a context with an empty manifest. /// @param context Context provider; used at construction to configure settings. /// @throws C2paException if context.is_valid() returns false, /// or for other errors encountered by the C2PA library. + /// @deprecated Use Builder(std::shared_ptr) instead. + /// The reference overload does not extend the lifetime of the context, which can + /// be problematic when progress callbacks fire after the context is destroyed. + [[deprecated("Use Builder(std::shared_ptr) instead. " + "The reference overload does not extend the lifetime of the context, which can " + "be problematic when progress callbacks fire after the context is destroyed.")]] explicit Builder(IContextProvider& context); /// @brief Create a Builder from a context and manifest JSON string. @@ -952,8 +1024,30 @@ namespace c2pa /// @param manifest_json The manifest JSON string. /// @throws C2paException if context.is_valid() returns false, /// or for other errors encountered by the C2PA library. + /// @deprecated Use Builder(std::shared_ptr, manifest_json) instead. + /// The reference overload does not extend the lifetime of the context, which can + /// be problematic when progress callbacks fire after the context is destroyed. + [[deprecated("Use Builder(std::shared_ptr, manifest_json) instead. " + "The reference overload does not extend the lifetime of the context, which can " + "be problematic when progress callbacks fire after the context is destroyed.")]] Builder(IContextProvider& context, const std::string &manifest_json); + /// @brief Create a Builder from a shared context with an empty manifest. + /// @details The Builder retains a shared reference to the context, keeping it + /// alive for the lifetime of the Builder. This is the preferred + /// constructor when the Context may be destroyed before the Builder. + /// @param context Shared context provider. + /// @throws C2paException if context is null or context->is_valid() returns false. + explicit Builder(std::shared_ptr context); + + /// @brief Create a Builder from a shared context and manifest JSON string. + /// @details The Builder retains a shared reference to the context, keeping it + /// alive for the lifetime of the Builder. + /// @param context Shared context provider. + /// @param manifest_json The manifest JSON string. + /// @throws C2paException if context is null or context->is_valid() returns false. + Builder(std::shared_ptr context, const std::string &manifest_json); + /// @brief Create a Builder from a manifest JSON string (will use global settings if any loaded). /// @param manifest_json The manifest JSON string. /// @throws C2paException for errors encountered by the C2PA library. @@ -970,13 +1064,16 @@ namespace c2pa Builder& operator=(const Builder&) = delete; - Builder(Builder&& other) noexcept : builder(std::exchange(other.builder, nullptr)) { + Builder(Builder&& other) noexcept + : builder(std::exchange(other.builder, nullptr)), + context_ref(std::move(other.context_ref)) { } Builder& operator=(Builder&& other) noexcept { if (this != &other) { c2pa_free(builder); builder = std::exchange(other.builder, nullptr); + context_ref = std::move(other.context_ref); } return *this; } diff --git a/src/c2pa_builder.cpp b/src/c2pa_builder.cpp index 16bd63d..8b1249a 100644 --- a/src/c2pa_builder.cpp +++ b/src/c2pa_builder.cpp @@ -30,8 +30,9 @@ namespace c2pa } } - Builder::Builder(IContextProvider& context) - : builder(nullptr) + // Shared initialization from any IContextProvider, used by both the + // overloads, so neither calls the other. + void Builder::init_from_context(IContextProvider& context) { if (!context.is_valid()) { throw C2paException("Invalid Context provider IContextProvider"); @@ -43,17 +44,11 @@ namespace c2pa } } - Builder::Builder(IContextProvider& context, const std::string &manifest_json) - : builder(nullptr) + // Shared initialization from any IContextProvider, used by both the + // overloads, so neither calls the other. + void Builder::init_from_context(IContextProvider& context, const std::string &manifest_json) { - if (!context.is_valid()) { - throw C2paException("Invalid Context provider IContextProvider"); - } - - builder = c2pa_builder_from_context(context.c_context()); - if (builder == nullptr) { - throw C2paException("Failed to create builder from context"); - } + init_from_context(context); // Apply the manifest definition to the Builder. // Note: c2pa_builder_with_definition always consumes the builder pointer, @@ -66,6 +61,32 @@ namespace c2pa builder = updated; } + Builder::Builder(IContextProvider& context) + : builder(nullptr) + { + init_from_context(context); + } + + Builder::Builder(IContextProvider& context, const std::string &manifest_json) + : builder(nullptr) + { + init_from_context(context, manifest_json); + } + + Builder::Builder(std::shared_ptr context) + : builder(nullptr) + { + init_from_context(*context); + context_ref = std::move(context); + } + + Builder::Builder(std::shared_ptr context, const std::string &manifest_json) + : builder(nullptr) + { + init_from_context(*context, manifest_json); + context_ref = std::move(context); + } + Builder::Builder(const std::string &manifest_json) : builder(nullptr) { diff --git a/src/c2pa_reader.cpp b/src/c2pa_reader.cpp index 42730e9..b0d1740 100644 --- a/src/c2pa_reader.cpp +++ b/src/c2pa_reader.cpp @@ -38,8 +38,9 @@ namespace c2pa { /// Reader class for reading manifests - Reader::Reader(IContextProvider& context, const std::string &format, std::istream &stream) - : c2pa_reader(nullptr) + // Shared initialization from any IContextProvider, used by both the + // overloads, so neither calls the other. + void Reader::init_from_context(IContextProvider& context, const std::string &format, std::istream &stream) { if (!context.is_valid()) { throw C2paException("Invalid Context provider IContextProvider"); @@ -62,8 +63,9 @@ namespace c2pa c2pa_reader = updated; } - Reader::Reader(IContextProvider& context, const std::filesystem::path &source_path) - : c2pa_reader(nullptr) + // Shared initialization from any IContextProvider, used by both the + // overloads, so neither calls the other. + void Reader::init_from_context(IContextProvider& context, const std::filesystem::path &source_path) { if (!context.is_valid()) { throw C2paException("Invalid Context provider IContextProvider"); @@ -94,6 +96,32 @@ namespace c2pa c2pa_reader = updated; } + Reader::Reader(IContextProvider& context, const std::string &format, std::istream &stream) + : c2pa_reader(nullptr) + { + init_from_context(context, format, stream); + } + + Reader::Reader(IContextProvider& context, const std::filesystem::path &source_path) + : c2pa_reader(nullptr) + { + init_from_context(context, source_path); + } + + Reader::Reader(std::shared_ptr context, const std::string &format, std::istream &stream) + : c2pa_reader(nullptr) + { + init_from_context(*context, format, stream); + context_ref = std::move(context); + } + + Reader::Reader(std::shared_ptr context, const std::filesystem::path &source_path) + : c2pa_reader(nullptr) + { + init_from_context(*context, source_path); + context_ref = std::move(context); + } + Reader::Reader(const std::string &format, std::istream &stream) { cpp_stream = std::make_unique(stream); @@ -172,10 +200,26 @@ namespace c2pa } std::optional Reader::from_asset(IContextProvider& context, const std::filesystem::path& source_path) { - return reader_from_asset_impl([&]() { return Reader(context, source_path); }); + return reader_from_asset_impl([&]() { + Reader r; + r.init_from_context(context, source_path); + return r; + }); } std::optional Reader::from_asset(IContextProvider& context, const std::string& format, std::istream& stream) { - return reader_from_asset_impl([&]() { return Reader(context, format, stream); }); + return reader_from_asset_impl([&]() { + Reader r; + r.init_from_context(context, format, stream); + return r; + }); + } + + std::optional Reader::from_asset(std::shared_ptr context, const std::filesystem::path& source_path) { + return reader_from_asset_impl([&]() { return Reader(std::move(context), source_path); }); + } + + std::optional Reader::from_asset(std::shared_ptr context, const std::string& format, std::istream& stream) { + return reader_from_asset_impl([&]() { return Reader(std::move(context), format, stream); }); } } // namespace c2pa diff --git a/tests/context.test.cpp b/tests/context.test.cpp index 5ba0762..8a943bf 100644 --- a/tests/context.test.cpp +++ b/tests/context.test.cpp @@ -98,8 +98,9 @@ TEST(Context, SettingsDefaultConstruction) c2pa::Context context; // Should not crash when building with default settings + auto ctx = std::make_shared(); EXPECT_NO_THROW({ - c2pa::Builder builder(context, manifest); + c2pa::Builder builder(ctx, manifest); }); } @@ -133,8 +134,9 @@ TEST(Context, MoveConstructor) // Moved-to context is usable auto manifest = load_fixture("training.json"); + auto ctx = std::make_shared(std::move(moved_to)); EXPECT_NO_THROW({ - c2pa::Builder builder(moved_to, manifest); + c2pa::Builder builder(ctx, manifest); }); } @@ -164,8 +166,9 @@ TEST(Context, MoveAssignmentOverwrites) EXPECT_FALSE(b.is_valid()); // Use a to ensure the adopted context works (no double-free of old a) auto manifest = load_fixture("training.json"); + auto ctx = std::make_shared(std::move(a)); EXPECT_NO_THROW({ - c2pa::Builder builder(a, manifest); + c2pa::Builder builder(ctx, manifest); }); } @@ -176,8 +179,8 @@ static bool has_thumbnail(const std::string& manifest_json) { return parsed["manifests"][active].contains("thumbnail"); } -// Helper function to sign with context and return manifest JSON -static std::string sign_with_context(c2pa::IContextProvider& context, const fs::path& dest_path) { +// Helper function to sign with context and return manifest JSON. +static std::string sign_with_context(std::shared_ptr context, const fs::path& dest_path) { auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); auto certs = c2pa_test::read_text_file(c2pa_test::get_fixture_path("es256_certs.pem")); auto private_key = c2pa_test::read_text_file(c2pa_test::get_fixture_path("es256_private.key")); @@ -199,7 +202,7 @@ TEST_F(ContextTest, SetOverridesLastWins) { settings.set("builder.thumbnail.enabled", "true"); settings.set("builder.thumbnail.enabled", "false"); - auto context = c2pa::Context::ContextBuilder().with_settings(settings).create_context(); + auto context = std::make_shared(c2pa::Context::ContextBuilder().with_settings(settings).create_context()); auto manifest_json = sign_with_context(context, get_temp_path("set_overrides_last_wins.jpg")); EXPECT_FALSE(has_thumbnail(manifest_json)); @@ -212,7 +215,7 @@ TEST_F(ContextTest, UpdateOverridesSetJson) { settings.set("builder.thumbnail.enabled", "true"); settings.update(settings_json, "json"); - auto context = c2pa::Context::ContextBuilder().with_settings(settings).create_context(); + auto context = std::make_shared(c2pa::Context::ContextBuilder().with_settings(settings).create_context()); auto manifest_json = sign_with_context(context, get_temp_path("update_overrides_set_json.jpg")); EXPECT_FALSE(has_thumbnail(manifest_json)); @@ -225,7 +228,7 @@ TEST_F(ContextTest, SetOverridesUpdateJson) { settings.update(settings_json, "json"); settings.set("builder.thumbnail.enabled", "true"); - auto context = c2pa::Context::ContextBuilder().with_settings(settings).create_context(); + auto context = std::make_shared(c2pa::Context::ContextBuilder().with_settings(settings).create_context()); auto manifest_json = sign_with_context(context, get_temp_path("set_overrides_update_json.jpg")); EXPECT_TRUE(has_thumbnail(manifest_json)); @@ -237,10 +240,10 @@ TEST_F(ContextTest, WithSettingsThenWithJson) { c2pa::Settings settings; settings.set("builder.thumbnail.enabled", "true"); - auto context = c2pa::Context::ContextBuilder() + auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_settings(settings) .with_json(settings_json) - .create_context(); + .create_context()); auto manifest_json = sign_with_context(context, get_temp_path("with_settings_then_json.jpg")); EXPECT_FALSE(has_thumbnail(manifest_json)); @@ -252,10 +255,10 @@ TEST_F(ContextTest, WithJsonThenWithSettings) { c2pa::Settings settings; settings.set("builder.thumbnail.enabled", "false"); - auto context = c2pa::Context::ContextBuilder() + auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_json(settings_json) .with_settings(settings) - .create_context(); + .create_context()); auto manifest_json = sign_with_context(context, get_temp_path("with_json_then_settings.jpg")); EXPECT_FALSE(has_thumbnail(manifest_json)); @@ -354,8 +357,7 @@ TEST(Context, DirectConstructWithSettings) { // Default constructor can be used with Builder TEST(Context, DirectConstructDefaultWithBuilder) { auto manifest = load_fixture("training.json"); - c2pa::Context context; - + auto context = std::make_shared(); EXPECT_NO_THROW({ c2pa::Builder builder(context, manifest); }); @@ -366,7 +368,7 @@ TEST_F(ContextTest, DirectConstructSettingsSignVerify) { c2pa::Settings settings; settings.set("builder.thumbnail.enabled", "false"); - c2pa::Context context(settings); + auto context = std::make_shared(settings); auto manifest_json = sign_with_context(context, get_temp_path("direct_construct_settings.jpg")); EXPECT_FALSE(has_thumbnail(manifest_json)); @@ -374,7 +376,7 @@ TEST_F(ContextTest, DirectConstructSettingsSignVerify) { // 2) Direct default construction: sign and verify thumbnail is enabled (default) TEST_F(ContextTest, DirectConstructDefaultSignVerify) { - c2pa::Context context; + auto context = std::make_shared(); auto manifest_json = sign_with_context(context, get_temp_path("direct_construct_default.jpg")); EXPECT_TRUE(has_thumbnail(manifest_json)); @@ -382,7 +384,7 @@ TEST_F(ContextTest, DirectConstructDefaultSignVerify) { // 3) JSON string constructor: sign and verify thumbnail is disabled TEST_F(ContextTest, JsonConstructorSignVerify) { - c2pa::Context context(R"({"builder": {"thumbnail": {"enabled": false}}})"); + auto context = std::make_shared(R"({"builder": {"thumbnail": {"enabled": false}}})"); auto manifest_json = sign_with_context(context, get_temp_path("json_constructor.jpg")); EXPECT_FALSE(has_thumbnail(manifest_json)); @@ -393,9 +395,9 @@ TEST_F(ContextTest, ContextBuilderWithSettingsSignVerify) { c2pa::Settings settings; settings.set("builder.thumbnail.enabled", "false"); - auto context = c2pa::Context::ContextBuilder() + auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_settings(settings) - .create_context(); + .create_context()); auto manifest_json = sign_with_context(context, get_temp_path("builder_with_settings.jpg")); EXPECT_FALSE(has_thumbnail(manifest_json)); @@ -403,9 +405,9 @@ TEST_F(ContextTest, ContextBuilderWithSettingsSignVerify) { // 5) ContextBuilder with JSON: sign and verify thumbnail is disabled TEST_F(ContextTest, ContextBuilderWithJsonSignVerify) { - auto context = c2pa::Context::ContextBuilder() + auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_json(R"({"builder": {"thumbnail": {"enabled": false}}})") - .create_context(); + .create_context()); auto manifest_json = sign_with_context(context, get_temp_path("builder_with_json.jpg")); EXPECT_FALSE(has_thumbnail(manifest_json)); @@ -413,7 +415,7 @@ TEST_F(ContextTest, ContextBuilderWithJsonSignVerify) { // 6) ContextBuilder empty (default): sign and verify thumbnail is enabled (default) TEST_F(ContextTest, ContextBuilderDefaultSignVerify) { - auto context = c2pa::Context::ContextBuilder().create_context(); + auto context = std::make_shared(c2pa::Context::ContextBuilder().create_context()); auto manifest_json = sign_with_context(context, get_temp_path("builder_default.jpg")); EXPECT_TRUE(has_thumbnail(manifest_json)); @@ -424,7 +426,7 @@ TEST_F(ContextTest, DirectConstructSettingsEnableThumbnailSignVerify) { c2pa::Settings settings; settings.set("builder.thumbnail.enabled", "true"); - c2pa::Context context(settings); + auto context = std::make_shared(settings); auto manifest_json = sign_with_context(context, get_temp_path("direct_construct_enable_thumb.jpg")); EXPECT_TRUE(has_thumbnail(manifest_json)); @@ -434,9 +436,9 @@ TEST_F(ContextTest, DirectConstructSettingsEnableThumbnailSignVerify) { TEST_F(ContextTest, ContextBuilderWithJsonSettingsFile) { auto settings_path = c2pa_test::get_fixture_path("settings/test_settings_no_thumbnail.json"); - auto context = c2pa::Context::ContextBuilder() + auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_json_settings_file(settings_path) - .create_context(); + .create_context()); auto manifest_json = sign_with_context(context, get_temp_path("with_json_settings_file.jpg")); EXPECT_FALSE(has_thumbnail(manifest_json)); @@ -458,25 +460,25 @@ TEST_F(ContextTest, ContextBuilderWithJsonSettingsFileChaining) { c2pa::Settings override_settings; override_settings.set("builder.thumbnail.enabled", "false"); - auto context = c2pa::Context::ContextBuilder() + auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_json_settings_file(settings_path) .with_settings(override_settings) - .create_context(); + .create_context()); auto manifest_json = sign_with_context(context, get_temp_path("with_json_settings_file_chained.jpg")); EXPECT_FALSE(has_thumbnail(manifest_json)); } -// Context is copied at construction, Reader still works after context goes out of scope +// Reader retains a shared reference to the context, keeping it alive for the lifetime of the Reader. TEST_F(ContextTest, ReaderWorksAfterContextOutOfScope) { fs::path signed_path = get_temp_path("reader_after_context_scope.jpg"); std::unique_ptr reader; { - c2pa::Context context; + auto context = std::make_shared(); sign_with_context(context, signed_path); reader = std::make_unique(context, signed_path); + // context goes out of scope here, but reader holds a shared reference } - // context is out of scope, implementation copies context state so reader still works EXPECT_NO_THROW(reader->json()); } @@ -496,7 +498,7 @@ TEST(Context, ContextBuilderWithSettingsAndSigner) { // Progress/cancel tests, available since c2pa-rs == 0.78.7. // Helper: sign a file and return the signed path, using a context with a progress callback. -static fs::path sign_with_progress_context(c2pa::IContextProvider& context, const fs::path& dest) { +static fs::path sign_with_progress_context(std::shared_ptr context, const fs::path& dest) { auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); auto certs = c2pa_test::read_text_file(c2pa_test::get_fixture_path("es256_certs.pem")); auto private_key = c2pa_test::read_text_file(c2pa_test::get_fixture_path("es256_private.key")); @@ -512,14 +514,14 @@ static fs::path sign_with_progress_context(c2pa::IContextProvider& context, cons TEST_F(ContextTest, ProgressCallback_InvokedDuringSigning) { std::atomic call_count{0}; - auto context = c2pa::Context::ContextBuilder() + auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_progress_callback([&](c2pa::ProgressPhase /*phase*/, uint32_t /*step*/, uint32_t /*total*/) { ++call_count; return true; }) - .create_context(); + .create_context()); - ASSERT_TRUE(context.is_valid()); + ASSERT_TRUE(context->is_valid()); EXPECT_NO_THROW(sign_with_progress_context(context, get_temp_path("progress_signing.jpg"))); EXPECT_GT(call_count.load(), 0); } @@ -528,20 +530,20 @@ TEST_F(ContextTest, ProgressCallback_InvokedDuringSigning) { TEST_F(ContextTest, ProgressCallback_InvokedDuringReading) { // First sign a file without a callback so we have something to read. { - c2pa::Context sign_ctx; + auto sign_ctx = std::make_shared(); sign_with_progress_context(sign_ctx, get_temp_path("progress_read_src.jpg")); } std::atomic call_count{0}; - auto context = c2pa::Context::ContextBuilder() + auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_progress_callback([&](c2pa::ProgressPhase /*phase*/, uint32_t /*step*/, uint32_t /*total*/) { ++call_count; return true; }) - .create_context(); + .create_context()); - ASSERT_TRUE(context.is_valid()); + ASSERT_TRUE(context->is_valid()); EXPECT_NO_THROW({ c2pa::Reader reader(context, get_temp_path("progress_read_src.jpg")); (void)reader.json(); @@ -553,7 +555,7 @@ TEST_F(ContextTest, ProgressCallback_InvokedDuringReading) { TEST_F(ContextTest, ProgressCallback_StepAndTotalValues) { bool saw_valid_step = false; - auto context = c2pa::Context::ContextBuilder() + auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_progress_callback([&](c2pa::ProgressPhase /*phase*/, uint32_t step, uint32_t total) { // step is 1-based when total > 0; both may be 0 for indeterminate phases. if (total > 0) { @@ -563,9 +565,9 @@ TEST_F(ContextTest, ProgressCallback_StepAndTotalValues) { } return true; }) - .create_context(); + .create_context()); - ASSERT_TRUE(context.is_valid()); + ASSERT_TRUE(context->is_valid()); EXPECT_NO_THROW(sign_with_progress_context(context, get_temp_path("progress_step_total.jpg"))); EXPECT_TRUE(saw_valid_step); } @@ -573,13 +575,13 @@ TEST_F(ContextTest, ProgressCallback_StepAndTotalValues) { // Returning false from the callback causes the operation to be cancelled. TEST_F(ContextTest, ProgressCallback_ReturnFalseCancels) { // Cancel on the very first callback invocation. - auto context = c2pa::Context::ContextBuilder() + auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_progress_callback([](c2pa::ProgressPhase /*phase*/, uint32_t /*step*/, uint32_t /*total*/) { return false; // request cancellation }) - .create_context(); + .create_context()); - ASSERT_TRUE(context.is_valid()); + ASSERT_TRUE(context->is_valid()); EXPECT_THROW( sign_with_progress_context(context, get_temp_path("progress_cancel_false.jpg")), c2pa::C2paException @@ -588,31 +590,24 @@ TEST_F(ContextTest, ProgressCallback_ReturnFalseCancels) { // Context::cancel() called before an operation prevents that operation from completing. TEST_F(ContextTest, ProgressCallback_CancelMethodAbortsOperation) { - auto context = c2pa::Context::ContextBuilder() - .with_progress_callback([](c2pa::ProgressPhase /*phase*/, uint32_t /*step*/, uint32_t /*total*/) { - return true; - }) - .create_context(); - - ASSERT_TRUE(context.is_valid()); - - // Cancel is called from within the callback (simulates a cross-thread cancel). - c2pa::Context* ctx_ptr = &context; + // ctx is declared before the lambda so the lambda can capture it by reference. + // It is assigned after construction since the context must be built first. + std::shared_ptr ctx; bool cancel_called = false; - auto context2 = c2pa::Context::ContextBuilder() + + ctx = std::make_shared(c2pa::Context::ContextBuilder() .with_progress_callback([&](c2pa::ProgressPhase /*phase*/, uint32_t /*step*/, uint32_t /*total*/) { if (!cancel_called) { cancel_called = true; - ctx_ptr->cancel(); + ctx->cancel(); } return true; // continue returning true; cancellation is handled via cancel() }) - .create_context(); + .create_context()); - ASSERT_TRUE(context2.is_valid()); - ctx_ptr = &context2; + ASSERT_TRUE(ctx->is_valid()); EXPECT_THROW( - sign_with_progress_context(context2, get_temp_path("progress_cancel_method.jpg")), + sign_with_progress_context(ctx, get_temp_path("progress_cancel_method.jpg")), c2pa::C2paException ); } @@ -631,15 +626,15 @@ TEST_F(ContextTest, ProgressCallback_ChainWithSettings) { c2pa::Settings settings; settings.set("builder.thumbnail.enabled", "false"); - auto context = c2pa::Context::ContextBuilder() + auto context = std::make_shared(c2pa::Context::ContextBuilder() .with_settings(settings) .with_progress_callback([&](c2pa::ProgressPhase /*phase*/, uint32_t /*step*/, uint32_t /*total*/) { ++call_count; return true; }) - .create_context(); + .create_context()); - ASSERT_TRUE(context.is_valid()); + ASSERT_TRUE(context->is_valid()); auto manifest_json = sign_with_context(context, get_temp_path("progress_chain_settings.jpg")); EXPECT_GT(call_count.load(), 0); EXPECT_FALSE(has_thumbnail(manifest_json)); @@ -656,9 +651,9 @@ TEST_F(ContextTest, ProgressCallback_SurvivesContextMove) { }) .create_context(); - c2pa::Context moved_to(std::move(original)); + auto moved_to = std::make_shared(std::move(original)); EXPECT_FALSE(original.is_valid()); - ASSERT_TRUE(moved_to.is_valid()); + ASSERT_TRUE(moved_to->is_valid()); EXPECT_NO_THROW(sign_with_progress_context(moved_to, get_temp_path("progress_move.jpg"))); EXPECT_GT(call_count.load(), 0); @@ -678,9 +673,88 @@ TEST_F(ContextTest, ProgressCallback_SurvivesBuilderMove) { EXPECT_FALSE(b1.is_valid()); ASSERT_TRUE(b2.is_valid()); - auto context = b2.create_context(); - ASSERT_TRUE(context.is_valid()); + auto context = std::make_shared(b2.create_context()); + ASSERT_TRUE(context->is_valid()); EXPECT_NO_THROW(sign_with_progress_context(context, get_temp_path("progress_builder_move.jpg"))); EXPECT_GT(call_count.load(), 0); } + +// Builder keeps the Context alive to use progress callbacks +TEST_F(ContextTest, BuilderKeepsContextAlive) { + std::atomic call_count{0}; + + c2pa::Builder builder = [&]() { + auto ctx = std::make_shared( + c2pa::Context::ContextBuilder() + .with_progress_callback([&](c2pa::ProgressPhase, uint32_t, uint32_t) { + ++call_count; + return true; + }) + .create_context() + ); + auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + return c2pa::Builder(ctx, manifest); + // ctx goes out of scope here + }(); + + auto certs = c2pa_test::read_text_file(c2pa_test::get_fixture_path("es256_certs.pem")); + auto private_key = c2pa_test::read_text_file(c2pa_test::get_fixture_path("es256_private.key")); + c2pa::Signer signer("es256", certs, private_key); + + EXPECT_NO_THROW(builder.sign(c2pa_test::get_fixture_path("A.jpg"), get_temp_path("shared_ptr_builder.jpg"), signer)); + EXPECT_GT(call_count.load(), 0); +} + +// Reader keeps the Context alive via shared_ptr. +TEST_F(ContextTest, ReaderKeepsContextAlive) { + // Sign a file first so we have something to read. + { + auto sign_ctx = std::make_shared(); + sign_with_progress_context(sign_ctx, get_temp_path("shared_ptr_reader_src.jpg")); + } + + std::atomic call_count{0}; + + c2pa::Reader reader = [&]() { + auto ctx = std::make_shared( + c2pa::Context::ContextBuilder() + .with_progress_callback([&](c2pa::ProgressPhase, uint32_t, uint32_t) { + ++call_count; + return true; + }) + .create_context() + ); + return c2pa::Reader(ctx, get_temp_path("shared_ptr_reader_src.jpg")); + // ctx goes out of scope, but reader holds a copy + }(); + + EXPECT_NO_THROW((void)reader.json()); + EXPECT_GT(call_count.load(), 0); +} + +// Move-constructing a Builder transfers the shared context reference. +TEST_F(ContextTest, SharedPtrContextMoveTransfersOwnership) { + auto ctx = std::make_shared(); + auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + + c2pa::Builder b1(ctx, manifest); + EXPECT_EQ(ctx.use_count(), 2); + + c2pa::Builder b2 = std::move(b1); + EXPECT_EQ(ctx.use_count(), 2); // b1 released, b2 took over +} + +// Reader::from_asset works with shared_ptr context. +TEST_F(ContextTest, SharedPtrContext_FromAsset) { + // Sign a file first so we have something with C2PA data. + { + auto sign_ctx = std::make_shared(); + sign_with_progress_context(sign_ctx, get_temp_path("shared_ptr_from_asset_src.jpg")); + } + + auto ctx = std::make_shared(); + auto reader = c2pa::Reader::from_asset(ctx, get_temp_path("shared_ptr_from_asset_src.jpg")); + EXPECT_TRUE(reader.has_value()); + EXPECT_EQ(ctx.use_count(), 2); // ctx + reader +}