diff --git a/docs/context-settings.md b/docs/context-settings.md index 5f12515..d16a457 100644 --- a/docs/context-settings.md +++ b/docs/context-settings.md @@ -165,7 +165,7 @@ auto context = c2pa::Context::ContextBuilder() | `with_signer(signer)` | Store a `Signer` in the context (consumed; used by `Builder::sign` with no explicit signer) | | `with_progress_callback(callback)` | Register a progress/cancel callback (see [Progress callbacks and cancellation](#progress-callbacks-and-cancellation)) | | `create_context()` | Build and return the `Context` (consumes the builder) | -| `release()` | Release the raw `C2paContextBuilder*` together with its progress-callback heap owner (see also [Releasing to a custom IContextProvider](#releasing-to-a-custom-icontextprovider)) | +| `release()` | Release the raw `C2paContextBuilder*` together with its progress and in-place signer callback owners (see also [Releasing to a custom IContextProvider](#releasing-to-a-custom-icontextprovider)) | ## Progress callbacks and cancellation @@ -883,7 +883,7 @@ c2pa::Context context(R"({ ## Migrating from deprecated APIs -The SDK introduced Context-based APIs to replace constructors and functions that relied on thread-local state. The older APIs still compile but produce deprecation warnings. This section covers each deprecation that involves passing a `Context`, and explains the `Builder::sign` overloads. +The SDK introduced Context-based APIs to replace constructors and functions that relied on thread-local state. `load_settings` has been removed; the older Reader and Builder constructors remain deprecated and now use default Contexts. This section covers migration to an explicit `Context`, and explains the `Builder::sign` overloads. ### Quick reference @@ -898,7 +898,7 @@ The SDK introduced Context-based APIs to replace constructors and functions that ### Replacing load_settings -`c2pa::load_settings(data, format)` sets thread-local settings that `Reader` and `Builder` pick up implicitly. Replace it with a `Context` that you pass explicitly. +The removed `c2pa::load_settings(data, format)` function set thread-local settings that `Reader` and `Builder` picked up implicitly. Replace it with a `Context` that you pass explicitly. | Aspect | load_settings (legacy) | Context | |--------|------------------------|---------| @@ -986,14 +986,28 @@ External libraries can implement `IContextProvider` to supply their own context #### Releasing to a custom IContextProvider -`Context::ContextBuilder::release()` hands off both the native `C2paContextBuilder*` and the heap-owned progress callback (if any) to the caller. It is the entry point when application code needs to build the native context directly or adopt it into a custom `IContextProvider` implementation. +`Context::ContextBuilder::release()` hands off the native `C2paContextBuilder*` +and both heap-owned callbacks (if any) to the caller. It is the entry point +when application code needs to build the native context directly or adopt it +into a custom `IContextProvider` implementation. -`ReleasedBuilder` exposes two fields: +`ReleasedBuilder` exposes three fields: - `builder`: the raw `C2paContextBuilder*`. It must be passed to `c2pa_context_builder_build` exactly once. - `callback_owner`: `std::unique_ptr`; `nullptr` when no progress callback was set. When non-null, it must remain alive at least as long as the native context built from `builder`. If it is destroyed earlier, the library will dereference freed memory on the next progress tick. - -When the built-in `Context` is sufficient, `create_context()` is preferred. It performs the same handoff internally and keeps the native handle and callback owner bound together. +- `signer_callback_owner`: `std::unique_ptr`; `nullptr` + unless an in-place callback signer was set. Keep it alive until all native + objects using the signer, including contexts and derived Builders, are + freed. Free native objects before destroying either callback owner. + +To adopt the result into the built-in Context, pass both owners to +`Context(native_context, std::move(released.callback_owner), +std::move(released.signer_callback_owner))` after successfully building the +native context. Do not drop the third field when adapting an older provider. + +When the built-in `Context` is sufficient, `create_context()` is preferred. It +performs the same handoff internally and keeps the native handle and both +callback owners bound together. ### Builder::sign overloads diff --git a/docs/embeddable-api.md b/docs/embeddable-api.md index 572af2f..4b60d4c 100644 --- a/docs/embeddable-api.md +++ b/docs/embeddable-api.md @@ -189,11 +189,12 @@ These methods perform the signing workflow: placeholder creation, hashing, and s | `Builder::update_hash_from_stream(format, stream)` | Reads the asset stream and computes the hard-binding hash. Automatically selects the appropriate path based on format: `BmffHash` for BMFF (skips manifest box), `BoxHash` for chunk-based formats (creates assertion if needed), or `DataHash` (skips exclusion ranges). Takes a `std::istream&`. | | `Builder::sign_embeddable(format)` | Signs the manifest and returns bytes ready to embed. For placeholder workflows, it pads the output to match the placeholder size for in-place patching. For BoxHash workflows, the output is the actual signed manifest size (not padded), suitable for appending as a new chunk. | -### Deprecated methods +### Removed methods -The c2pa-rs 0.92.0 removes C FFI functions behind some `Builder` methods. These methods will be removed when c2pa-c moves to c2pa-rs 0.92.0. +The following deprecated methods have been removed to support c2pa-rs 0.92.0, +which removes their underlying C FFI functions. -| Deprecated method | Replacement | +| Removed method | Replacement | |-------------------|-------------| | `data_hashed_placeholder(reserved_size, format)` | `needs_placeholder(format)`, then `placeholder(format)`. The reserve size comes from the signer on the Context. | | `sign_data_hashed_embeddable(signer, data_hash, format, asset)` | `set_data_hash_exclusions(exclusions)`, `update_hash_from_stream(format, stream)`, then `sign_embeddable(format)`. The signer comes from the Context. | @@ -201,6 +202,12 @@ The c2pa-rs 0.92.0 removes C FFI functions behind some `Builder` methods. These For the full replacement flow, see [Using the DataHash placeholder](#using-the-datahash-placeholder). +Attach the signer to the `Context` before constructing the `Builder`; the +placeholder's reserve size and signing operation both use that signer. +For a precomputed hash, include a `c2pa.hash.data` assertion in the manifest +definition, then call `sign_embeddable(format)` directly without creating a +placeholder or hashing an asset stream. + ## Using the DataHash placeholder Use this workflow for JPEG, PNG, and other non-BMFF formats. diff --git a/docs/intents.md b/docs/intents.md index 4e9eae8..856a1d3 100644 --- a/docs/intents.md +++ b/docs/intents.md @@ -52,7 +52,7 @@ You can set the intent on a `Builder` instance by: - [Using Context](#using-context) - [Using `set_intent` on the `Builder`](#using-set_intent-on-builder) -Don't set intent using the deprecated `load_settings()` function. For existing code, see [Context and Settings - Migrating from deprecated APIs](context-settings.md#migrating-from-deprecated-apis). +The deprecated `load_settings()` function has been removed. For existing code, see [Context and Settings - Migrating from deprecated APIs](context-settings.md#migrating-from-deprecated-apis). ### Using Context @@ -485,4 +485,3 @@ c2pa::Builder builder(context, R"({})"); builder.set_intent(Update); builder.sign("signed_asset.jpg", "updated_asset.jpg", signer); ``` - diff --git a/docs/release-notes.md b/docs/release-notes.md index fbb7b42..58fc061 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,37 @@ # Release notes +## Unreleased + +### Zero-copy C++ APIs + +- Added move-only `ByteBuffer`, read-only and mutable byte views, and + `sign_to_buffer` siblings for all five signing overloads, plus + `placeholder_to_buffer` and `sign_embeddable_to_buffer`. Existing vector + APIs retain their signatures and copy explicitly through the owning buffer. +- Added capturing `SignerIntoFunc` callbacks that borrow native input/output + buffers directly. Heap-owned callback state survives moves and transfers + into Contexts; `ReleasedBuilder` now carries a `signer_callback_owner` + field that custom providers must retain. See [usage and ownership](usage.md). +- Documented direct stream-buffer forwarding and deferred native release of + HTTP response bodies. + +### Breaking changes + +- Removed deprecated `Builder::data_hashed_placeholder`, + `Builder::sign_data_hashed_embeddable`, and `Builder::format_embeddable` + for compatibility with c2pa-rs 0.92.0. Use the context-signer workflow in + [Embeddable manifests](embeddable-api.md#removed-methods). +- Removed deprecated `c2pa::load_settings`. Configure each `Context` using + `Settings` or `Context::ContextBuilder` instead; settings are no longer + thread-global. +- Stream wrappers now hold opaque `C2paStream` handles rather than inheriting + from the C struct. Use their existing `c_stream` member when calling the C + API, and rebuild C++ consumers against the updated headers. + +Non-deprecated method names and signatures are unchanged. Development uses a +local Rust source build; the default prebuilt-library version has not yet +been updated. + ## 3 Mar 2026 This release includes changes relating to trust validation and handling (made in the Rust library [v0.68.0](https://github.com/contentauth/c2pa-rs/releases/tag/c2pa-v0.68.0), see also [trust lists](https://opensource.contentauthenticity.org/docs/conformance/trust-lists) for more details on trust lists). @@ -10,4 +42,3 @@ Additionally, this release adds support for: - Using `Builder` and `Reader` together to [selectively construct a manifest by filtering actions and ingredients](selective-manifests.md). For answers to frequently asked questions about these features, see the [FAQs](faqs.md). - diff --git a/docs/usage.md b/docs/usage.md index 9a99fee..3edc973 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -6,6 +6,68 @@ To use this library, include the header file in your code as follows: #include "c2pa.hpp" ``` +## Native byte buffers without a copy + +Every `Builder::sign` overload has a `sign_to_buffer` sibling with the same +arguments. `placeholder_to_buffer(format)` and +`sign_embeddable_to_buffer(format)` similarly mirror the embeddable APIs. +These return a move-only `c2pa::ByteBuffer` that owns the native allocation +without copying it into a C++ vector: + +```cpp +c2pa::ByteBuffer manifest = builder.sign_to_buffer("image/jpeg", source, dest); +c2pa::ConstBytes view = manifest.view(); +// Consume view.data[0..view.size) while manifest owns the allocation. +auto copied = manifest.to_vector(); // Explicit, optional copy. +``` + +`data()`, `size()`, `empty()`, `begin()`, and `end()` provide read-only access. +Moving a buffer preserves its allocation address and empties the source. +Destruction or move-assignment over the owner invalidates its views and frees +the allocation exactly once with `c2pa_free`. Do not modify, `free`, `realloc`, +or call `c2pa_free` on `data()` yourself. The buffer may outlive its Builder. +An empty buffer may have a null `data()` pointer; do not dereference it. +Existing vector-returning methods keep their signatures and make an explicit +copy from the same owning buffer internally. + +## In-place signing callbacks + +The `Signer(SignerIntoFunc, alg, certs, tsa_uri)` overload accepts capturing +callables. Its `ConstBytes` input views the native payload and its +`MutableBytes` output views the native signature destination. An application's +crypto or HSM backend can write directly into that destination, eliminating +the vector-based callback's input and output copies. + +The callable must write no more than `signature.size` bytes, return the actual +number written, or return a negative value on failure. The wrapper rejects +oversized counts and converts exceptions to an I/O error before returning to +the C API. A short or invalid cryptographic signature fails signing; returning +a small count is not a way to truncate a signature. Never retain either view +after the callback returns. Captured state must be safe for concurrent calls. + +The callable is heap-owned at a stable address across Signer moves. Passing +the signer to `ContextBuilder::with_signer` transfers the callable along with +the native signer to the resulting Context. Prefer a `shared_ptr` +when constructing Builders so that their callback owner remains alive. +Custom providers using `ContextBuilder::release()` must keep its +`signer_callback_owner` alive until every native object using that signer is +freed; see [Context ownership](context-settings.md#releasing-to-a-custom-icontextprovider). +The existing vector-based `SignerFunc` constructor remains available. + +## Stream and HTTP callback ownership + +`CppIStream`, `CppOStream`, and `CppIOStream` pass the native callback's buffer +directly to the stream's read/write operation. There is no intermediate C++ +payload vector. Custom `std::streambuf` implementations receive those same +pointers in `xsgetn`/`xsputn`; any buffering performed inside a streambuf is +outside this adapter guarantee. The caller's streams must outlive the native +operation, and callback buffers must not be retained. + +An HTTP resolver response body must still be allocated with `malloc`. +The native library reads directly from it without an intermediate body copy, +then calls `free` when the response is dropped, including error paths. After +returning the response, the resolver must not reuse or release its body. + ## Read and validate an istream Use the `Reader` constructor to read C2PA data from a stream. This constructor examines the specified stream for C2PA data in the given format, and its return value is a `Reader` that you can use to extract more information. The constructor throws exceptions on errors. diff --git a/include/c2pa.hpp b/include/c2pa.hpp index 22855b2..9a972e8 100644 --- a/include/c2pa.hpp +++ b/include/c2pa.hpp @@ -50,6 +50,65 @@ namespace c2pa { + /// Borrowed views, valid only for the lifetime of their owner or callback. + struct ConstBytes { + const unsigned char* data; + size_t size; + }; + + struct MutableBytes { + unsigned char* data; + size_t size; + }; + + /// Write a signature in place and return its byte count, or a negative error. + /// Views must not be retained. Captured state must support concurrent calls. + using SignerIntoFunc = std::function; + + class ByteBuffer; + namespace detail { + ByteBuffer to_byte_buffer(const unsigned char* data, int64_t size); + } + + /// Owns read-only native bytes without copying. Never free data() yourself. + /// Moves transfer the allocation without changing its address. Destruction or + /// replacement of the owning buffer invalidates views and releases via c2pa_free. + class C2PA_CPP_API ByteBuffer { + public: + ByteBuffer() noexcept = default; + ByteBuffer(const ByteBuffer&) = delete; + ByteBuffer& operator=(const ByteBuffer&) = delete; + ByteBuffer(ByteBuffer&& other) noexcept + : data_(std::exchange(other.data_, nullptr)), + size_(std::exchange(other.size_, 0)) {} + ByteBuffer& operator=(ByteBuffer&& other) noexcept { + if (this != &other) { + c2pa_free(data_); + data_ = std::exchange(other.data_, nullptr); + size_ = std::exchange(other.size_, 0); + } + return *this; + } + ~ByteBuffer() { c2pa_free(data_); } + + const unsigned char* data() const noexcept { return data_; } + size_t size() const noexcept { return size_; } + bool empty() const noexcept { return size_ == 0; } + const unsigned char* begin() const noexcept { return data_; } + const unsigned char* end() const noexcept { return size_ == 0 ? data_ : data_ + size_; } + ConstBytes view() const noexcept { return {data_, size_}; } + std::vector to_vector() const { + return empty() ? std::vector{} + : std::vector(begin(), end()); + } + + private: + friend ByteBuffer detail::to_byte_buffer(const unsigned char*, int64_t); + ByteBuffer(const unsigned char* data, size_t size) noexcept : data_(data), size_(size) {} + const unsigned char* data_ = nullptr; + size_t size_ = 0; + }; + /// @typedef SignerInfo /// @brief Type alias for C2paSignerInfo from the C API. typedef C2paSignerInfo SignerInfo; @@ -154,6 +213,10 @@ namespace c2pa /// - A consistent validation pattern exists across all context-using classes /// - Errors are caught early at construction time rather than during operations /// + /// Providers adopting ContextBuilder::release() must also retain its + /// signer_callback_owner until all native objects using that signer are + /// freed. Free native handles before destroying either callback owner. + /// /// @par Standard Context Implementation /// The built-in Context class always returns true from is_valid() after /// successful construction, as it validates the context pointer in its constructor. @@ -401,7 +464,9 @@ namespace c2pa /// @details The callback is invoked synchronously whenever the SDK needs to /// make an HTTP request (remote manifest fetch, OCSP, timestamp, etc.). /// The callback receives a C2paHttpRequest and must fill in a C2paHttpResponse. - /// The response body must be allocated with malloc(); the underlying native library will call free(). + /// The response body must be allocated with malloc(); the native library + /// reads it without an intermediate copy and calls free() when the + /// response is dropped, including error paths. Do not retain or free it. /// @param user_data Opaque user pointer passed to every callback invocation (may be nullptr). /// @param callback Function pointer matching C2paHttpResolverCallback. /// @return Reference to this ContextBuilder for method chaining. @@ -416,7 +481,9 @@ namespace c2pa /// @brief Result of ContextBuilder::release(): the raw native builder handle /// paired with the heap-owned progress callback (if any). - /// @details The caller takes joint ownership. If `callback_owner` is non-null, + /// @details The caller takes joint ownership of all three fields. + /// signer_callback_owner must outlive every native object using + /// the signer, including contexts and derived builders. If `callback_owner` is non-null, /// the native side stores a raw pointer into it and will invoke the /// callback until the native context built from `builder` is freed. /// Therefore `callback_owner` must remain alive at least until the @@ -426,6 +493,8 @@ namespace c2pa struct ReleasedBuilder { C2paContextBuilder* builder; std::unique_ptr callback_owner; + /// Keep alive until all native contexts/builders using the signer are freed. + std::unique_ptr signer_callback_owner = nullptr; }; /// @brief Release ownership of the underlying C2paContextBuilder handle and @@ -438,8 +507,8 @@ namespace c2pa /// Callers must keep ReleasedBuilder::callback_owner alive for as /// long as the native context built from ReleasedBuilder::builder /// exists. - /// @return ReleasedBuilder holding both the native builder and (optionally) - /// the owning unique_ptr for the progress callback. Fields are null + /// @return ReleasedBuilder holding the native builder and (optionally) + /// both callback owners. Fields are null /// when moved-from. [[nodiscard]] ReleasedBuilder release() noexcept; @@ -448,6 +517,7 @@ namespace c2pa // so destruction (reverse of declaration) frees context_builder first, while // the callback heap block is still live, then releases pending_callback_. std::unique_ptr pending_callback_; + std::unique_ptr pending_signer_callback_; C2paContextBuilder* context_builder; }; @@ -506,6 +576,10 @@ namespace c2pa Context(C2paContext* ctx, std::unique_ptr callback) noexcept; + /// Adopt a native context together with both callback owners from release(). + Context(C2paContext* ctx, std::unique_ptr callback, + std::unique_ptr signer_callback) noexcept; + /// @brief Request cancellation of any in-progress operation on this context. /// /// @details Safe to call from another thread while this Context remains valid @@ -527,22 +601,13 @@ namespace c2pa /// with a progress callback via ContextBuilder::with_progress_callback(). /// Destroyed after `context`. std::unique_ptr callback_owner_; + std::unique_ptr signer_callback_owner_; }; /// @brief Get the version of the C2PA library. /// @return Version string. std::string C2PA_CPP_API version(); - /// @brief Load C2PA settings from a string in a given format. - /// @param data The configuration data to load. - /// @param format The mimetype of the string. - /// @throws C2paException for errors encountered by the C2PA library. - /// @deprecated Use Context constructors or Context::ContextBuilder instead for better thread safety. - /// Will be removed when c2pa-c moves to c2pa-rs 0.92.0. - [[deprecated("Use Context(json), Context(settings), or Context::ContextBuilder instead. " - "Will be removed when c2pa-c moves to c2pa-rs 0.92.0.")]] - void C2PA_CPP_API load_settings(const std::string& data, const std::string& format); - /// @defgroup StreamWrappers Stream wrappers for C2PA C API /// @brief C++ stream types that adapt stream types to C2paStream. /// @@ -555,7 +620,7 @@ namespace c2pa /// @brief Input stream IStream wrapper for C2paStream. /// @details This class is used to wrap an input stream for use with the C2PA library. - class C2PA_CPP_API CppIStream : public C2paStream + class C2PA_CPP_API CppIStream { public: /// @brief Pointer to the underlying C2paStream. @@ -618,7 +683,7 @@ namespace c2pa /// @brief Output stream OStream wrapper for C2paStream. /// @details This class is used to wrap an output stream for use with the C2PA library. - class C2PA_CPP_API CppOStream : public C2paStream + class C2PA_CPP_API CppOStream { public: /// @brief Pointer to the underlying C2paStream. @@ -678,7 +743,7 @@ namespace c2pa /// @brief IOStream Class wrapper for C2paStream. /// @details This class is used to wrap an input/output stream for use with the C2PA library. - class C2PA_CPP_API CppIOStream : public C2paStream + class C2PA_CPP_API CppIOStream { public: /// @brief Pointer to the underlying C2paStream. @@ -1047,14 +1112,16 @@ namespace c2pa friend class Context::ContextBuilder; private: - C2paSigner *signer; + C2paSigner *signer = nullptr; + std::unique_ptr callback_owner_; /// @brief Transfers ownership of the underlying C2paSigner pointer out /// of this wrapper, without freeing it. /// @details Used by ContextBuilder::with_signer() to pass the raw pointer - /// to c2pa_context_builder_set_signer(), which takes ownership on - /// the Rust side via Box::from_raw. After this call the Signer - /// wrapper holds nullptr and its destructor is a no-op. + /// to c2pa_context_builder_set_signer(), which consumes it on + /// the Rust side. After this call the Signer + /// wrapper holds nullptr. ContextBuilder separately transfers + /// callback_owner_ after the native call succeeds. /// This is not the same as c2pa_signer_free(), which destroys /// the signer. Similar to std::unique_ptr::release(). /// @return Raw C2paSigner pointer, or nullptr if already released. @@ -1081,6 +1148,14 @@ namespace c2pa /// @throws C2paException if signer creation fails. Signer(SignerFunc *callback, C2paSigningAlg alg, const std::string &sign_cert, const std::string &tsa_uri); + /// Zero-copy signing with a capturing callable in stable heap storage. + /// Writes at most signature.size bytes and returns their count, or a negative + /// error. Exceptions become I/O errors; oversized results are rejected. + /// Concurrent signing requires thread-safe captured state. Callback views + /// are borrowed only until the callback returns. + Signer(SignerIntoFunc callback, C2paSigningAlg alg, + const std::string &sign_cert, const std::string &tsa_uri); + /// @brief Create a signer from a Signer pointer and take ownership of that pointer. /// @param c_signer The C2paSigner pointer (must be non-null). Signer(C2paSigner *c_signer) : signer(c_signer) { @@ -1103,13 +1178,16 @@ namespace c2pa /// @brief Move constructor. /// @param other Signer to move from. - Signer(Signer&& other) noexcept : signer(std::exchange(other.signer, nullptr)) { + Signer(Signer&& other) noexcept + : signer(std::exchange(other.signer, nullptr)), + callback_owner_(std::move(other.callback_owner_)) { } Signer& operator=(Signer&& other) noexcept { if (this != &other) { c2pa_free(signer); signer = std::exchange(other.signer, nullptr); + callback_owner_ = std::move(other.callback_owner_); } return *this; } @@ -1330,6 +1408,14 @@ namespace c2pa /// @throws C2paException if the context has no signer or on other errors. std::vector sign(const std::filesystem::path &source_path, const std::filesystem::path &dest_path); + /// Zero-copy siblings of sign(). Returned read-only bytes own their native + /// allocation independently of the Builder. Call to_vector() for an explicit copy. + ByteBuffer sign_to_buffer(const std::string &format, std::istream &source, std::ostream &dest, Signer &signer); + ByteBuffer sign_to_buffer(const std::string &format, std::istream &source, std::iostream &dest, Signer &signer); + ByteBuffer sign_to_buffer(const std::filesystem::path &source_path, const std::filesystem::path &dest_path, Signer &signer); + ByteBuffer sign_to_buffer(const std::string &format, std::istream &source, std::iostream &dest); + ByteBuffer sign_to_buffer(const std::filesystem::path &source_path, const std::filesystem::path &dest_path); + /// @brief Create a Builder from an archived Builder stream. /// @param archive The input stream to read the archive from. /// @return A new Builder instance loaded from the archive. @@ -1376,42 +1462,6 @@ namespace c2pa /// @throws C2paException for errors encountered by the C2PA library. void add_ingredient_from_archive(std::istream &archive); - /// @brief Create a hashed placeholder from the builder. - /// @param reserved_size The size required for a signature from the intended signer (in bytes). - /// @param format The mime format or extension of the asset. - /// @return A vector containing the hashed placeholder bytes. - /// @throws C2paException for errors encountered by the C2PA library. - /// @deprecated Use needs_placeholder() and placeholder(format), with the signer configured - /// on the Builder's Context. Will be removed when c2pa-c moves to c2pa-rs 0.92.0. - [[deprecated("Use needs_placeholder() and placeholder(format), with the signer configured " - "on the Builder's Context. Will be removed when c2pa-c moves to c2pa-rs 0.92.0.")]] - std::vector data_hashed_placeholder(uintptr_t reserved_size, const std::string &format); - - /// @brief Sign a Builder using the specified signer and data hash. - /// @param signer The signer to use for signing. - /// @param data_hash The data hash ranges to sign (must contain hashes unless an asset is provided). - /// @param format The mime format for embedding. Use "c2pa" for an unformatted result. - /// @param asset Optional asset to hash according to the data_hash information. - /// @return A vector containing the signed embeddable data. - /// @throws C2paException for errors encountered by the C2PA library. - /// @deprecated Use set_data_hash_exclusions(), update_hash_from_stream(), then - /// sign_embeddable(format); the signer comes from the Builder's Context. - /// Will be removed when c2pa-c moves to c2pa-rs 0.92.0. - [[deprecated("Use set_data_hash_exclusions(), update_hash_from_stream(), then sign_embeddable(format); " - "the signer comes from the Builder's Context. " - "Will be removed when c2pa-c moves to c2pa-rs 0.92.0.")]] - std::vector sign_data_hashed_embeddable(Signer &signer, const std::string &data_hash, const std::string &format, std::istream *asset = nullptr); - - /// @brief Convert unformatted manifest data to an embeddable format. - /// @param format The format for embedding. - /// @param data Unformatted manifest data from sign_data_hashed_embeddable using "c2pa" format. - /// @return A formatted copy of the data. - /// @deprecated Use sign_embeddable(format), which returns manifest bytes already formatted - /// for embedding. Will be removed when c2pa-c moves to c2pa-rs 0.92.0. - [[deprecated("Use sign_embeddable(format), which returns manifest bytes already formatted " - "for embedding. Will be removed when c2pa-c moves to c2pa-rs 0.92.0.")]] - static std::vector format_embeddable(const std::string &format, std::vector &data); - /// @brief Check if the given format requires a placeholder embedding step. /// @details Returns false for BoxHash-capable formats when prefer_box_hash is enabled in /// the context settings (no placeholder needed — hash covers the full asset). @@ -1432,6 +1482,8 @@ namespace c2pa /// @return Composed placeholder bytes ready to embed into the asset. /// @throws C2paException on error. std::vector placeholder(const std::string &format); + /// Zero-copy sibling of placeholder(); same workflow and errors. + ByteBuffer placeholder_to_buffer(const std::string &format); /// @brief Register the byte ranges where the placeholder was embedded (DataHash workflow). /// @details Call this after embedding the placeholder bytes into the asset and before @@ -1464,6 +1516,8 @@ namespace c2pa /// @return Signed manifest bytes ready to embed into the asset. /// @throws C2paException on error. std::vector sign_embeddable(const std::string &format); + /// Zero-copy sibling of sign_embeddable(); same workflow and errors. + ByteBuffer sign_embeddable_to_buffer(const std::string &format); /// @brief Get a list of mime types that the Builder supports. /// @return Vector of supported MIME type strings. diff --git a/src/c2pa_builder.cpp b/src/c2pa_builder.cpp index 2b8eec7..f56859f 100644 --- a/src/c2pa_builder.cpp +++ b/src/c2pa_builder.cpp @@ -90,11 +90,8 @@ namespace c2pa Builder::Builder(const std::string &manifest_json) : builder(nullptr) { - builder = c2pa_builder_from_json(manifest_json.c_str()); - if (builder == nullptr) - { - throw C2paException(); - } + Context context; + init_from_context(context, manifest_json); } /// @brief Create a Builder from an archive. @@ -221,6 +218,31 @@ namespace c2pa } std::vector Builder::sign(const std::string &format, std::istream &source, std::ostream &dest, Signer &signer) + { + return sign_to_buffer(format, source, dest, signer).to_vector(); + } + + std::vector Builder::sign(const std::string &format, std::istream &source, std::iostream &dest, Signer &signer) + { + return sign_to_buffer(format, source, dest, signer).to_vector(); + } + + std::vector Builder::sign(const std::filesystem::path &source_path, const std::filesystem::path &dest_path, Signer &signer) + { + return sign_to_buffer(source_path, dest_path, signer).to_vector(); + } + + std::vector Builder::sign(const std::string &format, std::istream &source, std::iostream &dest) + { + return sign_to_buffer(format, source, dest).to_vector(); + } + + std::vector Builder::sign(const std::filesystem::path &source_path, const std::filesystem::path &dest_path) + { + return sign_to_buffer(source_path, dest_path).to_vector(); + } + + ByteBuffer Builder::sign_to_buffer(const std::string &format, std::istream &source, std::ostream &dest, Signer &signer) { const std::string normalized_format = detail::normalize_format(format); @@ -232,10 +254,10 @@ namespace c2pa // c2pa_builder_sign() uses streams synchronously and completes before returning auto result = c2pa_builder_sign(builder, normalized_format.c_str(), c_source.c_stream, c_dest.c_stream, signer.c2pa_signer(), &c2pa_manifest_bytes); - return detail::to_byte_vector(c2pa_manifest_bytes, result); + return detail::to_byte_buffer(c2pa_manifest_bytes, result); } - std::vector Builder::sign(const std::string &format, std::istream &source, std::iostream &dest, Signer &signer) + ByteBuffer Builder::sign_to_buffer(const std::string &format, std::istream &source, std::iostream &dest, Signer &signer) { const std::string normalized_format = detail::normalize_format(format); @@ -247,7 +269,7 @@ namespace c2pa // c2pa_builder_sign() uses streams synchronously and completes before returning auto result = c2pa_builder_sign(builder, normalized_format.c_str(), c_source.c_stream, c_dest.c_stream, signer.c2pa_signer(), &c2pa_manifest_bytes); - return detail::to_byte_vector(c2pa_manifest_bytes, result); + return detail::to_byte_buffer(c2pa_manifest_bytes, result); } /// @brief Sign a file and write the signed data to an output file. @@ -256,7 +278,7 @@ namespace c2pa /// @param signer A signer object to use when signing. /// @return A vector containing the signed manifest bytes. /// @throws C2pa::C2paException for errors encountered by the C2PA library. - std::vector Builder::sign(const std::filesystem::path &source_path, const std::filesystem::path &dest_path, Signer &signer) + ByteBuffer Builder::sign_to_buffer(const std::filesystem::path &source_path, const std::filesystem::path &dest_path, Signer &signer) { const std::string format = detail::resolve_format(detail::extract_file_extension(dest_path)); @@ -277,11 +299,11 @@ namespace c2pa { throw std::runtime_error("Failed to open destination file: " + dest_path.string()); } - auto result = sign(format, *source, dest, signer); + auto result = sign_to_buffer(format, *source, dest, signer); return result; } - std::vector Builder::sign(const std::string &format, std::istream &source, std::iostream &dest) + ByteBuffer Builder::sign_to_buffer(const std::string &format, std::istream &source, std::iostream &dest) { const std::string normalized_format = detail::normalize_format(format); @@ -290,10 +312,10 @@ namespace c2pa const unsigned char *c2pa_manifest_bytes = nullptr; auto result = c2pa_builder_sign_context(builder, normalized_format.c_str(), c_source.c_stream, c_dest.c_stream, &c2pa_manifest_bytes); - return detail::to_byte_vector(c2pa_manifest_bytes, result); + return detail::to_byte_buffer(c2pa_manifest_bytes, result); } - std::vector Builder::sign(const std::filesystem::path &source_path, const std::filesystem::path &dest_path) + ByteBuffer Builder::sign_to_buffer(const std::filesystem::path &source_path, const std::filesystem::path &dest_path) { const std::string format = detail::resolve_format(detail::extract_file_extension(dest_path)); @@ -313,7 +335,7 @@ namespace c2pa { throw std::runtime_error("Failed to open destination file: " + dest_path.string()); } - auto result = sign(format, *source, dest); + auto result = sign_to_buffer(format, *source, dest); return result; } @@ -394,33 +416,6 @@ namespace c2pa } } - std::vector Builder::data_hashed_placeholder(uintptr_t reserve_size, const std::string &format) - { - const std::string normalized_format = detail::normalize_format(format); - - const unsigned char *c2pa_manifest_bytes = nullptr; - auto result = c2pa_builder_data_hashed_placeholder(builder, reserve_size, normalized_format.c_str(), &c2pa_manifest_bytes); - return detail::to_byte_vector(c2pa_manifest_bytes, result); - } - - std::vector Builder::sign_data_hashed_embeddable(Signer &signer, const std::string &data_hash, const std::string &format, std::istream *asset) - { - const std::string normalized_format = detail::normalize_format(format); - - int64_t result; - const unsigned char *c2pa_manifest_bytes = nullptr; - if (asset) - { - CppIStream c_asset(*asset); - result = c2pa_builder_sign_data_hashed_embeddable(builder, signer.c2pa_signer(), data_hash.c_str(), normalized_format.c_str(), c_asset.c_stream, &c2pa_manifest_bytes); - } - else - { - result = c2pa_builder_sign_data_hashed_embeddable(builder, signer.c2pa_signer(), data_hash.c_str(), normalized_format.c_str(), nullptr, &c2pa_manifest_bytes); - } - return detail::to_byte_vector(c2pa_manifest_bytes, result); - } - bool Builder::needs_placeholder(const std::string &format) { const std::string resolved_format = detail::resolve_format(format); @@ -434,13 +429,18 @@ namespace c2pa } std::vector Builder::placeholder(const std::string &format) + { + return placeholder_to_buffer(format).to_vector(); + } + + ByteBuffer Builder::placeholder_to_buffer(const std::string &format) { // The format selects the hash assertion written into the builder. const std::string normalized_format = detail::normalize_format(format); const unsigned char *c2pa_manifest_bytes = nullptr; auto result = c2pa_builder_placeholder(builder, normalized_format.c_str(), &c2pa_manifest_bytes); - return detail::to_byte_vector(c2pa_manifest_bytes, result); + return detail::to_byte_buffer(c2pa_manifest_bytes, result); } void Builder::set_data_hash_exclusions(const std::vector> &exclusions) @@ -475,20 +475,16 @@ namespace c2pa std::vector Builder::sign_embeddable(const std::string &format) { - const std::string normalized_format = detail::normalize_format(format); - - const unsigned char *c2pa_manifest_bytes = nullptr; - auto result = c2pa_builder_sign_embeddable(builder, normalized_format.c_str(), &c2pa_manifest_bytes); - return detail::to_byte_vector(c2pa_manifest_bytes, result); + return sign_embeddable_to_buffer(format).to_vector(); } - std::vector Builder::format_embeddable(const std::string &format, std::vector &data) + ByteBuffer Builder::sign_embeddable_to_buffer(const std::string &format) { const std::string normalized_format = detail::normalize_format(format); const unsigned char *c2pa_manifest_bytes = nullptr; - auto result = c2pa_format_embeddable(normalized_format.c_str(), data.data(), data.size(), &c2pa_manifest_bytes); - return detail::to_byte_vector(c2pa_manifest_bytes, result); + auto result = c2pa_builder_sign_embeddable(builder, normalized_format.c_str(), &c2pa_manifest_bytes); + return detail::to_byte_buffer(c2pa_manifest_bytes, result); } std::vector Builder::supported_mime_types() { diff --git a/src/c2pa_context.cpp b/src/c2pa_context.cpp index c763520..e3d8a2d 100644 --- a/src/c2pa_context.cpp +++ b/src/c2pa_context.cpp @@ -40,7 +40,8 @@ namespace c2pa Context::Context(Context&& other) noexcept : context(std::exchange(other.context, nullptr)), - callback_owner_(std::move(other.callback_owner_)) { + callback_owner_(std::move(other.callback_owner_)), + signer_callback_owner_(std::move(other.signer_callback_owner_)) { } Context& Context::operator=(Context&& other) noexcept { @@ -52,6 +53,7 @@ namespace c2pa } context = std::exchange(other.context, nullptr); callback_owner_ = std::move(other.callback_owner_); + signer_callback_owner_ = std::move(other.signer_callback_owner_); } return *this; } @@ -68,6 +70,12 @@ namespace c2pa : context(ctx), callback_owner_(std::move(callback)) { } + Context::Context(C2paContext* ctx, std::unique_ptr callback, + std::unique_ptr signer_callback) noexcept + : context(ctx), callback_owner_(std::move(callback)), + signer_callback_owner_(std::move(signer_callback)) { + } + void Context::cancel() noexcept { if (context) { c2pa_context_cancel(context); @@ -102,6 +110,7 @@ namespace c2pa Context::ContextBuilder::ContextBuilder(ContextBuilder&& other) noexcept : pending_callback_(std::move(other.pending_callback_)), + pending_signer_callback_(std::move(other.pending_signer_callback_)), context_builder(std::exchange(other.context_builder, nullptr)) { } @@ -115,6 +124,7 @@ namespace c2pa } context_builder = std::exchange(other.context_builder, nullptr); pending_callback_ = std::move(other.pending_callback_); + pending_signer_callback_ = std::move(other.pending_signer_callback_); } return *this; } @@ -187,6 +197,7 @@ namespace c2pa if (c2pa_context_builder_set_signer(context_builder, raw) != 0) { throw C2paException(); } + pending_signer_callback_ = std::move(signer.callback_owner_); return *this; } @@ -267,6 +278,7 @@ namespace c2pa return ReleasedBuilder{ std::exchange(context_builder, nullptr), std::move(pending_callback_), + std::move(pending_signer_callback_), }; } @@ -282,6 +294,6 @@ namespace c2pa throw C2paException("Failed to build context"); } - return Context(ctx, std::move(pending_callback_)); + return Context(ctx, std::move(pending_callback_), std::move(pending_signer_callback_)); } } // namespace c2pa diff --git a/src/c2pa_core.cpp b/src/c2pa_core.cpp index a3dadca..ab67b3a 100644 --- a/src/c2pa_core.cpp +++ b/src/c2pa_core.cpp @@ -49,18 +49,4 @@ namespace c2pa return detail::c_string_to_string(c2pa_version()); } - /// Loads C2PA settings from a std::string in a given format. - /// @param data the std::string to load. - /// @param format the mime format of the string. - /// @throws a C2pa::C2paException for errors encountered by the C2PA library. - [[deprecated("Use Context(json), Context(settings), or Context::ContextBuilder instead. Will be removed when c2pa-c moves to c2pa-rs 0.92.0.")]] - void load_settings(const std::string &data, const std::string &format) - { - auto result = c2pa_load_settings(data.c_str(), format.c_str()); - if (result != 0) - { - throw c2pa::C2paException(); - } - } - } // namespace c2pa diff --git a/src/c2pa_internal.hpp b/src/c2pa_internal.hpp index ee50d32..71bf7a2 100644 --- a/src/c2pa_internal.hpp +++ b/src/c2pa_internal.hpp @@ -27,6 +27,7 @@ #include #include #include +#include #include "c2pa.h" #include "c2pa.hpp" @@ -295,17 +296,17 @@ inline std::string c_string_to_string(T* c_result) { return str; } -/// @brief Convert C byte array result to C++ vector +/// @brief Adopt a C byte-array result without copying. /// @param data Raw byte array from C API /// @param size Size of the byte array (result from C API call) -/// @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. +/// @return Owning buffer (throws on invalid pointer/size combinations). +/// @details Validation failures release any returned allocation. /// 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 && size > 0)) { +inline ByteBuffer to_byte_buffer(const unsigned char* data, int64_t size) { + if (size < 0 || (data == nullptr && size > 0) || + static_cast(size) > std::numeric_limits::max()) { c2pa_free(data); // May be null or allocated, c2pa_free handles both throw C2paException(); } @@ -314,9 +315,32 @@ inline std::vector to_byte_vector(const unsigned char* data, int6 return {}; } - auto result = std::vector(data, data + size); - c2pa_free(data); - return result; + return ByteBuffer(data, static_cast(size)); +} + +inline std::vector to_byte_vector(const unsigned char* data, int64_t size) { + return to_byte_buffer(data, size).to_vector(); +} + +inline intptr_t signer_into_passthrough(const void* context, const unsigned char* data, + uintptr_t len, unsigned char* signature, + uintptr_t capacity) noexcept { + if (!context || !data || !signature) { + return stream_error_return(StreamError::InvalidArgument); + } + try { + const auto& callback = *static_cast(context); + const auto written = callback({data, len}, {signature, capacity}); + if (written < 0) { + return stream_error_return(StreamError::IoError); + } + if (static_cast(written) > capacity) { + return stream_error_return(StreamError::NoBufferSpace); + } + return written; + } catch (...) { + return stream_error_return(StreamError::IoError); + } } } // namespace detail diff --git a/src/c2pa_reader.cpp b/src/c2pa_reader.cpp index 3faff77..e10b73c 100644 --- a/src/c2pa_reader.cpp +++ b/src/c2pa_reader.cpp @@ -220,34 +220,17 @@ namespace c2pa } Reader::Reader(const std::string &format, std::istream &stream) + : c2pa_reader(nullptr) { - const std::string normalized_format = detail::normalize_format(format); - - cpp_stream = std::make_unique(stream); - c2pa_reader = c2pa_reader_from_stream(normalized_format.c_str(), cpp_stream->c_stream); - if (c2pa_reader == nullptr) - { - throw C2paException(); - } + Context context; + init_from_context(context, format, stream); } Reader::Reader(const std::filesystem::path &source_path) + : c2pa_reader(nullptr) { - // 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()) { - throw std::system_error(errno, std::system_category(), "Failed to open file: " + source_path.string()); - } - - std::string extension = detail::normalize_format(detail::extract_file_extension(source_path)); - - // CppIStream stores reference to owned_stream, which lives as long as Reader - cpp_stream = std::make_unique(*owned_stream); - c2pa_reader = c2pa_reader_from_stream(extension.c_str(), cpp_stream->c_stream); - if (c2pa_reader == nullptr) - { - throw C2paException(); - } + Context context; + init_from_context(context, source_path); } Reader::~Reader() diff --git a/src/c2pa_signer.cpp b/src/c2pa_signer.cpp index 9e6954e..5e1ece0 100644 --- a/src/c2pa_signer.cpp +++ b/src/c2pa_signer.cpp @@ -77,6 +77,20 @@ namespace c2pa } } + Signer::Signer(SignerIntoFunc callback, C2paSigningAlg alg, + const std::string &sign_cert, const std::string &tsa_uri) + : callback_owner_(std::make_unique(std::move(callback))) + { + if (!*callback_owner_) { + throw C2paException("Signer callback must not be empty"); + } + signer = c2pa_signer_create(callback_owner_.get(), detail::signer_into_passthrough, + alg, sign_cert.c_str(), validate_tsa_uri(tsa_uri)); + if (!signer) { + 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) }; diff --git a/tests/builder.test.cpp b/tests/builder.test.cpp index e5bd4c3..6339520 100644 --- a/tests/builder.test.cpp +++ b/tests/builder.test.cpp @@ -197,7 +197,10 @@ TEST_F(BuilderTest, wraps_c_ffi_builder) { fs::path manifest_path = current_dir / "../tests/fixtures/training.json"; auto manifest = c2pa_test::read_text_file(manifest_path); - C2paBuilder* ffi_builder = c2pa_builder_from_json(manifest.c_str()); + c2pa::Context context; + C2paBuilder* ffi_builder = c2pa_builder_from_context(context.c_context()); + ASSERT_NE(ffi_builder, nullptr); + ffi_builder = c2pa_builder_with_definition(ffi_builder, manifest.c_str()); ASSERT_NE(ffi_builder, nullptr); auto builder = c2pa::Builder(ffi_builder); @@ -880,9 +883,9 @@ TEST_F(BuilderTest, SignImageFileOnly) ASSERT_TRUE(std::filesystem::exists(output_path)); }; -TEST_F(BuilderTest, SignImageFileNoThumbnailAutoGenThreadLocalSettings) +TEST_F(BuilderTest, SignImageFileNoThumbnailAutoGenContextInThread) { - // Run in separate thread for complete test isolation (thread-local settings won't leak) + // Exercise context-scoped settings on another thread. fs::path temp_output = get_temp_path("training_image_only_thread_local.jpg"); std::thread test_thread([temp_output]() { fs::path current_dir = fs::path(__FILE__).parent_path(); @@ -898,13 +901,12 @@ TEST_F(BuilderTest, SignImageFileNoThumbnailAutoGenThreadLocalSettings) auto certs = c2pa_test::read_text_file(certs_path); auto p_key = c2pa_test::read_text_file(current_dir / "../tests/fixtures/es256_private.key"); - // set settings to not generate thumbnails (thread-local) - c2pa::load_settings("{\"builder\": { \"thumbnail\": {\"enabled\": false}}}", "json"); + auto context = c2pa::Context("{\"builder\": { \"thumbnail\": {\"enabled\": false}}}"); // create a signer c2pa::Signer signer = c2pa::Signer("Es256", certs, p_key, "http://timestamp.digicert.com"); - auto builder = c2pa::Builder(manifest); + auto builder = c2pa::Builder(context, manifest); // the Builder returns manifest bytes std::vector manifest_data; ASSERT_NO_THROW(manifest_data = builder.sign(signed_image_path, output_path, signer)); @@ -914,7 +916,6 @@ TEST_F(BuilderTest, SignImageFileNoThumbnailAutoGenThreadLocalSettings) auto reader = c2pa::Reader(output_path); ASSERT_NO_THROW(reader.json()); - // No need to reset settings - thread-local settings destroyed with thread }); // Wait for thread to complete @@ -1751,172 +1752,56 @@ TEST_F(BuilderTest, SignStreamCloudUrl) TEST_F(BuilderTest, SignDataHashedEmbedded) { - fs::path current_dir = fs::path(__FILE__).parent_path(); - - // Construct the paths relative to the current directory - fs::path manifest_path = current_dir / "../tests/fixtures/training.json"; - fs::path certs_path = current_dir / "../tests/fixtures/es256_certs.pem"; - // fs::path signed_image_path = current_dir / "../tests/fixtures/A.jpg"; - - auto manifest = c2pa_test::read_text_file(manifest_path); - auto certs = c2pa_test::read_text_file(certs_path); - auto p_key = c2pa_test::read_text_file(current_dir / "../tests/fixtures/es256_private.key"); - - // create a signer - c2pa::Signer signer = c2pa::Signer("Es256", certs, p_key, "http://timestamp.digicert.com"); - - auto builder = c2pa::Builder(manifest); - - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - - std::string data_hash = R"({ - "exclusions": [ - { - "start": 20, - "length": 45884 - } - ], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "gWZNEOMHQNiULfA/tO5HD2awOwYMA3tnfUPApIr9csk=", - "pad": " " - })"; + auto settings = c2pa_test::create_test_settings_with_signer(); + auto context = c2pa::Context(settings); + auto builder = c2pa::Builder(context, c2pa_test::prehashed_training_manifest()); std::vector manifest_data; - ASSERT_NO_THROW(manifest_data = builder.sign_data_hashed_embeddable(signer, data_hash, "image/jpeg")); + ASSERT_NO_THROW(manifest_data = builder.sign_embeddable("image/jpeg")); + EXPECT_FALSE(manifest_data.empty()); } TEST_F(BuilderTest, SignDataHashedEmbeddedUsingContext) { - fs::path current_dir = fs::path(__FILE__).parent_path(); - - // Construct the paths relative to the current directory - fs::path manifest_path = current_dir / "../tests/fixtures/training.json"; - fs::path certs_path = current_dir / "../tests/fixtures/es256_certs.pem"; - // fs::path signed_image_path = current_dir / "../tests/fixtures/A.jpg"; - - auto manifest = c2pa_test::read_text_file(manifest_path); - auto certs = c2pa_test::read_text_file(certs_path); - auto p_key = c2pa_test::read_text_file(current_dir / "../tests/fixtures/es256_private.key"); - - // create a signer - c2pa::Signer signer = c2pa::Signer("Es256", certs, p_key, "http://timestamp.digicert.com"); - - // Create a Context and pass it to the Builder - auto context = c2pa::Context(); - auto builder = c2pa::Builder(context, manifest); - - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - - std::string data_hash = R"({ - "exclusions": [ - { - "start": 20, - "length": 45884 - } - ], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "gWZNEOMHQNiULfA/tO5HD2awOwYMA3tnfUPApIr9csk=", - "pad": " " - })"; + auto context = c2pa::Context::ContextBuilder() + .with_signer(c2pa_test::create_test_signer()) + .create_context(); + auto builder = c2pa::Builder(context, c2pa_test::prehashed_training_manifest()); std::vector manifest_data; - ASSERT_NO_THROW(manifest_data = builder.sign_data_hashed_embeddable(signer, data_hash, "image/jpeg")); + ASSERT_NO_THROW(manifest_data = builder.sign_embeddable("image/jpeg")); + EXPECT_FALSE(manifest_data.empty()); } TEST_F(BuilderTest, SignDataHashedEmbeddedWithAsset) { - fs::path current_dir = fs::path(__FILE__).parent_path(); - - // Construct the paths relative to the current directory - fs::path manifest_path = current_dir / "../tests/fixtures/training.json"; - fs::path certs_path = current_dir / "../tests/fixtures/es256_certs.pem"; - fs::path image_path = current_dir / "../tests/fixtures/A.jpg"; - - auto manifest = c2pa_test::read_text_file(manifest_path); - auto certs = c2pa_test::read_text_file(certs_path); - auto p_key = c2pa_test::read_text_file(current_dir / "../tests/fixtures/es256_private.key"); - - // create a signer - c2pa::Signer signer = c2pa::Signer("Es256", certs, p_key, "http://timestamp.digicert.com"); - - auto builder = c2pa::Builder(manifest); - - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - - std::string data_hash = R"({ - "exclusions": [ - { - "start": 20, - "length": 45884 - } - ], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "", - "pad": " " - })"; - - std::ifstream asset(image_path, std::ios::binary); - if (!asset) - { - FAIL() << "Failed to open file: " << image_path << std::endl; - } - + auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + auto settings = c2pa_test::create_test_settings_with_signer(); + auto context = c2pa::Context(settings); + auto builder = c2pa::Builder(context, manifest); + auto placeholder = builder.placeholder("image/jpeg"); + builder.set_data_hash_exclusions({{20, placeholder.size()}}); + std::ifstream asset(c2pa_test::get_fixture_path("A.jpg"), std::ios::binary); + ASSERT_TRUE(asset.is_open()); + builder.update_hash_from_stream("image/jpeg", asset); std::vector manifest_data; - ASSERT_NO_THROW(manifest_data = builder.sign_data_hashed_embeddable(signer, data_hash, "application/c2pa", &asset)); - - auto embeddable_data = c2pa::Builder::format_embeddable("image/jpeg", manifest_data); - - ASSERT_TRUE(embeddable_data.size() > manifest_data.size()); + ASSERT_NO_THROW(manifest_data = builder.sign_embeddable("image/jpeg")); + EXPECT_EQ(manifest_data.size(), placeholder.size()); } TEST_F(BuilderTest, SignDataHashedEmbeddedWithAssetUsingContext) { - fs::path current_dir = fs::path(__FILE__).parent_path(); - - // Construct the paths relative to the current directory - fs::path manifest_path = current_dir / "../tests/fixtures/training.json"; - fs::path certs_path = current_dir / "../tests/fixtures/es256_certs.pem"; - fs::path image_path = current_dir / "../tests/fixtures/A.jpg"; - - auto manifest = c2pa_test::read_text_file(manifest_path); - auto certs = c2pa_test::read_text_file(certs_path); - auto p_key = c2pa_test::read_text_file(current_dir / "../tests/fixtures/es256_private.key"); - - // create a signer - c2pa::Signer signer = c2pa::Signer("Es256", certs, p_key, "http://timestamp.digicert.com"); - - // Create a Context and pass it to the Builder - auto context = c2pa::Context(); + auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + auto context = c2pa::Context::ContextBuilder() + .with_signer(c2pa_test::create_test_signer()) + .create_context(); auto builder = c2pa::Builder(context, manifest); - - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - - std::string data_hash = R"({ - "exclusions": [ - { - "start": 20, - "length": 45884 - } - ], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "", - "pad": " " - })"; - - std::ifstream asset(image_path, std::ios::binary); - if (!asset) - { - FAIL() << "Failed to open file: " << image_path << std::endl; - } - + auto placeholder = builder.placeholder("image/jpeg"); + builder.set_data_hash_exclusions({{20, placeholder.size()}}); + std::ifstream asset(c2pa_test::get_fixture_path("A.jpg"), std::ios::binary); + ASSERT_TRUE(asset.is_open()); + builder.update_hash_from_stream("image/jpeg", asset); std::vector manifest_data; - ASSERT_NO_THROW(manifest_data = builder.sign_data_hashed_embeddable(signer, data_hash, "application/c2pa", &asset)); - - auto embeddable_data = c2pa::Builder::format_embeddable("image/jpeg", manifest_data); - - ASSERT_TRUE(embeddable_data.size() > manifest_data.size()); + ASSERT_NO_THROW(manifest_data = builder.sign_embeddable("image/jpeg")); + EXPECT_EQ(manifest_data.size(), placeholder.size()); } TEST_F(BuilderTest, SignWithInvalidStream) @@ -2302,9 +2187,9 @@ TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePath) ASSERT_NO_THROW(reader.json()); } -TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePathPlacedActionThreadLocalSettings) +TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePathPlacedActionContextInThread) { - // Run in separate thread for complete test isolation (thread-local settings won't leak) + // Exercise context-scoped settings on another thread. std::thread test_thread([this]() { fs::path current_dir = fs::path(__FILE__).parent_path(); @@ -2325,8 +2210,7 @@ TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePathPlacedActionThreadLocalSe } fs::create_directories(temp_dir); - // set settings to not auto-add a placed action (thread-local) - c2pa::load_settings("{\"builder\": { \"actions\": {\"auto_placed_action\": {\"enabled\": false}}}}", "json"); + auto context = c2pa::Context("{\"builder\": { \"actions\": {\"auto_placed_action\": {\"enabled\": false}}}}"); // 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"; @@ -2371,7 +2255,7 @@ TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePathPlacedActionThreadLocalSe }; // Now we can create a Builder with the manifest - auto builder = c2pa::Builder(manifest_json.dump()); + auto builder = c2pa::Builder(context, manifest_json.dump()); // 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. @@ -2402,7 +2286,6 @@ TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePathPlacedActionThreadLocalSe } } - // No need to reset settings - thread-local settings destroyed with thread }); // Wait for thread to complete @@ -3530,7 +3413,8 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchiveLegacy) { const std::string& base_manifest_json) -> c2pa::Builder { // Create a Reader from the archive - c2pa::Reader reader("application/c2pa", archive_stream); + c2pa::Context context(R"({"verify": {"verify_after_reading": false}})"); + c2pa::Reader reader(context, "application/c2pa", archive_stream); auto json_result = reader.json(); // Parse the archive JSON to extract ingredient information @@ -3595,9 +3479,6 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchiveLegacy) { builder.to_archive(archive_stream); }); - // Skip verification since archives have placeholder signatures - c2pa::load_settings(R"({"verify": {"verify_after_reading": false}})", "json"); - // Use the helper function to create a builder with ingredients from the archive archive_stream.seekg(0); auto merged_builder = create_builder_with_ingredients_from_archive(archive_stream, manifest); @@ -3623,8 +3504,6 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchiveLegacy) { auto merged_ingredients = merged_parsed["manifests"][merged_active]["ingredients"]; EXPECT_EQ(merged_ingredients.size(), 3) << "Merged builder should have all 3 ingredients"; - // Reset settings - c2pa::load_settings(R"({"verify": {"verify_after_reading": true}})", "json"); } TEST_F(BuilderTest, ExtractIngredientsFromArchiveToBuilderLegacy) { @@ -3641,13 +3520,14 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchiveToBuilderLegacy) { builder.to_archive(builder_archive); builder_archive.seekg(0); - c2pa::Reader builder_reader("application/c2pa", builder_archive); + c2pa::Context context(R"({"verify": {"verify_after_reading": false}})"); + c2pa::Reader builder_reader(context, "application/c2pa", builder_archive); auto builder_parsed = json::parse(builder_reader.json()); std::string builder_active = builder_parsed["active_manifest"]; json current_ingredients = builder_parsed["manifests"][builder_active].value("ingredients", json::array()); // Read the incoming archive to get its ingredients - c2pa::Reader archive_reader("application/c2pa", archive_stream); + c2pa::Reader archive_reader(context, "application/c2pa", archive_stream); auto archive_parsed = json::parse(archive_reader.json()); std::string archive_active = archive_parsed["active_manifest"]; auto new_ingredients = archive_parsed["manifests"][archive_active]["ingredients"]; @@ -3773,9 +3653,6 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchiveToBuilderLegacy) { builder2.to_archive(archive2_stream); }); - // Skip verification since archives have placeholder signatures - c2pa::load_settings(R"({"verify": {"verify_after_reading": false}})", "json"); - auto merged_builder = c2pa::Builder(manifest); // First call: add ingredients from archive 1 (A.jpg, C.jpg) @@ -3807,8 +3684,6 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchiveToBuilderLegacy) { auto merged_ingredients = merged_parsed["manifests"][merged_active]["ingredients"]; EXPECT_EQ(merged_ingredients.size(), 3) << "Merged builder should have all 3 ingredients from both archives"; - // Reset settings - c2pa::load_settings(R"({"verify": {"verify_after_reading": true}})", "json"); } TEST_F(BuilderTest, ExtractIngredientsFromArchivesLegacy) { @@ -3816,6 +3691,7 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchivesLegacy) { std::vector>& archive_streams, const std::string& base_manifest_json) -> c2pa::Builder { + c2pa::Context context(R"({"verify": {"verify_after_reading": false}})"); // Collect ingredients and their associated readers from all archives. // Each entry pairs an ingredient JSON (possibly with remapped identifiers) // with a pointer to the Reader it came from, so we can copy resources later. @@ -3857,7 +3733,7 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchivesLegacy) { // Process each archive: extract ingredients, remap conflicting IDs for (auto& stream_ref : archive_streams) { - auto reader = std::make_unique("application/c2pa", stream_ref.get()); + auto reader = std::make_unique(context, "application/c2pa", stream_ref.get()); auto parsed = json::parse(reader->json()); std::string active = parsed["active_manifest"]; auto ingredients = parsed["manifests"][active]["ingredients"]; @@ -3944,9 +3820,6 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchivesLegacy) { builder2.to_archive(archive2_stream); }); - // Skip verification since archives have placeholder signatures - c2pa::load_settings(R"({"verify": {"verify_after_reading": false}})", "json"); - // Use the helper to create a builder from both archives at once archive1_stream.seekg(0); archive2_stream.seekg(0); @@ -3974,8 +3847,6 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchivesLegacy) { auto merged_ingredients = merged_parsed["manifests"][merged_active]["ingredients"]; EXPECT_EQ(merged_ingredients.size(), 3) << "Merged builder should have all 3 ingredients from both archives"; - // Reset settings - c2pa::load_settings(R"({"verify": {"verify_after_reading": true}})", "json"); } TEST_F(BuilderTest, AddMultipleActions) @@ -5932,7 +5803,7 @@ TEST_F(BuilderTest, SignStreamWithCallbackSignerInContext) { throw std::runtime_error("c2pa_ed25519_sign failed"); } std::vector signature(sig_ptr, sig_ptr + 64); - c2pa_signature_free(sig_ptr); + c2pa_free(sig_ptr); return signature; }; diff --git a/tests/c-app-test/file_stream.h b/tests/c-app-test/file_stream.h index 8e5daed..009946b 100644 --- a/tests/c-app-test/file_stream.h +++ b/tests/c-app-test/file_stream.h @@ -95,25 +95,32 @@ void release_stream(C2paStream *stream) c2pa_release_stream(stream); } -C2paStream *open_file_stream(const char *path, const char *mode) +typedef struct FileStream { + FILE *file; + C2paStream *stream; +} FileStream; + +FileStream open_file_stream(const char *path, const char *mode) { - FILE *file = fopen(path, mode); - if (file != NULL) - { - // printf("file open = %0lx\n", (unsigned long)file); - return create_file_stream(file); + FileStream result = {NULL, NULL}; + result.file = fopen(path, mode); + if (result.file != NULL) { + result.stream = create_file_stream(result.file); + if (result.stream == NULL) { + fclose(result.file); + result.file = NULL; + } } - return NULL; + return result; } -int close_file_stream(C2paStream *stream) +int close_file_stream(FileStream stream) { - if (stream == NULL) { + if (stream.stream == NULL) { return -1; } - FILE *file = (FILE *)stream->context; - int result = fclose(file); - c2pa_release_stream(stream); + c2pa_release_stream(stream.stream); + int result = fclose(stream.file); return result; } diff --git a/tests/c-app-test/test.c b/tests/c-app-test/test.c index e9edbd5..c3b08be 100644 --- a/tests/c-app-test/test.c +++ b/tests/c-app-test/test.c @@ -23,11 +23,11 @@ int main(void) char *version = c2pa_version(); assert_contains("version", version, "c2pa-c-ffi/0."); - C2paStream *input_stream = open_file_stream("tests/fixtures/C.jpg", "rb"); - assert_not_null("open_file_stream", input_stream); + FileStream input_stream = open_file_stream("tests/fixtures/C.jpg", "rb"); + assert_not_null("open_file_stream", input_stream.stream); - C2paReader *reader = c2pa_reader_from_stream("image/jpeg", input_stream); - assert_not_null("c2pa_reader_from_stream", reader); + C2paReader *reader = c2pa_reader_with_stream(c2pa_reader_new(), "image/jpeg", input_stream.stream); + assert_not_null("c2pa_reader_with_stream", reader); close_file_stream(input_stream); @@ -44,11 +44,12 @@ int main(void) } // Open a file to write the thumbnail into - C2paStream *thumb_stream = open_file_stream("build/thumb_c.jpg", "wb"); - assert_not_null("open_file_stream thumbnail", thumb_stream); + FileStream thumb_stream = open_file_stream("build/thumb_c.jpg", "wb"); + assert_not_null("open_file_stream thumbnail", thumb_stream.stream); // write the thumbnail resource to the stream - int res = c2pa_reader_resource_to_stream(reader, uri, thumb_stream); + int res = c2pa_reader_resource_to_stream(reader, uri, thumb_stream.stream); + close_file_stream(thumb_stream); free(uri); if (json != NULL) c2pa_free(json); @@ -62,34 +63,41 @@ int main(void) char *manifest = load_file("tests/fixtures/training.json"); - C2paBuilder *builder = c2pa_builder_from_json(manifest); - assert_not_null("c2pa_builder_from_json", builder); + C2paContext *context = c2pa_context_new(); + assert_not_null("c2pa_context_new", context); + C2paBuilder *builder = c2pa_builder_with_definition(c2pa_builder_from_context(context), manifest); + assert_not_null("c2pa_builder_with_definition", builder); - C2paStream *archive = open_file_stream("build/archive.zip", "wb"); - int arch_result = c2pa_builder_to_archive(builder, archive); + FileStream archive = open_file_stream("build/archive.zip", "wb"); + assert_not_null("open_file_stream archive", archive.stream); + int arch_result = c2pa_builder_to_archive(builder, archive.stream); assert_int("c2pa_builder_to_archive", arch_result); close_file_stream(archive); - C2paStream *archive2 = open_file_stream("build/archive.zip", "rb"); - C2paBuilder *builder2 = c2pa_builder_from_archive(archive2); - assert_not_null("c2pa_builder_from_archive", builder2); + FileStream archive2 = open_file_stream("build/archive.zip", "rb"); + assert_not_null("open_file_stream archive2", archive2.stream); + C2paBuilder *builder2 = c2pa_builder_with_archive(c2pa_builder_from_context(context), archive2.stream); + assert_not_null("c2pa_builder_with_archive", builder2); + c2pa_free(context); close_file_stream(archive2); C2paSigner *signer = c2pa_signer_create((const void *)"testing context", &signer_callback, Es256, certs, "http://timestamp.digicert.com"); assert_not_null("c2pa_signer_create", signer); - C2paStream *source = open_file_stream("tests/fixtures/C.jpg", "rb"); + FileStream source = open_file_stream("tests/fixtures/C.jpg", "rb"); + assert_not_null("open_file_stream source", source.stream); remove("build/earth4.jpg"); // stream needs to be w+b because we'll write, rewind, read - C2paStream *dest = open_file_stream("build/earth4.jpg", "w+b"); + FileStream dest = open_file_stream("build/earth4.jpg", "w+b"); + assert_not_null("open_file_stream dest", dest.stream); 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); + int64_t result2 = c2pa_builder_sign(builder2, "image/jpeg", source.stream, dest.stream, signer, &manifest_bytes); assert_int("c2pa_builder_sign", result2); const unsigned char *formatted_bytes = NULL; - int64_t result3 = c2pa_format_embeddable("image/jpeg", manifest_bytes, result2, (const unsigned char **)&formatted_bytes); - assert_int("c2pa_format_embeddable", result3); + int64_t result3 = c2pa_builder_compose_manifest(builder2, "image/jpeg", manifest_bytes, result2, &formatted_bytes); + assert_int("c2pa_builder_compose_manifest", result3); if (manifest_bytes != NULL) c2pa_free((void *)manifest_bytes); if (formatted_bytes != NULL) diff --git a/tests/embeddable.test.cpp b/tests/embeddable.test.cpp index 3205b08..ac0e6ba 100644 --- a/tests/embeddable.test.cpp +++ b/tests/embeddable.test.cpp @@ -4,7 +4,6 @@ // 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 @@ -25,12 +24,10 @@ namespace fs = std::filesystem; using nlohmann::json; -// Test fixture for embeddable tests with automatic cleanup class EmbeddableTest : public ::testing::Test { protected: std::vector temp_files; - // Get path for temp embeddable test files in build directory fs::path get_temp_path(const std::string& name) { fs::path current_dir = fs::path(__FILE__).parent_path(); fs::path build_dir = current_dir.parent_path() / "build"; @@ -51,464 +48,138 @@ class EmbeddableTest : public ::testing::Test { temp_files.clear(); } - // Helper: Creates a Builder with a test manifest. c2pa::Builder make_builder() { auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - return c2pa::Builder(manifest); + auto context = c2pa::Context::ContextBuilder() + .with_signer(c2pa_test::create_test_signer()) + .create_context(); + return c2pa::Builder(context, manifest); } + }; -// e2e workflow with A.jpg (has no existing C2PA) TEST_F(EmbeddableTest, FullWorkflowWithAJpg) { - // This test demonstrates the complete data-hashed embeddable workflow: - // 1. Create placeholder - // 2. Sign with auto-calculated hash - // 3. Format for embedding - // 4. Verify sizes match (critical invariant for in-place patching) - // - // Note: We don't actually embed and read back because manual byte-level - // embedding requires format-specific knowledge of JPEG structure. The SDK's - // normal sign() method handles this internally. This test verifies the - // embeddable APIs work correctly. - - auto manifest_json = 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")); - auto source_asset = c2pa_test::get_fixture_path("A.jpg"); - - // Create signing infra - auto context = c2pa::Context(); - auto builder = c2pa::Builder(context, manifest_json); - c2pa::Signer signer("Es256", certs, private_key, "http://timestamp.digicert.com"); - - // 1: Get placeholder manifest - // The placeholder is the exact size of the final signed manifest. - // We specify "image/jpeg" to get JPEG-formatted placeholder bytes. - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - ASSERT_GT(placeholder.size(), 0) << "Placeholder should not be empty"; - size_t placeholder_size = placeholder.size(); - - // 2: Build DataHash JSON - // In production, you would embed the placeholder and note its location. - // The exclusion range would match where you embedded it. - size_t embed_offset = 20; // Example offset - std::string data_hash = R"({ - "exclusions": [{ - "start": )" + std::to_string(embed_offset) + R"(, - "length": )" + std::to_string(placeholder.size()) + R"( - }], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "", - "pad": " " - })"; - - // 3: Sign with auto-calculated hash - // The SDK reads the asset stream, skips the exclusion range, - // calculates SHA-256 hash, and signs the manifest. - std::ifstream asset_stream(source_asset, std::ios::binary); - ASSERT_TRUE(asset_stream.is_open()); - - auto raw_manifest = builder.sign_data_hashed_embeddable( - signer, data_hash, "application/c2pa", &asset_stream); - asset_stream.close(); - ASSERT_GT(raw_manifest.size(), 0); - - // 4: Format for JPEG embedding - // Convert raw JUMBF to JPEG APP11 segments - auto jpeg_embeddable = c2pa::Builder::format_embeddable("image/jpeg", raw_manifest); - ASSERT_GT(jpeg_embeddable.size(), 0); - - // 5: Verify invariant (the placement size) - // The formatted manifest must be exactly the same size as the placeholder. - // This enables in-place patching in production workflows. - EXPECT_EQ(jpeg_embeddable.size(), placeholder_size) - << "Signed manifest size must match placeholder size for in-place patching"; + auto builder = make_builder(); + auto placeholder = builder.placeholder("image/jpeg"); + ASSERT_FALSE(placeholder.empty()); + builder.set_data_hash_exclusions({{20, placeholder.size()}}); + std::ifstream asset(c2pa_test::get_fixture_path("A.jpg"), std::ios::binary); + ASSERT_TRUE(asset.is_open()); + builder.update_hash_from_stream("image/jpeg", asset); + auto manifest = builder.sign_embeddable("image/jpeg"); + EXPECT_EQ(manifest.size(), placeholder.size()); } -// e2e workflow with A.jpg using new context-signer embeddable APIs TEST_F(EmbeddableTest, FullWorkflowWithAJpgContextSigner) { - // Demonstrates the new embeddable API where the signer lives on the context - // rather than being passed explicitly to placeholder/sign methods. - // - // Workflow: - // 1. needs_placeholder() — check whether a placeholder embed step is required - // 2. placeholder() — get the correctly-sized placeholder bytes to embed - // 3. set_data_hash_exclusions() — register where the placeholder was embedded - // 4. update_hash_from_stream() — hash the asset (SDK skips exclusion ranges) - // 5. sign_embeddable() — sign; output size is exactly placeholder.size() - auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - auto source_asset = c2pa_test::get_fixture_path("A.jpg"); - - // Create a context with the signer attached programmatically auto context = c2pa::Context::ContextBuilder() .with_signer(c2pa_test::create_test_signer()) .create_context(); - auto builder = c2pa::Builder(context, manifest_json); - - // 1: Check if a placeholder is required for JPEG - ASSERT_TRUE(builder.needs_placeholder("image/jpeg")) - << "JPEG always requires a placeholder"; - - // 2: Get the placeholder bytes (size is committed internally for later validation) - auto placeholder_bytes = builder.placeholder("image/jpeg"); - ASSERT_GT(placeholder_bytes.size(), 0) << "Placeholder must not be empty"; - size_t placeholder_size = placeholder_bytes.size(); - - // 3: Register where we would embed the placeholder. - // In production this offset is where you physically write placeholder_bytes. - size_t embed_offset = 20; - builder.set_data_hash_exclusions({{embed_offset, placeholder_bytes.size()}}); - - // 4: Hash the asset stream, skipping the exclusion range - std::ifstream asset_stream(source_asset, std::ios::binary); - ASSERT_TRUE(asset_stream.is_open()); - builder.update_hash_from_stream("image/jpeg", asset_stream); - asset_stream.close(); - - // 5: Sign — output must match the placeholder size (enables in-place patching) - auto signed_manifest = builder.sign_embeddable("image/jpeg"); - ASSERT_GT(signed_manifest.size(), 0); - EXPECT_EQ(signed_manifest.size(), placeholder_size) - << "Signed manifest must be exactly the same size as the placeholder"; + ASSERT_TRUE(builder.needs_placeholder("image/jpeg")); + auto placeholder = builder.placeholder("image/jpeg"); + ASSERT_FALSE(placeholder.empty()); + builder.set_data_hash_exclusions({{20, placeholder.size()}}); + std::ifstream asset(c2pa_test::get_fixture_path("A.jpg"), std::ios::binary); + ASSERT_TRUE(asset.is_open()); + builder.update_hash_from_stream("image/jpeg", asset); + auto manifest = builder.sign_embeddable("image/jpeg"); + EXPECT_EQ(manifest.size(), placeholder.size()); } -// e2e workflow with C.jpg (has existing C2PA metadata) TEST_F(EmbeddableTest, FullWorkflowWithCJpg) { - auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - auto signer = c2pa_test::create_test_signer(); - auto source_asset = c2pa_test::get_fixture_path("C.jpg"); - - auto context = c2pa::Context(); - auto builder = c2pa::Builder(context, manifest_json); - - // Get placeholder - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - ASSERT_GT(placeholder.size(), 0); - - // Build data hash JSON - size_t embed_offset = 20; - std::string data_hash = R"({ - "exclusions": [{ - "start": )" + std::to_string(embed_offset) + R"(, - "length": )" + std::to_string(placeholder.size()) + R"( - }], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "", - "pad": " " - })"; - - // Sign with the asset that has existing C2PA - std::ifstream asset_stream(source_asset, std::ios::binary); - auto raw_manifest = builder.sign_data_hashed_embeddable( - signer, data_hash, "application/c2pa", &asset_stream); - asset_stream.close(); - ASSERT_GT(raw_manifest.size(), 0); - - // Format for jpeg - auto jpeg_embeddable = c2pa::Builder::format_embeddable("image/jpeg", raw_manifest); - ASSERT_GT(jpeg_embeddable.size(), 0); - - // Verify invariant: size matches so we could place - EXPECT_EQ(jpeg_embeddable.size(), placeholder.size()); + auto builder = make_builder(); + auto placeholder = builder.placeholder("image/jpeg"); + ASSERT_FALSE(placeholder.empty()); + builder.set_data_hash_exclusions({{20, placeholder.size()}}); + std::ifstream asset(c2pa_test::get_fixture_path("C.jpg"), std::ios::binary); + ASSERT_TRUE(asset.is_open()); + builder.update_hash_from_stream("image/jpeg", asset); + auto manifest = builder.sign_embeddable("image/jpeg"); + EXPECT_EQ(manifest.size(), placeholder.size()); } -// Pre-calculated hash TEST_F(EmbeddableTest, PreCalculatedHash) { - // This test demonstrates signing with a pre-calculated hash. - // In production, you might calculate the hash on a different machine or at a different time than signing. - - auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - auto signer = c2pa_test::create_test_signer(); - - auto context = c2pa::Context(); - auto builder = c2pa::Builder(context, manifest_json); - - // Get placeholder - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - - // Build data hash JSON with a KNOWN hash value - // In this test, we use a dummy hash since we're not actually embedding. - // In production, this would be a real SHA-256 hash of your asset. - std::string data_hash = R"({ - "exclusions": [{ - "start": 20, - "length": )" + std::to_string(placeholder.size()) + R"( - }], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "gWZNEOMHQNiULfA/tO5HD2awOwYMA3tnfUPApIr9csk=", - "pad": " " - })"; - - // Sign without providing an asset stream - // The SDK uses the hash value from the data_hash JSON directly - auto manifest = builder.sign_data_hashed_embeddable( - signer, data_hash, "image/jpeg", nullptr); - - EXPECT_GT(manifest.size(), 0) << "Should produce signed manifest bytes"; - EXPECT_EQ(manifest.size(), placeholder.size()) << "Size should match placeholder"; + auto context = c2pa::Context::ContextBuilder() + .with_signer(c2pa_test::create_test_signer()) + .create_context(); + auto builder = c2pa::Builder(context, c2pa_test::prehashed_training_manifest()); + // A precomputed hard binding uses direct mode, with no asset stream. + auto manifest = builder.sign_embeddable("image/jpeg"); + EXPECT_FALSE(manifest.empty()); } -// Auto-calculated hash TEST_F(EmbeddableTest, AutoCalculatedHash) { - // This test demonstrates the SDK calculating the hash automatically from an asset stream. - // The hash field is left empty (""), and the SDK will: - // 1. Read the asset stream - // 2. Skip the exclusion ranges - // 3. Calculate the SHA-256 hash of the remaining bytes - // - // Usually (not in a test), you would have already embedded the placeholder at the exclusion offset. - // Here we just verify the API accepts an asset stream and auto-calculates the hash. - - auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - auto signer = c2pa_test::create_test_signer(); - auto source_asset = c2pa_test::get_fixture_path("A.jpg"); - - auto context = c2pa::Context(); - auto builder = c2pa::Builder(context, manifest_json); - - // Get placeholder - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - - // Build data hash JSON with empty hash field - // The exclusion range would match where you embedded the placeholder - std::string data_hash = R"({ - "exclusions": [{ - "start": 20, - "length": )" + std::to_string(placeholder.size()) + R"( - }], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "", - "pad": " " - })"; - - // Sign with asset stream: SDK will auto-calculate hash - // This stream would be your asset with the placeholder already embedded - std::ifstream asset_stream(source_asset, std::ios::binary); - auto manifest = builder.sign_data_hashed_embeddable( - signer, data_hash, "application/c2pa", &asset_stream); - asset_stream.close(); - - EXPECT_GT(manifest.size(), 0) << "Should produce signed manifest bytes"; + auto builder = make_builder(); + auto placeholder = builder.placeholder("image/jpeg"); + builder.set_data_hash_exclusions({{20, placeholder.size()}}); + std::ifstream asset(c2pa_test::get_fixture_path("A.jpg"), std::ios::binary); + ASSERT_TRUE(asset.is_open()); + builder.update_hash_from_stream("image/jpeg", asset); + auto manifest = builder.sign_embeddable("application/c2pa"); + EXPECT_FALSE(manifest.empty()); } -// Embedding roundtrip TEST_F(EmbeddableTest, FormatEmbeddableRoundTrip) { - // This test demonstrates the two-step format conversion: - // 1. Sign with format="application/c2pa" to get raw JUMBF bytes - // 2. Use format_embeddable() to convert to JPEG-embeddable format - // - // This is useful when you want to sign once and embed in multiple formats. - - auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - auto signer = c2pa_test::create_test_signer(); - auto source_asset = c2pa_test::get_fixture_path("A.jpg"); - - auto context = c2pa::Context(); - auto builder = c2pa::Builder(context, manifest_json); - - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - - std::string data_hash = R"({ - "exclusions": [{ - "start": 20, - "length": )" + std::to_string(placeholder.size()) + R"( - }], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "", - "pad": " " - })"; - - // 1: Sign with "application/c2pa" to get raw JUMBF - std::ifstream asset_stream(source_asset, std::ios::binary); - auto raw_jumbf = builder.sign_data_hashed_embeddable( - signer, data_hash, "application/c2pa", &asset_stream); - asset_stream.close(); - - // 2: Convert raw JUMBF to JPEG-embeddable format - auto jpeg_formatted = c2pa::Builder::format_embeddable("image/jpeg", raw_jumbf); - - // The JPEG format adds container overhead (APP11 markers, headers) - // so the formatted output should be larger than raw JUMBF due to this - EXPECT_GT(jpeg_formatted.size(), raw_jumbf.size()) - << "JPEG format should add container overhead"; - - // But still matches placeholder size (which was already JPEG-formatted) + auto builder = make_builder(); + auto placeholder = builder.placeholder("image/jpeg"); + builder.set_data_hash_exclusions({{20, placeholder.size()}}); + std::ifstream asset(c2pa_test::get_fixture_path("A.jpg"), std::ios::binary); + ASSERT_TRUE(asset.is_open()); + builder.update_hash_from_stream("image/jpeg", asset); + auto raw_jumbf = builder.sign_embeddable("application/c2pa"); + auto jpeg_formatted = builder.sign_embeddable("image/jpeg"); + EXPECT_GT(jpeg_formatted.size(), raw_jumbf.size()); EXPECT_EQ(jpeg_formatted.size(), placeholder.size()); } -// Verify placeholder size matches final size (asset has no existing C2PA) TEST_F(EmbeddableTest, PlaceholderSizeMatchesFinalInvariant) { - // This test verifies the critical invariant: - // placeholder.size() == final_signed_manifest.size() - // This is what enables in-place patching in workflows. - auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - auto signer = c2pa_test::create_test_signer(); - auto source_asset = c2pa_test::get_fixture_path("A.jpg"); - - auto context = c2pa::Context(); - auto builder = c2pa::Builder(context, manifest_json); - - // Get placeholder and record its size - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - size_t placeholder_size = placeholder.size(); - - // Complete the signing flow - size_t embed_offset = 20; - std::string data_hash = R"({ - "exclusions": [{ - "start": )" + std::to_string(embed_offset) + R"(, - "length": )" + std::to_string(placeholder.size()) + R"( - }], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "", - "pad": " " - })"; - - std::ifstream asset_stream(source_asset, std::ios::binary); - auto raw_manifest = builder.sign_data_hashed_embeddable( - signer, data_hash, "application/c2pa", &asset_stream); - asset_stream.close(); - - auto jpeg_embeddable = c2pa::Builder::format_embeddable("image/jpeg", raw_manifest); - - // The final manifest is exactly the same size as placeholder - EXPECT_EQ(jpeg_embeddable.size(), placeholder_size) - << "Final signed manifest MUST be same size as placeholder"; + auto builder = make_builder(); + auto placeholder = builder.placeholder("image/jpeg"); + builder.set_data_hash_exclusions({{20, placeholder.size()}}); + std::ifstream asset(c2pa_test::get_fixture_path("A.jpg"), std::ios::binary); + ASSERT_TRUE(asset.is_open()); + builder.update_hash_from_stream("image/jpeg", asset); + auto manifest = builder.sign_embeddable("image/jpeg"); + EXPECT_EQ(manifest.size(), placeholder.size()); } -// Verify placeholder size matches final size (asset has existing C2PA metadata) TEST_F(EmbeddableTest, PlaceholderSizeMatchesFinalInvariantWithMetadata) { - auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - auto signer = c2pa_test::create_test_signer(); - auto source_asset = c2pa_test::get_fixture_path("C.jpg"); - - auto context = c2pa::Context(); - auto builder = c2pa::Builder(context, manifest_json); - - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - size_t placeholder_size = placeholder.size(); - - size_t embed_offset = 20; - std::string data_hash = R"({ - "exclusions": [{ - "start": )" + std::to_string(embed_offset) + R"(, - "length": )" + std::to_string(placeholder.size()) + R"( - }], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "", - "pad": " " - })"; - - std::ifstream asset_stream(source_asset, std::ios::binary); - auto signed_manifest = builder.sign_data_hashed_embeddable( - signer, data_hash, "image/jpeg", &asset_stream); - asset_stream.close(); - - EXPECT_EQ(signed_manifest.size(), placeholder_size); + auto builder = make_builder(); + auto placeholder = builder.placeholder("image/jpeg"); + builder.set_data_hash_exclusions({{20, placeholder.size()}}); + std::ifstream asset(c2pa_test::get_fixture_path("C.jpg"), std::ios::binary); + ASSERT_TRUE(asset.is_open()); + builder.update_hash_from_stream("image/jpeg", asset); + auto manifest = builder.sign_embeddable("image/jpeg"); + EXPECT_EQ(manifest.size(), placeholder.size()); } TEST_F(EmbeddableTest, ContextSettingsPropagation) { + auto context = c2pa::Context::ContextBuilder() + .with_signer(c2pa_test::create_test_signer()) + .with_json(R"({"builder":{"thumbnail":{"enabled":false}}})") + .create_context(); auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - auto signer = c2pa_test::create_test_signer(); - auto source_asset = c2pa_test::get_fixture_path("A.jpg"); - - // Create context with custom settings - auto context = c2pa::Context(R"({ - "builder": { - "thumbnail": { - "enabled": false - } - } - })"); - - // Use context for builder auto builder = c2pa::Builder(context, manifest_json); - - // Get placeholder - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - ASSERT_GT(placeholder.size(), 0); - - // Sign using the same context - std::string data_hash = R"({ - "exclusions": [{ - "start": 20, - "length": )" + std::to_string(placeholder.size()) + R"( - }], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "", - "pad": " " - })"; - - std::ifstream asset_stream(source_asset, std::ios::binary); - auto manifest = builder.sign_data_hashed_embeddable( - signer, data_hash, "application/c2pa", &asset_stream); - asset_stream.close(); - - EXPECT_GT(manifest.size(), 0) << "Context propagated successfully through workflow"; + auto placeholder = builder.placeholder("image/jpeg"); + ASSERT_FALSE(placeholder.empty()); + builder.set_data_hash_exclusions({{20, placeholder.size()}}); + std::ifstream asset(c2pa_test::get_fixture_path("A.jpg"), std::ios::binary); + ASSERT_TRUE(asset.is_open()); + builder.update_hash_from_stream("image/jpeg", asset); + EXPECT_FALSE(builder.sign_embeddable("image/jpeg").empty()); } -// Verify different formats TEST_F(EmbeddableTest, MultipleFormats) { - auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - auto signer = c2pa_test::create_test_signer(); - - auto context = c2pa::Context(); - - // jpeg - { - auto builder = c2pa::Builder(context, manifest_json); - auto placeholder = builder.data_hashed_placeholder( - signer.reserve_size(), "image/jpeg"); - EXPECT_GT(placeholder.size(), 0) << "JPEG placeholder should not be empty"; - - // Build simple data hash with dummy values - std::string data_hash = R"({ - "exclusions": [{ - "start": 20, - "length": )" + std::to_string(placeholder.size()) + R"( - }], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "gWZNEOMHQNiULfA/tO5HD2awOwYMA3tnfUPApIr9csk=", - "pad": " " - })"; - - auto manifest = builder.sign_data_hashed_embeddable( - signer, data_hash, "image/jpeg", nullptr); - EXPECT_GT(manifest.size(), 0) << "JPEG signed manifest should not be empty"; - } - - // png - { - auto builder = c2pa::Builder(context, manifest_json); - auto placeholder = builder.data_hashed_placeholder( - signer.reserve_size(), "image/png"); - EXPECT_GT(placeholder.size(), 0) << "PNG placeholder should not be empty"; - - std::string data_hash = R"({ - "exclusions": [{ - "start": 33, - "length": )" + std::to_string(placeholder.size()) + R"( - }], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "gWZNEOMHQNiULfA/tO5HD2awOwYMA3tnfUPApIr9csk=", - "pad": " " - })"; - - auto manifest = builder.sign_data_hashed_embeddable( - signer, data_hash, "image/png", nullptr); - EXPECT_GT(manifest.size(), 0) << "PNG signed manifest should not be empty"; + auto context = c2pa::Context::ContextBuilder() + .with_signer(c2pa_test::create_test_signer()) + .create_context(); + for (const auto& format : {"image/jpeg", "image/png"}) { + auto placeholder_builder = make_builder(); + EXPECT_FALSE(placeholder_builder.placeholder(format).empty()); + auto builder = c2pa::Builder(context, c2pa_test::prehashed_training_manifest()); + EXPECT_FALSE(builder.sign_embeddable(format).empty()); } } @@ -613,44 +284,24 @@ TEST_F(EmbeddableTest, BoxHashBmffAlwaysRequiresPlaceholder) { << "MP4 must always require a placeholder regardless of prefer_box_hash"; } -// Direct embedding when giving format TEST_F(EmbeddableTest, DirectEmbeddingWithFormat) { - // This test demonstrates that when you sign with format="image/jpeg", - // the output is already embeddable: no need for format_embeddable()! - // Whereas... - // Signing with "application/c2pa" requires a separate format_embeddable() call. - - auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - auto signer = c2pa_test::create_test_signer(); - auto source_asset = c2pa_test::get_fixture_path("A.jpg"); + auto builder = make_builder(); + auto placeholder = builder.placeholder("image/jpeg"); + builder.set_data_hash_exclusions({{20, placeholder.size()}}); + std::ifstream asset(c2pa_test::get_fixture_path("A.jpg"), std::ios::binary); + ASSERT_TRUE(asset.is_open()); + builder.update_hash_from_stream("image/jpeg", asset); + auto manifest = builder.sign_embeddable("image/jpeg"); + ASSERT_GT(manifest.size(), 2u); + EXPECT_EQ(manifest[0], 0xff); + EXPECT_EQ(manifest[1], 0xeb); + EXPECT_EQ(manifest.size(), placeholder.size()); +} +TEST_F(EmbeddableTest, PlaceholderRequiresContextSigner) { auto context = c2pa::Context(); - auto builder = c2pa::Builder(context, manifest_json); - - auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); - - std::string data_hash = R"({ - "exclusions": [{ - "start": 20, - "length": )" + std::to_string(placeholder.size()) + R"( - }], - "name": "jumbf manifest", - "alg": "sha256", - "hash": "", - "pad": " " - })"; - - std::ifstream asset_stream(source_asset, std::ios::binary); - - // Sign directly with "image/jpeg" format - // This output is already formatted: no format_embeddable() needed - auto jpeg_manifest = builder.sign_data_hashed_embeddable( - signer, data_hash, "image/jpeg", &asset_stream); - asset_stream.close(); - - EXPECT_GT(jpeg_manifest.size(), 0); - EXPECT_EQ(jpeg_manifest.size(), placeholder.size()) - << "Direct JPEG format output matches placeholder size"; + auto builder = c2pa::Builder(context, "{}"); + EXPECT_THROW(builder.placeholder("image/jpeg"), c2pa::C2paException); } class EmbeddableBlankFormatTest : public EmbeddableTest, public ::testing::WithParamInterface {}; diff --git a/tests/include/test_utils.hpp b/tests/include/test_utils.hpp index 1608981..d4cb74d 100644 --- a/tests/include/test_utils.hpp +++ b/tests/include/test_utils.hpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "c2pa.hpp" @@ -78,6 +79,22 @@ inline c2pa::Settings create_test_settings_with_signer() return c2pa::Settings(settings_json, "json"); } +inline std::string prehashed_training_manifest() +{ + auto manifest = nlohmann::json::parse(read_text_file(get_fixture_path("training.json"))); + manifest["assertions"].push_back({ + {"label", "c2pa.hash.data"}, + {"data", { + {"exclusions", {{{"start", 20}, {"length", 100}}}}, + {"name", "jumbf manifest"}, + {"alg", "sha256"}, + {"hash", "gWZNEOMHQNiULfA/tO5HD2awOwYMA3tnfUPApIr9csk="}, + {"pad", " "} + }} + }); + return manifest.dump(); +} + } // namespace c2pa_test #endif // C2PA_TEST_UTILS_HPP diff --git a/tests/zero_copy.test.cpp b/tests/zero_copy.test.cpp new file mode 100644 index 0000000..0c722cc --- /dev/null +++ b/tests/zero_copy.test.cpp @@ -0,0 +1,475 @@ +// Copyright 2026 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 +#include + +#include "include/test_utils.hpp" +#include "../src/c2pa_internal.hpp" + +namespace { + +std::string manifest_definition() +{ + return c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); +} + +std::string signing_certs() +{ + return c2pa_test::read_text_file(c2pa_test::get_fixture_path("ed25519.pub")); +} + +const unsigned char* native_bytes() +{ + const auto key = c2pa_test::read_text_file(c2pa_test::get_fixture_path("ed25519.pem")); + const unsigned char message[] = {1, 2, 3}; + auto* bytes = c2pa_ed25519_sign(message, sizeof(message), key.c_str()); + if (!bytes) { + throw c2pa::C2paException(); + } + return bytes; +} + +c2pa::Signer direct_signer() +{ + return c2pa::Signer("Ed25519", signing_certs(), + c2pa_test::read_text_file(c2pa_test::get_fixture_path("ed25519.pem"))); +} + +c2pa::Signer callback_signer(std::shared_ptr calls) +{ + auto key = c2pa_test::read_text_file(c2pa_test::get_fixture_path("ed25519.pem")); + return c2pa::Signer( + [key, calls](c2pa::ConstBytes data, c2pa::MutableBytes dest) -> intptr_t { + ++*calls; + auto signature = c2pa::detail::to_byte_buffer( + c2pa_ed25519_sign(data.data, data.size, key.c_str()), 64); + if (dest.size < signature.size()) { + return -1; + } + std::copy(signature.begin(), signature.end(), dest.data); + return static_cast(signature.size()); + }, C2paSigningAlg::Ed25519, signing_certs(), ""); +} + +std::stringstream asset() +{ + return std::stringstream(c2pa_test::read_text_file(c2pa_test::get_fixture_path("A.jpg")), + std::ios::in | std::ios::out | std::ios::binary); +} + +void expect_valid(std::iostream& signed_asset) +{ + signed_asset.clear(); + signed_asset.seekg(0); + auto context = std::make_shared(); + auto reader = c2pa::Reader(context, "image/jpeg", signed_asset); + const auto report = nlohmann::json::parse(reader.json()); + const auto state = report.at("validation_state"); + EXPECT_TRUE(state == "Valid" || state == "Trusted") << report.dump(2); +} + +static_assert(!std::is_copy_constructible::value); +static_assert(!std::is_copy_assignable::value); +static_assert(std::is_nothrow_move_constructible::value); +static_assert(std::is_nothrow_move_assignable::value); + +TEST(ByteBuffer, EmptyAndInvalidResults) +{ + c2pa::ByteBuffer empty; + EXPECT_EQ(empty.data(), nullptr); + EXPECT_EQ(empty.begin(), empty.end()); + EXPECT_EQ(empty.size(), 0u); + EXPECT_TRUE(empty.empty()); + EXPECT_TRUE(empty.to_vector().empty()); + EXPECT_TRUE(c2pa::detail::to_byte_buffer(nullptr, 0).empty()); + EXPECT_THROW(c2pa::detail::to_byte_buffer(nullptr, 1), c2pa::C2paException); + EXPECT_THROW(c2pa::detail::to_byte_buffer(nullptr, -1), c2pa::C2paException); + + auto* bytes = native_bytes(); + EXPECT_THROW(c2pa::detail::to_byte_buffer(bytes, -1), c2pa::C2paException); + EXPECT_NE(c2pa_free(bytes), 0); + bytes = native_bytes(); + EXPECT_TRUE(c2pa::detail::to_byte_buffer(bytes, 0).empty()); + EXPECT_NE(c2pa_free(bytes), 0); +} + +TEST(ByteBuffer, IdentityMovesAndExactlyOnceRelease) +{ + auto* bytes = native_bytes(); + const std::vector expected(bytes, bytes + 64); + { + auto original = c2pa::detail::to_byte_buffer(bytes, 64); + EXPECT_EQ(original.data(), bytes); + EXPECT_EQ(original.view().data, bytes); + EXPECT_EQ(original.view().size, 64u); + EXPECT_EQ(original.to_vector(), expected); + EXPECT_EQ(c2pa::detail::to_byte_vector(native_bytes(), 64), expected); + EXPECT_EQ(std::vector(original.begin(), original.end()), expected); + c2pa::ByteBuffer moved(std::move(original)); + EXPECT_TRUE(original.empty()); + EXPECT_EQ(original.data(), nullptr); + EXPECT_EQ(moved.data(), bytes); + auto* replaced = native_bytes(); + auto assigned = c2pa::detail::to_byte_buffer(replaced, 64); + assigned = std::move(moved); + EXPECT_NE(c2pa_free(replaced), 0); + EXPECT_TRUE(moved.empty()); + auto& self = assigned; + assigned = std::move(self); + EXPECT_EQ(assigned.data(), bytes); + EXPECT_EQ(assigned.to_vector(), expected); + } + EXPECT_NE(c2pa_free(bytes), 0); +} + +TEST(ZeroCopyBuilder, AllStreamOverloadsRoundTrip) +{ + auto context = std::make_shared( + c2pa::Context::ContextBuilder().with_signer(direct_signer()).create_context()); + for (int mode = 0; mode < 3; ++mode) { + auto source = asset(); + std::stringstream dest(std::ios::in | std::ios::out | std::ios::binary); + auto signer = direct_signer(); + c2pa::ByteBuffer bytes; + { + c2pa::Builder builder(context, manifest_definition()); + if (mode == 0) { + bytes = builder.sign_to_buffer("image/jpeg", source, dest, signer); + } else if (mode == 1) { + std::ostream& output = dest; + bytes = builder.sign_to_buffer("image/jpeg", source, output, signer); + } else { + bytes = builder.sign_to_buffer("image/jpeg", source, dest); + } + } + ASSERT_FALSE(bytes.empty()); + EXPECT_EQ(bytes.to_vector(), std::vector(bytes.begin(), bytes.end())); + expect_valid(dest); + } +} + +TEST(ZeroCopyBuilder, FileOverloadsRoundTrip) +{ + auto context = std::make_shared( + c2pa::Context::ContextBuilder().with_signer(direct_signer()).create_context()); + for (bool use_context : {false, true}) { + auto path = std::filesystem::temp_directory_path() / + ("c2pa-zero-copy-" + std::to_string( + reinterpret_cast(context.get())) + + (use_context ? "-context.jpg" : "-explicit.jpg")); + struct Cleanup { + std::filesystem::path path; + ~Cleanup() { std::error_code ec; std::filesystem::remove(path, ec); } + } cleanup{path}; + c2pa::Builder builder(context, manifest_definition()); + auto signer = direct_signer(); + auto bytes = use_context + ? builder.sign_to_buffer(c2pa_test::get_fixture_path("A.jpg"), path) + : builder.sign_to_buffer(c2pa_test::get_fixture_path("A.jpg"), path, signer); + EXPECT_FALSE(bytes.empty()); + std::fstream result(path, std::ios::in | std::ios::out | std::ios::binary); + expect_valid(result); + } +} + +TEST(ZeroCopyBuilder, EmbeddableAndVectorEquivalence) +{ + auto context = std::make_shared( + c2pa::Context::ContextBuilder().with_signer(direct_signer()).create_context()); + c2pa::Builder builder(context, c2pa_test::prehashed_training_manifest()); + auto bytes = builder.sign_embeddable_to_buffer("application/c2pa"); + auto vector = builder.sign_embeddable("application/c2pa"); + auto reading_context = std::make_shared( + R"({"verify":{"verify_after_reading":false}})"); + auto assertions = [&](const unsigned char* data, size_t size) { + std::istringstream input(std::string(reinterpret_cast(data), size)); + c2pa::Reader reader(reading_context, "application/c2pa", input); + auto report = nlohmann::json::parse(reader.json()); + return report["manifests"][report["active_manifest"].get()]["assertions"]; + }; + // Independent signing calls generate fresh labels and assertion salts. + // Compare decoded assertions here; the adapter test checks exact byte equality. + EXPECT_EQ(assertions(bytes.data(), bytes.size()), assertions(vector.data(), vector.size())); + EXPECT_THROW(builder.sign_embeddable_to_buffer(""), c2pa::C2paException); + + for (const auto* format : {"image/jpeg", "image/png"}) { + c2pa::Builder buffered(context, manifest_definition()); + c2pa::Builder copied(context, manifest_definition()); + auto placeholder = buffered.placeholder_to_buffer(format); + auto vector_placeholder = copied.placeholder(format); + EXPECT_EQ(placeholder.size(), vector_placeholder.size()); + ASSERT_GT(placeholder.size(), 8u); + EXPECT_TRUE(std::equal(placeholder.begin(), placeholder.begin() + 8, + vector_placeholder.begin())); + buffered.set_data_hash_exclusions({{20, placeholder.size()}}); + auto source = asset(); + buffered.update_hash_from_stream(format, source); + auto final = buffered.sign_embeddable_to_buffer(format); + EXPECT_EQ(final.size(), placeholder.size()); + } + c2pa::Builder no_signer(std::make_shared(), manifest_definition()); + EXPECT_THROW(no_signer.placeholder_to_buffer("image/jpeg"), c2pa::C2paException); + auto source = asset(); + std::stringstream dest; + EXPECT_THROW(no_signer.sign_to_buffer("image/jpeg", source, dest), c2pa::C2paException); +} + +TEST(SignerInto, AdapterForwardsExactPointersAndLengths) +{ + std::array payload{1, 2, 3, 4, 5}; + std::array signature{}; + c2pa::SignerIntoFunc callback = [&](c2pa::ConstBytes data, c2pa::MutableBytes dest) { + EXPECT_EQ(data.data, payload.data() + 1); + EXPECT_EQ(data.size, 3u); + EXPECT_EQ(dest.data, signature.data() + 2); + EXPECT_EQ(dest.size, 6u); + dest.data[0] = 42; + return 1; + }; + EXPECT_EQ(c2pa::detail::signer_into_passthrough( + &callback, payload.data() + 1, 3, signature.data() + 2, 6), 1); + EXPECT_EQ(signature[2], 42); +} + +TEST(SignerInto, AdapterRejectsOversizeExceptionsAndInvalidArguments) +{ + unsigned char byte = 0; + c2pa::SignerIntoFunc callback = [](c2pa::ConstBytes, c2pa::MutableBytes dest) { + return static_cast(dest.size + 1); + }; + EXPECT_EQ(c2pa::detail::signer_into_passthrough(&callback, &byte, 1, &byte, 1), -1); + EXPECT_EQ(errno, ENOBUFS); + callback = [](c2pa::ConstBytes, c2pa::MutableBytes) -> intptr_t { throw 42; }; + EXPECT_EQ(c2pa::detail::signer_into_passthrough(&callback, &byte, 1, &byte, 1), -1); + EXPECT_EQ(errno, EIO); + callback = [](c2pa::ConstBytes, c2pa::MutableBytes) { return -2; }; + EXPECT_EQ(c2pa::detail::signer_into_passthrough(&callback, &byte, 1, &byte, 1), -1); + EXPECT_EQ(errno, EIO); + EXPECT_EQ(c2pa::detail::signer_into_passthrough(nullptr, &byte, 1, &byte, 1), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(SignerInto, MovesContextTransferAndSharedBuilderLifetime) +{ + auto calls = std::make_shared(0); + std::weak_ptr lifetime = calls; + auto signer = callback_signer(calls); + c2pa::Signer moved(std::move(signer)); + auto assigned = direct_signer(); + assigned = std::move(moved); + auto& self = assigned; + assigned = std::move(self); + ASSERT_EQ(signer.c2pa_signer(), nullptr); + ASSERT_EQ(moved.c2pa_signer(), nullptr); + c2pa::Context::ContextBuilder config; + config.with_signer(std::move(assigned)); + auto moved_config = std::move(config); + c2pa::Context::ContextBuilder final_config; + final_config = std::move(moved_config); + auto context = final_config.create_context(); + auto moved_context = std::move(context); + c2pa::Context final_context; + final_context = std::move(moved_context); + auto shared = std::make_shared(std::move(final_context)); + { + c2pa::Builder builder(shared, manifest_definition()); + shared.reset(); + auto source = asset(); + std::stringstream dest; + EXPECT_FALSE(builder.sign_to_buffer("image/jpeg", source, dest).empty()); + expect_valid(dest); + EXPECT_GT(*calls, 0); + calls.reset(); + EXPECT_FALSE(lifetime.expired()); + } + EXPECT_TRUE(lifetime.expired()); +} + +TEST(SignerInto, ReleaseCarriesStableCallbackOwnerAndReplacementCleansUp) +{ + auto calls = std::make_shared(0); + std::weak_ptr lifetime = calls; + { + c2pa::Context::ContextBuilder builder; + builder.with_signer(callback_signer(calls)); + calls.reset(); + auto released = builder.release(); + ASSERT_NE(released.signer_callback_owner, nullptr); + auto* address = released.signer_callback_owner.get(); + auto moved = std::move(released); + EXPECT_EQ(moved.signer_callback_owner.get(), address); + auto* native = c2pa_context_builder_build(moved.builder); + ASSERT_NE(native, nullptr); + c2pa::Context context(native, std::move(moved.callback_owner), + std::move(moved.signer_callback_owner)); + auto source = asset(); + std::stringstream dest; + c2pa::Builder manifest(context, manifest_definition()); + EXPECT_FALSE(manifest.sign_to_buffer("image/jpeg", source, dest).empty()); + expect_valid(dest); + } + EXPECT_TRUE(lifetime.expired()); + + calls = std::make_shared(0); + lifetime = calls; + c2pa::Context::ContextBuilder builder; + builder.with_signer(callback_signer(calls)); + calls.reset(); + builder.with_signer(direct_signer()); + EXPECT_TRUE(lifetime.expired()); +} + +TEST(SignerInto, FailuresReleaseCapturesAndPropagate) +{ + EXPECT_THROW(c2pa::Signer(c2pa::SignerIntoFunc{}, C2paSigningAlg::Ed25519, + signing_certs(), ""), c2pa::C2paException); + for (int failure = 0; failure < 4; ++failure) { + auto marker = std::make_shared(0); + std::weak_ptr lifetime = marker; + { + c2pa::Signer signer([marker, failure](c2pa::ConstBytes, c2pa::MutableBytes dest) -> intptr_t { + ++*marker; + if (failure == 0) { return static_cast(dest.size + 1); } + if (failure == 1) { throw std::runtime_error("signing failed"); } + if (failure == 2) { return -1; } + dest.data[0] = 0; + return 1; + }, C2paSigningAlg::Ed25519, signing_certs(), ""); + auto context = std::make_shared(); + c2pa::Builder builder(context, manifest_definition()); + auto source = asset(); + std::stringstream dest; + EXPECT_THROW(builder.sign_to_buffer("image/jpeg", source, dest, signer), c2pa::C2paException); + EXPECT_GT(*marker, 0); + marker.reset(); + } + EXPECT_TRUE(lifetime.expired()); + } +} + +TEST(SignerInto, StandaloneMoveReplacementAndConfigurationFailureReleaseCaptures) +{ + auto old_calls = std::make_shared(0); + auto new_calls = std::make_shared(0); + std::weak_ptr old_lifetime = old_calls; + std::weak_ptr new_lifetime = new_calls; + { + auto signer = callback_signer(old_calls); + auto replacement = callback_signer(new_calls); + old_calls.reset(); + signer = std::move(replacement); + EXPECT_TRUE(old_lifetime.expired()); + c2pa::Builder builder(std::make_shared(), manifest_definition()); + auto source = asset(); + std::stringstream dest; + EXPECT_FALSE(builder.sign_to_buffer("image/jpeg", source, dest, signer).empty()); + expect_valid(dest); + EXPECT_GT(*new_calls, 0); + new_calls.reset(); + } + EXPECT_TRUE(new_lifetime.expired()); + + auto calls = std::make_shared(0); + std::weak_ptr lifetime = calls; + auto signer = callback_signer(calls); + calls.reset(); + EXPECT_THROW(c2pa::Context::ContextBuilder() + .with_signer(std::move(signer)).with_json("not-json"), c2pa::C2paException); + EXPECT_TRUE(lifetime.expired()); +} + +TEST(SignerInto, ThrowingCallableCopyDoesNotLeakDuringConstruction) +{ + struct Captured { + int* destructions; + ~Captured() { ++*destructions; } + }; + struct Callable { + std::shared_ptr capture; + std::shared_ptr reject_copy; + Callable(std::shared_ptr state, std::shared_ptr reject) + : capture(std::move(state)), reject_copy(std::move(reject)) {} + Callable(const Callable& other) + : capture(other.capture), reject_copy(other.reject_copy) { + if (*reject_copy) { + throw std::runtime_error("callable copy failed"); + } + } + intptr_t operator()(c2pa::ConstBytes, c2pa::MutableBytes) const { return -1; } + }; + int destructions = 0; + auto capture = std::make_shared(); + capture->destructions = &destructions; + auto reject = std::make_shared(false); + c2pa::SignerIntoFunc callback = Callable(capture, reject); + capture.reset(); + *reject = true; + EXPECT_THROW(c2pa::Signer(callback, C2paSigningAlg::Ed25519, signing_certs(), ""), + std::runtime_error); + EXPECT_EQ(destructions, 0); + callback = nullptr; + EXPECT_EQ(destructions, 1); +} + +class RecordingStreambuf : public std::streambuf { +public: + char* read_pointer = nullptr; + const char* write_pointer = nullptr; + std::streamsize read_size = 0; + std::streamsize write_size = 0; +protected: + std::streamsize xsgetn(char* data, std::streamsize size) override { + read_pointer = data; + read_size = size; + std::memset(data, 7, static_cast(size)); + return size; + } + std::streamsize xsputn(const char* data, std::streamsize size) override { + write_pointer = data; + write_size = size; + return size; + } +}; + +TEST(ZeroCopyStreams, SharedAdaptersPreserveCallbackPointers) +{ + RecordingStreambuf buffer; + std::array bytes{}; + std::istream input(&buffer); + std::ostream output(&buffer); + std::iostream io(&buffer); + auto read = [&](auto& stream) { + using Stream = std::decay_t; + EXPECT_EQ(c2pa::detail::stream_reader( + reinterpret_cast(&stream), bytes.data() + 1, 5), 5); + EXPECT_EQ(buffer.read_pointer, reinterpret_cast(bytes.data() + 1)); + EXPECT_EQ(buffer.read_size, 5); + EXPECT_EQ(bytes[1], 7); + }; + auto write = [&](auto& stream) { + using Stream = std::decay_t; + EXPECT_EQ(c2pa::detail::stream_writer( + reinterpret_cast(&stream), bytes.data() + 2, 3), 3); + EXPECT_EQ(buffer.write_pointer, reinterpret_cast(bytes.data() + 2)); + EXPECT_EQ(buffer.write_size, 3); + }; + read(input); + write(output); + read(io); + write(io); +} + +} // namespace