From 3b6f683fcc49b6483d14c1e9fc8509ad7beba0c6 Mon Sep 17 00:00:00 2001 From: Darren Clarke Date: Fri, 17 Jul 2026 08:52:58 +0000 Subject: [PATCH 1/3] chore: bump c2pa-rs to v0.90.0 Set c2paVersion=v0.90.0 in library/gradle.properties. All FFI functions the JNI bridge calls are present in the 0.90.0 header with unchanged signatures; the functions 0.90.0 removes were already migrated off. --- library/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/gradle.properties b/library/gradle.properties index e157deb..ff893b0 100644 --- a/library/gradle.properties +++ b/library/gradle.properties @@ -1,3 +1,3 @@ # C2PA Native Library Version # Update this to use a different release from https://github.com/contentauth/c2pa-rs/releases -c2paVersion=v0.85.2 +c2paVersion=v0.90.0 From 170f344ea14c2533ae2ca46828e1116b38a3d125 Mon Sep 17 00:00:00 2001 From: Darren Clarke Date: Fri, 19 Jun 2026 15:23:59 +0000 Subject: [PATCH 2/3] feat: expose reader certificate revocation JSON Add Reader.crJSON() over c2pa_reader_crjson, alongside json() and detailedJson(), wired through the JNI bridge. Includes shared and Android instrumented tests. --- .../contentauth/c2pa/AndroidBuilderTests.kt | 6 +++ library/src/main/jni/c2pa_jni.c | 20 +++++++++ .../kotlin/org/contentauth/c2pa/Reader.kt | 22 ++++++++++ .../contentauth/c2pa/testapp/TestScreen.kt | 1 + .../c2pa/test/shared/BuilderTests.kt | 41 +++++++++++++++++++ 5 files changed, 90 insertions(+) diff --git a/library/src/androidTest/kotlin/org/contentauth/c2pa/AndroidBuilderTests.kt b/library/src/androidTest/kotlin/org/contentauth/c2pa/AndroidBuilderTests.kt index 71f620c..68794a7 100644 --- a/library/src/androidTest/kotlin/org/contentauth/c2pa/AndroidBuilderTests.kt +++ b/library/src/androidTest/kotlin/org/contentauth/c2pa/AndroidBuilderTests.kt @@ -194,6 +194,12 @@ class AndroidBuilderTests : BuilderTests() { assertTrue(result.success, "Context Builder Callback Error Paths test failed: ${result.message}") } + @Test + fun runTestReaderCrJson() = runBlocking { + val result = testReaderCrJson() + assertTrue(result.success, "Reader crJSON test failed: ${result.message}") + } + @Test fun runTestBuilderFromArchive() = runBlocking { val result = testBuilderFromArchive() diff --git a/library/src/main/jni/c2pa_jni.c b/library/src/main/jni/c2pa_jni.c index 3a3e014..a07df46 100644 --- a/library/src/main/jni/c2pa_jni.c +++ b/library/src/main/jni/c2pa_jni.c @@ -770,6 +770,26 @@ JNIEXPORT jstring JNICALL Java_org_contentauth_c2pa_Reader_toDetailedJsonNative( return result; } +JNIEXPORT jstring JNICALL Java_org_contentauth_c2pa_Reader_crjsonNative(JNIEnv *env, jobject obj, jlong readerPtr) { + if (readerPtr == 0) { + (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalStateException"), + "Reader is not initialized"); + return NULL; + } + + struct C2paReader *reader = (struct C2paReader*)(uintptr_t)readerPtr; + char *json = c2pa_reader_crjson(reader); + + if (json == NULL) { + throw_c2pa_exception(env, "Failed to generate crJSON from reader"); + return NULL; + } + + jstring result = cstring_to_jstring(env, json); + c2pa_free(json); + return result; +} + JNIEXPORT jstring JNICALL Java_org_contentauth_c2pa_Reader_remoteUrlNative(JNIEnv *env, jobject obj, jlong readerPtr) { if (readerPtr == 0) { (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalStateException"), diff --git a/library/src/main/kotlin/org/contentauth/c2pa/Reader.kt b/library/src/main/kotlin/org/contentauth/c2pa/Reader.kt index b9ead79..b241d9c 100644 --- a/library/src/main/kotlin/org/contentauth/c2pa/Reader.kt +++ b/library/src/main/kotlin/org/contentauth/c2pa/Reader.kt @@ -296,6 +296,27 @@ class Reader internal constructor(private var ptr: Long) : Closeable { return json } + /** + * Returns the manifest store as a crJSON string. + * + * crJSON is the Content Credentials JSON export format defined by the crJSON specification. + * Use [json] for the standard representation or [detailedJson] for the verbose one. + * + * @return The manifest as a crJSON string + * @throws C2PAError.Api if the manifest cannot be serialized + * + * @see json + * @see detailedJson + */ + @Throws(C2PAError::class) + fun crJSON(): String { + val json = crjsonNative(ptr) + if (json == null) { + throw C2PAError.Api(C2PA.getError() ?: "Failed to convert to crJSON") + } + return json + } + /** * Returns the remote URL where the manifest is hosted, if available. * @@ -395,6 +416,7 @@ class Reader internal constructor(private var ptr: Long) : Closeable { private external fun withFragmentNative(handle: Long, format: String, streamHandle: Long, fragmentHandle: Long): Long private external fun toJsonNative(handle: Long): String? private external fun toDetailedJsonNative(handle: Long): String? + private external fun crjsonNative(handle: Long): String? private external fun remoteUrlNative(handle: Long): String? private external fun isEmbeddedNative(handle: Long): Boolean private external fun resourceToStreamNative(handle: Long, uri: String, streamHandle: Long): Long diff --git a/test-app/app/src/main/kotlin/org/contentauth/c2pa/testapp/TestScreen.kt b/test-app/app/src/main/kotlin/org/contentauth/c2pa/testapp/TestScreen.kt index 60ba1d0..56babd9 100644 --- a/test-app/app/src/main/kotlin/org/contentauth/c2pa/testapp/TestScreen.kt +++ b/test-app/app/src/main/kotlin/org/contentauth/c2pa/testapp/TestScreen.kt @@ -205,6 +205,7 @@ private suspend fun runAllTests(context: Context): List = withContex results.add(builderTests.testContextHttpResolverRemoteFetch()) results.add(builderTests.testContextHttpResolverOkHttpFetch()) results.add(builderTests.testContextBuilderCallbackErrorPaths()) + results.add(builderTests.testReaderCrJson()) results.add(builderTests.testBuilderFromArchive()) results.add(builderTests.testReaderWithManifestData()) results.add(builderTests.testJsonRoundTrip()) diff --git a/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/BuilderTests.kt b/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/BuilderTests.kt index 293b307..4b7df51 100644 --- a/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/BuilderTests.kt +++ b/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/BuilderTests.kt @@ -1239,6 +1239,47 @@ abstract class BuilderTests : TestBase() { } } + suspend fun testReaderCrJson(): TestResult = withContext(Dispatchers.IO) { + runTest("Reader crJSON") { + try { + val certPem = loadResourceAsString("es256_certs") + val keyPem = loadResourceAsString("es256_private") + val sourceImageData = loadResourceAsBytes("pexels_asadphoto_457882") + + val signedData = Builder.fromJson(TEST_MANIFEST_JSON).use { builder -> + ByteArrayStream(sourceImageData).use { source -> + ByteArrayStream().use { dest -> + Signer.fromInfo(SignerInfo(SigningAlgorithm.ES256, certPem, keyPem)).use { signer -> + builder.sign("image/jpeg", source, dest, signer) + } + dest.getData() + } + } + } + + val crjson = ByteArrayStream(signedData).use { signedStream -> + Reader.fromStream("image/jpeg", signedStream).use { reader -> + reader.crJSON() + } + } + + val success = crjson.isNotEmpty() + TestResult( + "Reader crJSON", + success, + if (success) { + "crJSON returned ${crjson.length} chars" + } else { + "crJSON was empty" + }, + "crJSON length: ${crjson.length}", + ) + } catch (e: C2PAError) { + TestResult("Reader crJSON", false, "crJSON flow threw", e.toString()) + } + } + } + suspend fun testBuilderFromArchive(): TestResult = withContext(Dispatchers.IO) { runTest("Builder from Archive") { val manifestJson = TEST_MANIFEST_JSON From 5868c19a8a426240c1119afd738302aac51a71f6 Mon Sep 17 00:00:00 2001 From: Darren Clarke Date: Fri, 17 Jul 2026 09:10:11 +0000 Subject: [PATCH 3/3] chore: migrate Builder.fromArchive off deprecated FFI Route nativeFromArchive through c2pa_builder_from_context and c2pa_builder_with_archive, which replace c2pa_builder_from_archive in c2pa-rs 0.90.0. The Kotlin API is unchanged. --- library/src/main/jni/c2pa_jni.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/library/src/main/jni/c2pa_jni.c b/library/src/main/jni/c2pa_jni.c index a07df46..ce87047 100644 --- a/library/src/main/jni/c2pa_jni.c +++ b/library/src/main/jni/c2pa_jni.c @@ -873,13 +873,25 @@ JNIEXPORT jlong JNICALL Java_org_contentauth_c2pa_Builder_nativeFromArchive(JNIE } struct C2paStream *stream = (struct C2paStream*)(uintptr_t)streamPtr; - struct C2paBuilder *builder = c2pa_builder_from_archive(stream); - + + // Create a builder from a default context, then attach the archive. The + // context can be released once the builder has been created from it. + struct C2paContext *ctx = c2pa_context_new(); + struct C2paBuilder *builder = NULL; + if (ctx != NULL) { + struct C2paBuilder *base = c2pa_builder_from_context(ctx); + if (base != NULL) { + // with_archive consumes `base` and returns a new builder. + builder = c2pa_builder_with_archive(base, stream); + } + c2pa_free(ctx); + } + if (builder == NULL) { throw_c2pa_exception(env, "Failed to create builder from archive"); return 0; } - + return (jlong)(uintptr_t)builder; }