From 0d1f7b77d7fb3f32b936c076ca67deccf651d0c3 Mon Sep 17 00:00:00 2001 From: Eric Scouten Date: Thu, 26 Mar 2026 13:27:00 -0700 Subject: [PATCH 1/2] feat: Add a new factory method to `Reader` which returns `std::nullopt` if no manifest is found (CAI-9682) --- include/c2pa.hpp | 11 +++++++++ src/c2pa_core.cpp | 2 +- src/c2pa_internal.hpp | 5 ++++ src/c2pa_reader.cpp | 24 ++++++++++++++++++ tests/reader.test.cpp | 57 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/include/c2pa.hpp b/include/c2pa.hpp index 52537bc2..a03a3c14 100644 --- a/include/c2pa.hpp +++ b/include/c2pa.hpp @@ -653,6 +653,17 @@ namespace c2pa [[deprecated("Use Reader(IContextProvider& context, source_path) instead")]] Reader(const std::filesystem::path &source_path); + /// @brief Try to open a Reader from a context and file path when the asset may lack C2PA data. + /// @return A Reader if JUMBF (c2pa/manifest) data is present; std::nullopt if none. + /// @throws C2paException for errors other than a missing manifest (e.g. invalid asset). + /// @throws std::system_error if the file cannot be opened. + static std::optional from_asset(IContextProvider& context, const std::filesystem::path &source_path); + + /// @brief Try to create a Reader from a context and stream when the asset may lack C2PA data. + /// @return A Reader if JUMBF (c2pa/manifest) data is present; std::nullopt if none. + /// @throws C2paException for errors other than a missing manifest. + static std::optional from_asset(IContextProvider& context, const std::string &format, std::istream &stream); + // Non-copyable Reader(const Reader&) = delete; diff --git a/src/c2pa_core.cpp b/src/c2pa_core.cpp index 869e6d78..e379c2a0 100644 --- a/src/c2pa_core.cpp +++ b/src/c2pa_core.cpp @@ -86,7 +86,7 @@ namespace c2pa if (result == nullptr) { auto C2paException = c2pa::C2paException(); - if (strstr(C2paException.what(), "ManifestNotFound") != nullptr) + if (detail::error_indicates_manifest_not_found(C2paException.what())) { return std::nullopt; } diff --git a/src/c2pa_internal.hpp b/src/c2pa_internal.hpp index 54d20449..e1fe8ab2 100644 --- a/src/c2pa_internal.hpp +++ b/src/c2pa_internal.hpp @@ -31,6 +31,11 @@ namespace c2pa { namespace detail { +/// @brief True if the C2PA error message indicates no JUMBF / manifest in the asset (ManifestNotFound). +inline bool error_indicates_manifest_not_found(const char* message) noexcept { + return message != nullptr && std::strstr(message, "ManifestNotFound") != nullptr; +} + /// @brief Converts a C array of C strings to a std::vector of std::string. /// @param mime_types Pointer to an array of C strings (const char*). /// @param count Number of elements in the array. diff --git a/src/c2pa_reader.cpp b/src/c2pa_reader.cpp index a5e55f94..42730e92 100644 --- a/src/c2pa_reader.cpp +++ b/src/c2pa_reader.cpp @@ -18,6 +18,22 @@ #include "c2pa.hpp" #include "c2pa_internal.hpp" +namespace { + +template +std::optional reader_from_asset_impl(F&& construct_reader) { + try { + return construct_reader(); + } catch (const c2pa::C2paException& e) { + if (c2pa::detail::error_indicates_manifest_not_found(e.what())) { + return std::nullopt; + } + throw; + } +} + +} // namespace + namespace c2pa { /// Reader class for reading manifests @@ -154,4 +170,12 @@ namespace c2pa auto ptr = c2pa_reader_supported_mime_types(&count); return detail::c_mime_types_to_vector(ptr, count); } + + std::optional Reader::from_asset(IContextProvider& context, const std::filesystem::path& source_path) { + return reader_from_asset_impl([&]() { return Reader(context, source_path); }); + } + + std::optional Reader::from_asset(IContextProvider& context, const std::string& format, std::istream& stream) { + return reader_from_asset_impl([&]() { return Reader(context, format, stream); }); + } } // namespace c2pa diff --git a/tests/reader.test.cpp b/tests/reader.test.cpp index 20bc3c4e..8c56bce8 100644 --- a/tests/reader.test.cpp +++ b/tests/reader.test.cpp @@ -246,6 +246,63 @@ TEST_F(ReaderTest, FileNoManifest) EXPECT_THROW({ auto reader = c2pa::Reader(test_file); }, c2pa::C2paException); }; +TEST_F(ReaderTest, FromAssetNoManifestReturnsNullopt) +{ + fs::path current_dir = fs::path(__FILE__).parent_path(); + fs::path test_file = current_dir / "../tests/fixtures/A.jpg"; + auto context = c2pa::Context(); + auto reader = c2pa::Reader::from_asset(context, test_file); + EXPECT_FALSE(reader.has_value()); +} + +TEST_F(ReaderTest, FromAssetWithManifestReturnsReader) +{ + fs::path current_dir = fs::path(__FILE__).parent_path(); + fs::path test_file = current_dir / "../tests/fixtures/C.jpg"; + auto context = c2pa::Context(); + auto reader = c2pa::Reader::from_asset(context, test_file); + ASSERT_TRUE(reader.has_value()); + EXPECT_TRUE(reader->json().find("C.jpg") != std::string::npos); +} + +TEST_F(ReaderTest, FromAssetStreamNoManifestReturnsNullopt) +{ + fs::path current_dir = fs::path(__FILE__).parent_path(); + fs::path test_file = current_dir / "../tests/fixtures/A.jpg"; + std::ifstream stream(test_file, std::ios::binary); + ASSERT_TRUE(stream); + auto context = c2pa::Context(); + auto reader = c2pa::Reader::from_asset(context, "image/jpeg", stream); + EXPECT_FALSE(reader.has_value()); +} + +TEST_F(ReaderTest, FromAssetStreamWithManifestReturnsReader) +{ + fs::path current_dir = fs::path(__FILE__).parent_path(); + fs::path test_file = current_dir / "../tests/fixtures/C.jpg"; + std::ifstream stream(test_file, std::ios::binary); + ASSERT_TRUE(stream); + auto context = c2pa::Context(); + auto reader = c2pa::Reader::from_asset(context, "image/jpeg", stream); + ASSERT_TRUE(reader.has_value()); + EXPECT_TRUE(reader->json().find("C.jpg") != std::string::npos); +} + +TEST_F(ReaderTest, FromAssetEmptyFileStillThrows) +{ + fs::path empty_file = get_temp_path("from_asset_empty"); + { + std::ofstream f(empty_file, std::ios::binary); + ASSERT_TRUE(f); + } + auto context = c2pa::Context(); + EXPECT_THROW( + { + (void)c2pa::Reader::from_asset(context, empty_file); + }, + c2pa::C2paException); +} + class RemoteUrlTests : public ::testing::TestWithParam> { public: From bf86b09107f6dad4cbf0a490f726bc63a6546d68 Mon Sep 17 00:00:00 2001 From: Eric Scouten Date: Thu, 26 Mar 2026 17:01:39 -0700 Subject: [PATCH 2/2] chore: Bump version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a281460c..1eb57de6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ cmake_minimum_required(VERSION 3.27) # This is the current version of this C++ project -project(c2pa-c VERSION 0.19.0) +project(c2pa-c VERSION 0.19.1) # Set the version of the c2pa_rs library used set(C2PA_VERSION "0.78.6")