From 878c24b4fad634702fde4e0eb2722763e3905237 Mon Sep 17 00:00:00 2001 From: Eric Scouten Date: Wed, 14 Oct 2026 11:58:22 -0700 Subject: [PATCH 1/3] DO NOT MERGE: Build against c2pa-rs main (nightly) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae96dcf..b3c41e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,19 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Checkout c2pa-rs (main) + uses: actions/checkout@v4 + with: + repository: contentauth/c2pa-rs + ref: main + path: c2pa-rs + + - name: Build against c2pa-rs main + shell: bash + run: | + echo "C2PA_BUILD_FROM_SOURCE=ON" >> "$GITHUB_ENV" + echo "C2PA_RS_PATH=${GITHUB_WORKSPACE//\\//}/c2pa-rs" >> "$GITHUB_ENV" + - name: Check cmake run: cmake --version From 7243f263a0240656cfd6cbdc3d955dcac966655c Mon Sep 17 00:00:00 2001 From: Eric Scouten Date: Fri, 16 Oct 2026 13:38:06 -0700 Subject: [PATCH 2/3] ci: Report nightly revision and skip prebuilt configure Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3c41e3..d8756fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,9 +42,12 @@ jobs: ref: main path: c2pa-rs - - name: Build against c2pa-rs main + - name: Configure c2pa-rs main source build shell: bash run: | + git -C c2pa-rs log -1 --format='c2pa-rs main: %H %s' + cargo --version + rustc --version echo "C2PA_BUILD_FROM_SOURCE=ON" >> "$GITHUB_ENV" echo "C2PA_RS_PATH=${GITHUB_WORKSPACE//\\//}/c2pa-rs" >> "$GITHUB_ENV" @@ -54,9 +57,6 @@ jobs: - name: Check Ninja run: ninja --version - - name: Configure - run: cmake -S . -B build -G Ninja - - name: Setup MSVC toolchain on Windows if: matrix.os == 'windows-latest' uses: ilammy/msvc-dev-cmd@v1 From c1a0855e2aa85ea9a533a5c8f85d70c00efc663b Mon Sep 17 00:00:00 2001 From: Eric Scouten Date: Fri, 16 Oct 2026 14:39:28 -0700 Subject: [PATCH 3/3] Migrate nightly compatibility probe to supported c2pa APIs Replace deleted constructors and settings APIs, adapt opaque streams, and migrate embeddable signing to context signers. Fix source-build artifacts and Windows toolchain setup. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 4 +- docs/context-settings.md | 14 +- docs/embeddable-api.md | 18 + docs/intents.md | 12 +- include/c2pa.hpp | 38 +- src/CMakeLists.txt | 28 +- src/c2pa_builder.cpp | 38 +- src/c2pa_core.cpp | 14 - src/c2pa_reader.cpp | 29 +- tests/CMakeLists.txt | 11 +- tests/builder.test.cpp | 227 ++-------- tests/c-app-test/file_stream.h | 47 +-- tests/c-app-test/test.c | 49 ++- tests/embeddable.test.cpp | 736 ++++++++++----------------------- 14 files changed, 383 insertions(+), 882 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8756fd..7e855ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,8 +58,10 @@ jobs: run: ninja --version - name: Setup MSVC toolchain on Windows - if: matrix.os == 'windows-latest' + if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{ runner.arch == 'ARM64' && 'arm64' || 'x64' }} - name: Print make version and path run: | diff --git a/docs/context-settings.md b/docs/context-settings.md index 5f12515..67148c3 100644 --- a/docs/context-settings.md +++ b/docs/context-settings.md @@ -60,7 +60,7 @@ c2pa::Reader reader(context, "image.jpg"); - **Improves code clarity**: `Builder(context, manifest)` shows configuration is being used > [!NOTE] -> The deprecated `c2pa::load_settings(data, format)` still works but you should migrate to `Context`. **Never mix the two approaches**. +> `c2pa::load_settings(data, format)` has been removed on this nightly-compatibility branch. Pass settings explicitly through a `Context`. > See [Migrating from deprecated APIs](#migrating-from-deprecated-apis). ### Context lifecycle @@ -883,11 +883,11 @@ 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. On this nightly-compatibility branch, `load_settings` has been removed. The context-free `Reader` and `Builder` constructors still compile with deprecation warnings, but create a new default Context rather than reading thread-local settings. This section covers migration to explicit contexts and the `Builder::sign` overloads. ### Quick reference -| Deprecated API | Replacement | +| Removed or deprecated API | Replacement | |---|---| | `load_settings(data, format)` | [`Context` constructors or `ContextBuilder`](#replacing-load_settings) | | `Reader(format, stream)` | [`Reader(shared_ptr, format, stream)`](#adding-a-context-parameter-to-reader-and-builder) | @@ -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)` used to 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 | |--------|------------------------|---------| @@ -906,7 +906,7 @@ The SDK introduced Context-based APIs to replace constructors and functions that | Multiple configs | Awkward (per-thread) | One context per configuration | | Testing | Shared global state | Isolated contexts per test | -**Deprecated:** +**Removed (old code):** ```cpp std::ifstream config_file("settings.json"); @@ -922,11 +922,11 @@ c2pa::Context context(settings_json_string); // or Context(Settings(...)) c2pa::Reader reader(context, "image/jpeg", stream); ``` -If you still use `load_settings`, construct `Reader` or `Builder` **without** a context parameter to continue using the thread-local settings. Prefer passing a context for new code. +There is no thread-local settings fallback. Context-free constructors use default settings; pass a context whenever configuration matters. ### Adding a context parameter to Reader and Builder -The following constructors are deprecated because they rely on thread-local settings: +The following constructors are deprecated because they do not accept explicit configuration: - `Reader(const std::string& format, std::istream& stream)` - `Reader(const std::filesystem::path& source_path)` diff --git a/docs/embeddable-api.md b/docs/embeddable-api.md index f7bc01a..1521e66 100644 --- a/docs/embeddable-api.md +++ b/docs/embeddable-api.md @@ -11,6 +11,24 @@ The embeddable signing API provides direct, fine-grained control over embedding 4. Sign the claim. 5. Patch the manifest in place. +## Migrating from the removed data-hashed APIs + +On this nightly-compatibility branch, `data_hashed_placeholder(reserve_size, format)` +and `sign_data_hashed_embeddable(signer, data_hash, format, asset)` have been removed. +The supported workflow is: + +1. Attach the signer to the Context before constructing the Builder. +2. Call `placeholder(format)` and embed the returned bytes in the asset. +3. Call `set_data_hash_exclusions()` with the embedded placeholder's byte offset and length. +4. Call `update_hash_from_stream(format, asset)` to hash the asset. +5. Call `sign_embeddable(format)` and overwrite the placeholder with its result. + +The new workflow obtains the signature reserve size from the Context signer and +computes the hash from the supplied stream; it does not accept the old precomputed +DataHash JSON argument. Do not change the asset outside the excluded placeholder +region after hashing. `format_embeddable(format, data)` remains available for raw +manifest bytes and now uses the supported `c2pa_builder_compose_manifest` API. + ## Why use the embeddable API The standard `Builder::sign()` handles the full pipeline internally: diff --git a/docs/intents.md b/docs/intents.md index 4e9eae8..a0e13c6 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 `load_settings()` function has been removed on this nightly-compatibility branch. For existing code, see [Context and Settings - Migrating from deprecated APIs](context-settings.md#migrating-from-deprecated-apis). ### Using Context @@ -112,11 +112,7 @@ flowchart TD Check --> |No| CheckCtx{Was a Context with builder.intent provided?} CheckCtx --> |Yes| UseCtx["Use Context intent"] - CheckCtx --> |No| CheckGlobal{Was load_settings called - with builder.intent?} - CheckGlobal --> |Yes| UseGlobal["Use global intent - (deprecated)"] - CheckGlobal --> |No| NoIntent["No intent set. + CheckCtx --> |No| NoIntent["No intent set. Caller must define actions manually in manifest JSON."] ``` @@ -171,8 +167,7 @@ The `Builder` intent controls what `Builder` does with the source stream (source ```mermaid flowchart TD Intent["Intent - (via Context, set_intent, - or load_settings)"] --> Q{Intent type?} + (via Context or set_intent)"] --> Q{Intent type?} Q --> |CREATE| CreateFlow["No parent allowed Source stream is new content"] Q --> |EDIT or UPDATE| EditFlow{Was a parentOf ingredient @@ -485,4 +480,3 @@ c2pa::Builder builder(context, R"({})"); builder.set_intent(Update); builder.sign("signed_asset.jpg", "updated_asset.jpg", signer); ``` - diff --git a/include/c2pa.hpp b/include/c2pa.hpp index ec0bcea..34efab9 100644 --- a/include/c2pa.hpp +++ b/include/c2pa.hpp @@ -533,14 +533,6 @@ namespace c2pa /// @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. - [[deprecated("Use Context::from_json() or Context::from_settings() instead")]] - 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. /// @@ -553,7 +545,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. @@ -616,7 +608,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. @@ -676,7 +668,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. @@ -847,7 +839,7 @@ namespace c2pa std::istream& image_stream, const std::vector& manifest_jumbf); - /// @brief Create a Reader from a stream (will use global settings if any loaded). + /// @brief Create a Reader from a stream using a new default Context. /// @details The validation_status field in the JSON contains validation results. /// @param format The mime format of the stream. /// @param stream The input stream to read from. @@ -856,7 +848,7 @@ namespace c2pa [[deprecated("Use Reader(IContextProvider& context, format, stream) instead")]] Reader(const std::string &format, std::istream &stream); - /// @brief Create a Reader from a file path (will use global settings if any loaded). + /// @brief Create a Reader from a file path using a new default Context. /// @param source_path The path to the file to read. /// @throws C2paException for errors encountered by the C2PA library. /// @deprecated Use Reader(IContextProvider& context, source_path) instead. @@ -1172,7 +1164,7 @@ namespace c2pa /// @throws C2paException if context is null or context->is_valid() returns false. Builder(std::shared_ptr context, const std::string &manifest_json); - /// @brief Create a Builder from a manifest JSON string (will use global settings if any loaded). + /// @brief Create a Builder from a manifest JSON string using a new default Context. /// @param manifest_json The manifest JSON string. /// @throws C2paException for errors encountered by the C2PA library. /// @deprecated Use Builder(IContextProvider& context, manifest_json) instead. @@ -1368,25 +1360,9 @@ 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. - 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. - 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. + /// @param data Unformatted manifest data in "application/c2pa" format. /// @return A formatted copy of the data. static std::vector format_embeddable(const std::string &format, std::vector &data); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 54da8a8..42683a3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -77,13 +77,22 @@ if(C2PA_BUILD_FROM_SOURCE) find_program(CARGO_EXECUTABLE cargo PATHS "$ENV{HOME}/.cargo/bin" REQUIRED) message(STATUS "Found cargo at: ${CARGO_EXECUTABLE}") + set(C2PA_RUST_ARTIFACTS "${C2PA_LIB_NAME}" "c2pa.h") + if(WIN32) + list(APPEND C2PA_RUST_ARTIFACTS "${C2PA_IMPLIB_NAME}") + endif() + set(C2PA_RUST_SOURCES ${C2PA_RUST_ARTIFACTS}) + list(TRANSFORM C2PA_RUST_SOURCES PREPEND "${C2PA_RS_PATH}/target/release/") + set(C2PA_RUST_OUTPUTS ${C2PA_RUST_ARTIFACTS}) + list(TRANSFORM C2PA_RUST_OUTPUTS PREPEND "${C2PA_RUST_BUILD_DIR}/") + # Build the Rust c2pa_c_ffi library add_custom_command( - OUTPUT "${C2PA_RUST_BUILD_DIR}/${C2PA_LIB_NAME}" + OUTPUT ${C2PA_RUST_OUTPUTS} COMMAND ${CARGO_EXECUTABLE} build --release --package c2pa-c-ffi --no-default-features --features "rust_native_crypto, file_io, http, add_thumbnails" - COMMAND ${CMAKE_COMMAND} -E copy - "${C2PA_RS_PATH}/target/release/${C2PA_LIB_NAME}" - "${C2PA_RUST_BUILD_DIR}/${C2PA_LIB_NAME}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${C2PA_RUST_SOURCES} + "${C2PA_RUST_BUILD_DIR}" WORKING_DIRECTORY "${C2PA_RS_PATH}" COMMENT "Building c2pa_c from source using Cargo" VERBATIM @@ -91,16 +100,7 @@ if(C2PA_BUILD_FROM_SOURCE) # Create a custom target that depends on the library add_custom_target(build_c2pa_c ALL - DEPENDS "${C2PA_RUST_BUILD_DIR}/${C2PA_LIB_NAME}" - ) - - # Copy the generated c2pa.h header - add_custom_command( - TARGET build_c2pa_c POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${C2PA_RS_PATH}/target/release/c2pa.h" - "${C2PA_RUST_BUILD_DIR}/c2pa.h" - COMMENT "Copying c2pa.h header" + DEPENDS ${C2PA_RUST_OUTPUTS} ) set(C2PA_C_LIB "${C2PA_RUST_BUILD_DIR}/${C2PA_LIB_NAME}") diff --git a/src/c2pa_builder.cpp b/src/c2pa_builder.cpp index 2b8eec7..bd265b7 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. @@ -394,33 +391,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); @@ -487,7 +457,9 @@ namespace c2pa 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); + Context context; + Builder composer(context); + auto result = c2pa_builder_compose_manifest(composer.c2pa_builder(), normalized_format.c_str(), data.data(), data.size(), &c2pa_manifest_bytes); return detail::to_byte_vector(c2pa_manifest_bytes, result); } diff --git a/src/c2pa_core.cpp b/src/c2pa_core.cpp index aaaf0df..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 Settings on Builder and Reader instead")]] - 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_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/tests/CMakeLists.txt b/tests/CMakeLists.txt index 69b8815..96e937d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -135,9 +135,11 @@ if(UNIX AND NOT APPLE) COMMAND bash -c "echo '=== RPATH Debug Info ===' && echo 'Working dir:' && pwd && echo 'Library files in tests dir:' && ls -la ${c2pa-c_BINARY_DIR}/tests/ && echo 'RPATH of c2pa_c_tests:' && readelf -d ${c2pa-c_BINARY_DIR}/tests/c2pa_c_tests | grep -E '(RPATH|RUNPATH)' && echo 'RPATH of ctest:' && readelf -d ${c2pa-c_BINARY_DIR}/tests/ctest | grep -E '(RPATH|RUNPATH)' && echo 'ldd output for c2pa_c_tests:' && ldd ${c2pa-c_BINARY_DIR}/tests/c2pa_c_tests" ) - add_test(NAME debug_library_location - COMMAND bash -c "echo '=== Library Location Debug ===' && echo 'Current dir:' && pwd && echo 'Contents of build/debug:' && ls -la ${CMAKE_BINARY_DIR}/ && echo 'Contents of _deps:' && ls -la ${CMAKE_BINARY_DIR}/_deps/ && echo 'Contents of prebuilt lib:' && ls -la ${CMAKE_BINARY_DIR}/_deps/c2pa_prebuilt-src/lib/" - ) + if(NOT C2PA_BUILD_FROM_SOURCE) + add_test(NAME debug_library_location + COMMAND bash -c "echo '=== Library Location Debug ===' && echo 'Current dir:' && pwd && echo 'Contents of build/debug:' && ls -la ${CMAKE_BINARY_DIR}/ && echo 'Contents of _deps:' && ls -la ${CMAKE_BINARY_DIR}/_deps/ && echo 'Contents of prebuilt lib:' && ls -la ${CMAKE_BINARY_DIR}/_deps/c2pa_prebuilt-src/lib/" + ) + endif() endif() # For better reliability, especially on Linux, manually add tests instead of auto-discovery @@ -162,7 +164,7 @@ endif() if(WIN32) # On Windows, link to the import library (.lib file) - target_link_libraries(ctest PRIVATE "${C2PA_PREBUILT_INCLUDE_DIR}/../lib/c2pa_c.dll.lib") + target_link_libraries(ctest PRIVATE "${C2PA_C_IMPLIB}") # Add MSVC-specific compile options for C code to handle prebuilt library issues if(MSVC) @@ -196,4 +198,3 @@ setup_c2pa_runtime_deps(ctest) add_test(NAME c_test COMMAND ctest) # Set platform-specific environment and working directory setup_test_environment(c_test "${c2pa-c_SOURCE_DIR}") - diff --git a/tests/builder.test.cpp b/tests/builder.test.cpp index e5bd4c3..1a3b488 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, SignImageFileNoThumbnailAutoGenIsolatedSettings) { - // Run in separate thread for complete test isolation (thread-local settings won't leak) + // Exercise context settings in a worker 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"); + c2pa::Context 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 @@ -1749,174 +1750,20 @@ 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": " " - })"; - std::vector manifest_data; - ASSERT_NO_THROW(manifest_data = builder.sign_data_hashed_embeddable(signer, data_hash, "image/jpeg")); -} - -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": " " - })"; - std::vector manifest_data; - ASSERT_NO_THROW(manifest_data = builder.sign_data_hashed_embeddable(signer, data_hash, "image/jpeg")); -} - -TEST_F(BuilderTest, SignDataHashedEmbeddedWithAsset) +TEST_F(BuilderTest, EmbeddablePlaceholderRequiresContextSigner) { - 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; - } - - 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()); + auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + c2pa::Context context; + c2pa::Builder builder(context, manifest); + EXPECT_THROW(builder.placeholder("image/jpeg"), c2pa::C2paException); } -TEST_F(BuilderTest, SignDataHashedEmbeddedWithAssetUsingContext) +TEST_F(BuilderTest, EmbeddableSigningRequiresContextSigner) { - 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 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; - } - - 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()); + auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + c2pa::Context context; + c2pa::Builder builder(context, manifest); + EXPECT_THROW(builder.sign_embeddable("image/jpeg"), c2pa::C2paException); } TEST_F(BuilderTest, SignWithInvalidStream) @@ -2302,9 +2149,9 @@ TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePath) ASSERT_NO_THROW(reader.json()); } -TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePathPlacedActionThreadLocalSettings) +TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePathPlacedActionIsolatedSettings) { - // Run in separate thread for complete test isolation (thread-local settings won't leak) + // Exercise context settings in a worker thread. std::thread test_thread([this]() { fs::path current_dir = fs::path(__FILE__).parent_path(); @@ -2325,8 +2172,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"); + c2pa::Context 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 +2217,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 +2248,6 @@ TEST_F(BuilderTest, AddIngredientToBuilderUsingBasePathPlacedActionThreadLocalSe } } - // No need to reset settings - thread-local settings destroyed with thread }); // Wait for thread to complete @@ -3530,7 +3375,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 archive_context(R"({"verify": {"verify_after_reading": false}})"); + c2pa::Reader reader(archive_context, "application/c2pa", archive_stream); auto json_result = reader.json(); // Parse the archive JSON to extract ingredient information @@ -3595,9 +3441,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 +3466,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 +3482,14 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchiveToBuilderLegacy) { builder.to_archive(builder_archive); builder_archive.seekg(0); - c2pa::Reader builder_reader("application/c2pa", builder_archive); + c2pa::Context archive_context(R"({"verify": {"verify_after_reading": false}})"); + c2pa::Reader builder_reader(archive_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(archive_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 +3615,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 +3646,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) { @@ -3856,8 +3693,9 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchivesLegacy) { }; // Process each archive: extract ingredients, remap conflicting IDs + c2pa::Context archive_context(R"({"verify": {"verify_after_reading": false}})"); for (auto& stream_ref : archive_streams) { - auto reader = std::make_unique("application/c2pa", stream_ref.get()); + auto reader = std::make_unique(archive_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 +3782,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 +3809,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 +5765,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..3e0a269 100644 --- a/tests/c-app-test/file_stream.h +++ b/tests/c-app-test/file_stream.h @@ -81,39 +81,40 @@ intptr_t flusher(StreamContext *context) return 0; } -C2paStream *create_file_stream(FILE *file) -{ - if (file != NULL) - { - return c2pa_create_stream((StreamContext *)file, reader, seeker, writer, flusher); - } - return NULL; -} - -void release_stream(C2paStream *stream) -{ - c2pa_release_stream(stream); -} +typedef struct FileStream { + FILE *file; + C2paStream *stream; +} FileStream; -C2paStream *open_file_stream(const char *path, const char *mode) +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); + if (file == NULL) { + return NULL; + } + FileStream *wrapper = (FileStream *)malloc(sizeof(*wrapper)); + if (wrapper == NULL) { + fclose(file); + return NULL; + } + wrapper->file = file; + wrapper->stream = c2pa_create_stream((StreamContext *)file, reader, seeker, writer, flusher); + if (wrapper->stream == NULL) { + fclose(file); + free(wrapper); + return NULL; } - return NULL; + return wrapper; } -int close_file_stream(C2paStream *stream) +int close_file_stream(FileStream *stream) { if (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); + free(stream); return result; } diff --git a/tests/c-app-test/test.c b/tests/c-app-test/test.c index e9edbd5..3cd5047 100644 --- a/tests/c-app-test/test.c +++ b/tests/c-app-test/test.c @@ -22,12 +22,17 @@ int main(void) { char *version = c2pa_version(); assert_contains("version", version, "c2pa-c-ffi/0."); + c2pa_free(version); - C2paStream *input_stream = open_file_stream("tests/fixtures/C.jpg", "rb"); + FileStream *input_stream = open_file_stream("tests/fixtures/C.jpg", "rb"); assert_not_null("open_file_stream", input_stream); - C2paReader *reader = c2pa_reader_from_stream("image/jpeg", input_stream); - assert_not_null("c2pa_reader_from_stream", reader); + C2paContext *context = c2pa_context_new(); + assert_not_null("c2pa_context_new", context); + C2paReader *reader = c2pa_reader_from_context(context); + assert_not_null("c2pa_reader_from_context", reader); + reader = c2pa_reader_with_stream(reader, "image/jpeg", input_stream->stream); + assert_not_null("c2pa_reader_with_stream", reader); close_file_stream(input_stream); @@ -44,11 +49,12 @@ int main(void) } // Open a file to write the thumbnail into - C2paStream *thumb_stream = open_file_stream("build/thumb_c.jpg", "wb"); + FileStream *thumb_stream = open_file_stream("build/thumb_c.jpg", "wb"); assert_not_null("open_file_stream thumbnail", thumb_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 +68,42 @@ 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); + C2paBuilder *builder = c2pa_builder_from_context(context); + assert_not_null("c2pa_builder_from_context", builder); + builder = c2pa_builder_with_definition(builder, 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 archive", archive); + 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 archive for reading", archive2); + C2paBuilder *builder2 = c2pa_builder_from_context(context); + assert_not_null("c2pa_builder_from_context", builder2); + builder2 = c2pa_builder_with_archive(builder2, archive2->stream); + assert_not_null("c2pa_builder_with_archive", builder2); 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 source", source); 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 destination", dest); 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) @@ -104,6 +118,7 @@ int main(void) c2pa_free(builder); if (signer != NULL) c2pa_free(signer); + c2pa_free(context); free(certs); free(private_key); diff --git a/tests/embeddable.test.cpp b/tests/embeddable.test.cpp index 3205b08..5ef30db 100644 --- a/tests/embeddable.test.cpp +++ b/tests/embeddable.test.cpp @@ -14,10 +14,12 @@ #include #include #include +#include #include +#include #include +#include #include -#include #include "c2pa.hpp" #include "include/test_utils.hpp" @@ -25,12 +27,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,474 +51,252 @@ 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); } -}; - -// 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"); + std::vector read_bytes(const fs::path& path) { + std::ifstream input(path, std::ios::binary); + if (!input.is_open()) { + throw std::runtime_error("Could not open " + path.string()); + } + return std::vector( + std::istreambuf_iterator(input), std::istreambuf_iterator()); + } - // 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()); + // Replace the fixtures' existing JUMBF APP11 segments, retaining the other + // metadata and the complete scan data. A second manifest store is not valid. + std::vector jpeg_without_jumbf(const std::vector& source, + size_t& removed_segments) { + if (source.size() < 2 || source[0] != 0xff || source[1] != 0xd8) { + throw std::runtime_error("Expected a JPEG fixture"); + } + std::vector result(source.begin(), source.begin() + 2); + removed_segments = 0; + size_t offset = 2; + while (offset + 4 <= source.size()) { + if (source[offset] != 0xff) { + throw std::runtime_error("Expected a JPEG segment"); + } + const auto marker = source[offset + 1]; + if (marker == 0xda || marker == 0xd9) { + result.insert(result.end(), source.begin() + offset, source.end()); + return result; + } + const size_t length = (static_cast(source[offset + 2]) << 8) + | source[offset + 3]; + if (length < 2 || length > source.size() - offset - 2) { + throw std::runtime_error("Invalid JPEG segment length"); + } + const size_t end = offset + 2 + length; + const bool is_jumbf = marker == 0xeb && length >= 4 + && source[offset + 4] == 'J' && source[offset + 5] == 'P'; + if (is_jumbf) { + ++removed_segments; + } else { + result.insert(result.end(), source.begin() + offset, source.begin() + end); + } + offset = end; + } + throw std::runtime_error("JPEG fixture has no scan data"); + } - 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"; -} + void expect_valid_manifest(const json& parsed) { + ASSERT_TRUE(parsed.contains("active_manifest")) << parsed.dump(2); + const auto active = parsed["active_manifest"].get(); + ASSERT_TRUE(parsed.contains("manifests")); + ASSERT_TRUE(parsed["manifests"].contains(active)); + const auto state = parsed.value("validation_state", std::string()); + EXPECT_TRUE(state == "Valid" || state == "Trusted") << parsed.dump(2); + + const auto& manifest = parsed["manifests"][active]; + ASSERT_TRUE(manifest.contains("assertions")); + EXPECT_TRUE(std::any_of(manifest["assertions"].begin(), manifest["assertions"].end(), + [](const json& assertion) { + return assertion.value("label", std::string()) == "cawg.training-mining"; + })) << manifest.dump(2); + } -// 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() + void jpeg_round_trip(c2pa::Context& context, const std::string& fixture, + bool format_raw = false, bool check_tampering = false) { + SCOPED_TRACE(fixture); + auto manifest = json::parse(c2pa_test::read_text_file( + c2pa_test::get_fixture_path("training.json"))); + manifest["title"] = "Embeddable " + fixture; + manifest["format"] = "image/jpeg"; + auto builder = c2pa::Builder(context, manifest.dump()); + ASSERT_TRUE(builder.needs_placeholder("image/jpeg")); + auto placeholder = builder.placeholder("image/jpeg"); + ASSERT_FALSE(placeholder.empty()); + + size_t removed_segments = 0; + auto asset = jpeg_without_jumbf( + read_bytes(c2pa_test::get_fixture_path(fixture)), removed_segments); + if (fixture == "C.jpg") { + EXPECT_GT(removed_segments, 0u) << "C.jpg must exercise manifest replacement"; + } else { + EXPECT_EQ(removed_segments, 0u); + } - 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"); + // A valid COM segment gives the tampering check a non-structural byte + // outside the excluded manifest slot, without corrupting JPEG parsing. + const std::vector comment = {0xff, 0xfe, 0x00, 0x06, 't', 'e', 's', 't'}; + asset.insert(asset.begin() + 2, comment.begin(), comment.end()); + const size_t embed_offset = 2 + comment.size(); + asset.insert(asset.begin() + embed_offset, placeholder.begin(), placeholder.end()); + const auto asset_with_placeholder = asset; + ASSERT_LE(embed_offset + placeholder.size(), asset.size()); + builder.set_data_hash_exclusions({{embed_offset, placeholder.size()}}); + + std::istringstream asset_stream( + std::string(asset.begin(), asset.end()), std::ios::in | std::ios::binary); + builder.update_hash_from_stream("image/jpeg", asset_stream); + + auto signed_manifest = builder.sign_embeddable( + format_raw ? "application/c2pa" : "image/jpeg"); + ASSERT_FALSE(signed_manifest.empty()); + if (format_raw) { + auto formatted = c2pa::Builder::format_embeddable("image/jpeg", signed_manifest); + EXPECT_GT(formatted.size(), signed_manifest.size()); + signed_manifest = std::move(formatted); + } + ASSERT_EQ(signed_manifest.size(), placeholder.size()) + << "In-place patching must not shift any asset bytes"; + std::copy(signed_manifest.begin(), signed_manifest.end(), asset.begin() + embed_offset); + EXPECT_EQ(asset.size(), asset_with_placeholder.size()); + EXPECT_TRUE(std::equal(asset.begin(), asset.begin() + embed_offset, + asset_with_placeholder.begin())); + EXPECT_TRUE(std::equal(asset.begin() + embed_offset + placeholder.size(), asset.end(), + asset_with_placeholder.begin() + embed_offset + placeholder.size())); + + std::istringstream signed_stream( + std::string(asset.begin(), asset.end()), std::ios::in | std::ios::binary); + auto reader = c2pa::Reader(context, "image/jpeg", signed_stream); + auto parsed = json::parse(reader.json()); + expect_valid_manifest(parsed); + ASSERT_TRUE(parsed.contains("active_manifest")); + auto active = parsed["active_manifest"].get(); + EXPECT_EQ(parsed["manifests"][active]["title"], manifest["title"]); + + if (check_tampering) { + asset[6] ^= 1; // COM payload, outside the manifest exclusion. + std::istringstream tampered_stream( + std::string(asset.begin(), asset.end()), std::ios::in | std::ios::binary); + auto tampered_reader = c2pa::Reader(context, "image/jpeg", tampered_stream); + auto tampered = json::parse(tampered_reader.json()); + EXPECT_EQ(tampered.value("validation_state", std::string()), "Invalid"); + EXPECT_NE(tampered.dump().find("assertion.dataHash.mismatch"), std::string::npos); + } + } +}; - // Create a context with the signer attached programmatically +TEST_F(EmbeddableTest, FullWorkflowWithAJpgContextSigner) { 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"; + jpeg_round_trip(context, "A.jpg", false, true); } -// 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()); -} - -// 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-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 context = c2pa::Context::ContextBuilder() + .with_signer(c2pa_test::create_test_signer()) + .create_context(); + jpeg_round_trip(context, "C.jpg"); } -// 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) - 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"; -} - -// 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 context = c2pa::Context::ContextBuilder() + .with_signer(c2pa_test::create_test_signer()) + .create_context(); + jpeg_round_trip(context, "A.jpg", true); } TEST_F(EmbeddableTest, ContextSettingsPropagation) { - 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 + auto context = c2pa::Context::ContextBuilder() + .with_signer(c2pa_test::create_test_signer()) + .with_json(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"; + })") + .create_context(); + jpeg_round_trip(context, "A.jpg"); } -// 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"; - } + auto raw_manifest = read_bytes(c2pa_test::get_fixture_path("cloud_manifest.c2pa")); + ASSERT_FALSE(raw_manifest.empty()); + const auto original = raw_manifest; + auto jpeg = c2pa::Builder::format_embeddable("image/jpeg", raw_manifest); + auto png = c2pa::Builder::format_embeddable("image/png", raw_manifest); + ASSERT_GT(jpeg.size(), raw_manifest.size()); + EXPECT_EQ(jpeg[0], 0xff); + EXPECT_EQ(jpeg[1], 0xeb); + ASSERT_EQ(png.size(), raw_manifest.size() + 12); + EXPECT_EQ(std::string(png.begin() + 4, png.begin() + 8), "caBX"); + EXPECT_TRUE(std::equal(raw_manifest.begin(), raw_manifest.end(), png.begin() + 8)); + EXPECT_EQ(raw_manifest, original) << "Formatting must not modify the raw manifest"; +} - // 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"; - } +TEST_F(EmbeddableTest, PngPlaceholderRoundTrip) { + // A complete 1x1 grayscale/alpha PNG keeps this test independent of extra fixtures. + std::vector asset = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, + 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x08, 0x04, 0x00, 0x00, 0x00, 0xb5, 0x1c, 0x0c, 0x02, + 0x00, 0x00, 0x00, 0x0b, 0x49, 0x44, 0x41, 0x54, + 0x78, 0xda, 0x63, 0x64, 0xf8, 0x0f, 0x00, 0x01, + 0x05, 0x01, 0x01, 0x27, 0x18, 0xe3, 0x66, + 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, + 0xae, 0x42, 0x60, 0x82 + }; + auto context = c2pa::Context::ContextBuilder() + .with_signer(c2pa_test::create_test_signer()) + .create_context(); + auto manifest = json::parse(c2pa_test::read_text_file( + c2pa_test::get_fixture_path("training.json"))); + manifest["format"] = "image/png"; + auto builder = c2pa::Builder(context, manifest.dump()); + ASSERT_TRUE(builder.needs_placeholder("image/png")); + auto placeholder = builder.placeholder("image/png"); + ASSERT_FALSE(placeholder.empty()); + + // PNG ancillary chunks may be inserted immediately after IHDR. + const size_t embed_offset = 33; + asset.insert(asset.begin() + embed_offset, placeholder.begin(), placeholder.end()); + builder.set_data_hash_exclusions({{embed_offset, placeholder.size()}}); + std::istringstream asset_stream( + std::string(asset.begin(), asset.end()), std::ios::in | std::ios::binary); + builder.update_hash_from_stream("image/png", asset_stream); + auto signed_manifest = builder.sign_embeddable("image/png"); + ASSERT_EQ(signed_manifest.size(), placeholder.size()); + std::copy(signed_manifest.begin(), signed_manifest.end(), asset.begin() + embed_offset); + + std::istringstream signed_stream( + std::string(asset.begin(), asset.end()), std::ios::in | std::ios::binary); + auto reader = c2pa::Reader(context, "image/png", signed_stream); + expect_valid_manifest(json::parse(reader.json())); } -// BoxHash: standard Builder::sign() flow with prefer_box_hash enabled +TEST_F(EmbeddableTest, SetDataHashExclusionsRequiresPlaceholder) { + auto context = c2pa::Context::ContextBuilder() + .with_signer(c2pa_test::create_test_signer()) + .create_context(); + auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + auto builder = c2pa::Builder(context, manifest); + EXPECT_THROW(builder.set_data_hash_exclusions({{0, 1}}), c2pa::C2paException); +} + +// BoxHash: standard Builder::sign() flow with prefer_box_hash enabled. TEST_F(EmbeddableTest, BoxHashStandardSign) { auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); auto source_path = c2pa_test::get_fixture_path("A.jpg"); auto output_path = get_temp_path("boxhash_standard.jpg"); - // Enable BoxHash via context settings auto context = c2pa::Context::ContextBuilder() .with_signer(c2pa_test::create_test_signer()) .with_json(R"({ @@ -527,25 +305,18 @@ TEST_F(EmbeddableTest, BoxHashStandardSign) { } })") .create_context(); - auto builder = c2pa::Builder(context, manifest_json); - // Standard sign() handles the entire BoxHash workflow internally std::vector manifest_data; ASSERT_NO_THROW(manifest_data = builder.sign(source_path, output_path)); ASSERT_FALSE(manifest_data.empty()); - // Verify the output can be read back with valid C2PA data auto reader = c2pa::Reader(context, output_path); - std::string json_result; - ASSERT_NO_THROW(json_result = reader.json()); - EXPECT_FALSE(json_result.empty()); + expect_valid_manifest(json::parse(reader.json())); } -// BoxHash: needs_placeholder() returns false when prefer_box_hash is enabled TEST_F(EmbeddableTest, BoxHashNeedsPlaceholderReturnsFalse) { auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - auto context = c2pa::Context::ContextBuilder() .with_signer(c2pa_test::create_test_signer()) .with_json(R"({ @@ -554,19 +325,14 @@ TEST_F(EmbeddableTest, BoxHashNeedsPlaceholderReturnsFalse) { } })") .create_context(); - auto builder = c2pa::Builder(context, manifest_json); - // BoxHash-capable formats should not require a placeholder - ASSERT_FALSE(builder.needs_placeholder("image/jpeg")) - << "JPEG with prefer_box_hash should not require a placeholder"; + ASSERT_FALSE(builder.needs_placeholder("image/jpeg")); } -// BoxHash: embeddable API workflow (update_hash_from_stream + sign_embeddable) TEST_F(EmbeddableTest, BoxHashEmbeddableWorkflow) { 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"); - auto context = c2pa::Context::ContextBuilder() .with_signer(c2pa_test::create_test_signer()) .with_json(R"({ @@ -575,28 +341,25 @@ TEST_F(EmbeddableTest, BoxHashEmbeddableWorkflow) { } })") .create_context(); - auto builder = c2pa::Builder(context, manifest_json); - // No placeholder needed for BoxHash ASSERT_FALSE(builder.needs_placeholder("image/jpeg")); - - // Hash the original asset directly (no placeholder to embed first) 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(); - - // Sign — output is the actual manifest size (not padded to a placeholder) auto manifest_bytes = builder.sign_embeddable("image/jpeg"); - ASSERT_GT(manifest_bytes.size(), 0) - << "BoxHash embeddable should produce signed manifest bytes"; + ASSERT_FALSE(manifest_bytes.empty()); + + auto asset = read_bytes(source_asset); + asset.insert(asset.begin() + 2, manifest_bytes.begin(), manifest_bytes.end()); + std::istringstream signed_stream( + std::string(asset.begin(), asset.end()), std::ios::in | std::ios::binary); + auto reader = c2pa::Reader(context, "image/jpeg", signed_stream); + expect_valid_manifest(json::parse(reader.json())); } -// BoxHash: BMFF formats always require a placeholder regardless of prefer_box_hash TEST_F(EmbeddableTest, BoxHashBmffAlwaysRequiresPlaceholder) { auto manifest_json = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); - auto context = c2pa::Context::ContextBuilder() .with_signer(c2pa_test::create_test_signer()) .with_json(R"({ @@ -605,56 +368,13 @@ TEST_F(EmbeddableTest, BoxHashBmffAlwaysRequiresPlaceholder) { } })") .create_context(); - - auto builder = c2pa::Builder(context, manifest_json); - - // BMFF formats always require a placeholder, even with prefer_box_hash - ASSERT_TRUE(builder.needs_placeholder("video/mp4")) - << "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 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"; + ASSERT_TRUE(builder.needs_placeholder("video/mp4")); } -class EmbeddableBlankFormatTest : public EmbeddableTest, public ::testing::WithParamInterface {}; -// needs_placeholder() and update_hash_from_stream() are the only embeddable steps when the core is silent on a blank format. +class EmbeddableBlankFormatTest : public EmbeddableTest, + public ::testing::WithParamInterface {}; INSTANTIATE_TEST_SUITE_P(EmbeddableBlankFormats, EmbeddableBlankFormatTest, ::testing::ValuesIn(c2pa_test::kBlankFormats));