From 51ae5c5a186d58e594c55d5aed8e708e4e299b2d Mon Sep 17 00:00:00 2001 From: Matt Gaylor Date: Thu, 20 Nov 2025 08:58:30 -0800 Subject: [PATCH 1/2] feat(sdk): Introduce new API to retrieve raw JUMBF manifest from C2paReader --- c2pa_c_ffi/src/c_api.rs | 29 +++++++++++++++++++++++++++++ sdk/src/crypto/asn1/mod.rs | 10 ++++------ sdk/src/reader.rs | 6 ++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/c2pa_c_ffi/src/c_api.rs b/c2pa_c_ffi/src/c_api.rs index c43effb24..b56c97081 100644 --- a/c2pa_c_ffi/src/c_api.rs +++ b/c2pa_c_ffi/src/c_api.rs @@ -758,6 +758,30 @@ pub unsafe extern "C" fn c2pa_reader_detailed_json(reader_ptr: *mut C2paReader) to_c_string(c2pa_reader.detailed_json()) } +/// Returns a raw JUMBF byte array generated from a C2paReader. +/// +/// # Safety +/// The returned value MUST be released by calling c2pa_string_free +/// and it is no longer valid after that call. +#[no_mangle] +pub unsafe extern "C" fn c2pa_reader_raw_jumbf( + reader_ptr: *mut C2paReader, + manifest_bytes_ptr: *mut *const c_uchar, +) -> i64 { + check_or_return_int!(reader_ptr); + check_or_return_int!(manifest_bytes_ptr); + let c2pa_reader = guard_boxed!(reader_ptr); + let result = c2pa_reader.jumbf_manifest(); + ok_or_return_int!(result, |manifest_bytes: Vec| { + let len = manifest_bytes.len() as i64; + if !manifest_bytes_ptr.is_null() { + *manifest_bytes_ptr = + Box::into_raw(manifest_bytes.into_boxed_slice()) as *const c_uchar; + }; + len + }) +} + /// Returns the remote url of the manifest if it was obtained remotely. /// /// # Parameters @@ -1836,6 +1860,11 @@ mod tests { assert!(json_content.contains("manifest")); assert!(json_content.contains("com.example.test-action")); + let mut manifest_bytes_ptr = std::ptr::null(); + let raw_jumbf_result = unsafe { c2pa_reader_raw_jumbf(reader, &mut manifest_bytes_ptr) }; + assert!(raw_jumbf_result > 0); + assert!(!manifest_bytes_ptr.is_null()); + TestC2paStream::drop_c_stream(source_stream); TestC2paStream::drop_c_stream(read_stream); unsafe { diff --git a/sdk/src/crypto/asn1/mod.rs b/sdk/src/crypto/asn1/mod.rs index 9ef6f355a..dc62e5e52 100644 --- a/sdk/src/crypto/asn1/mod.rs +++ b/sdk/src/crypto/asn1/mod.rs @@ -334,8 +334,8 @@ mod tests { // Helper to load test certificate fn load_test_cert_pem(name: &str) -> Vec { - let path = format!("tests/fixtures/certs/{}", name); - std::fs::read(&path).unwrap_or_else(|_| panic!("Failed to read test certificate: {}", path)) + let path = format!("tests/fixtures/certs/{name}"); + std::fs::read(&path).unwrap_or_else(|_| panic!("Failed to read test certificate: {path}")) } // Helper to parse PEM and extract DER certificate @@ -622,13 +622,11 @@ mod tests { // Verify we got valid DER data for each cert type assert!( !cert_der.is_empty(), - "Certificate {} DER should not be empty", - cert_name + "Certificate {cert_name} DER should not be empty" ); assert_eq!( cert_der[0], 0x30, - "Certificate {} should start with SEQUENCE tag", - cert_name + "Certificate {cert_name} should start with SEQUENCE tag" ); } diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index 0eff6e704..067213534 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -522,6 +522,12 @@ impl Reader { } } + /// Get the Reader as a raw JUMBF Manifest + /// This just calls to_jumbf_internal on the store with no min_reserved_size + pub fn jumbf_manifest(&self) -> Result> { + self.store.to_jumbf_internal(0) + } + /// Returns the remote url of the manifest if this [`Reader`] obtained the manifest remotely. pub fn remote_url(&self) -> Option<&str> { self.store.remote_url() From d5d69a8dbc5d61ad127a1a4dbe5f0681ece1e472 Mon Sep 17 00:00:00 2001 From: Matt Gaylor Date: Fri, 21 Nov 2025 09:41:15 -0800 Subject: [PATCH 2/2] Renaming methods to raw_manifest_jumbf for consistency --- c2pa_c_ffi/src/c_api.rs | 7 ++++--- sdk/src/reader.rs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/c2pa_c_ffi/src/c_api.rs b/c2pa_c_ffi/src/c_api.rs index b56c97081..fd3cca529 100644 --- a/c2pa_c_ffi/src/c_api.rs +++ b/c2pa_c_ffi/src/c_api.rs @@ -764,14 +764,14 @@ pub unsafe extern "C" fn c2pa_reader_detailed_json(reader_ptr: *mut C2paReader) /// The returned value MUST be released by calling c2pa_string_free /// and it is no longer valid after that call. #[no_mangle] -pub unsafe extern "C" fn c2pa_reader_raw_jumbf( +pub unsafe extern "C" fn c2pa_reader_raw_manifest_jumbf( reader_ptr: *mut C2paReader, manifest_bytes_ptr: *mut *const c_uchar, ) -> i64 { check_or_return_int!(reader_ptr); check_or_return_int!(manifest_bytes_ptr); let c2pa_reader = guard_boxed!(reader_ptr); - let result = c2pa_reader.jumbf_manifest(); + let result = c2pa_reader.raw_manifest_jumbf(); ok_or_return_int!(result, |manifest_bytes: Vec| { let len = manifest_bytes.len() as i64; if !manifest_bytes_ptr.is_null() { @@ -1861,7 +1861,8 @@ mod tests { assert!(json_content.contains("com.example.test-action")); let mut manifest_bytes_ptr = std::ptr::null(); - let raw_jumbf_result = unsafe { c2pa_reader_raw_jumbf(reader, &mut manifest_bytes_ptr) }; + let raw_jumbf_result = + unsafe { c2pa_reader_raw_manifest_jumbf(reader, &mut manifest_bytes_ptr) }; assert!(raw_jumbf_result > 0); assert!(!manifest_bytes_ptr.is_null()); diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index 067213534..b3fdd1b8a 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -524,7 +524,7 @@ impl Reader { /// Get the Reader as a raw JUMBF Manifest /// This just calls to_jumbf_internal on the store with no min_reserved_size - pub fn jumbf_manifest(&self) -> Result> { + pub fn raw_manifest_jumbf(&self) -> Result> { self.store.to_jumbf_internal(0) }