Skip to content

Commit bbd3057

Browse files
authored
feat: adds a progress/cancel feature to Context for the c2pa-rs sdk (#1927)
* Progress callback API and cancellation Progress API: Replace OperationPhase with ProgressPhase (#[repr(u8)]), and ProgressCallback trait with ProgressCallbackFunc (Fn(ProgressPhase, f32) -> bool). Progress is reported as f32 0.0–1.0 instead of u8 0–100. Cancellation: Add cancel() and is_cancelled() on Context; remove Arc<AtomicBool> cancellation token. Add Error::OperationCancelled. C FFI: Add c2pa_context_builder_set_progress_callback(builder, user_data, callback) and c2pa_context_cancel(ctx). Remove C2paCancellationToken and related APIs. Coverage: Add check_progress in builder (save_to_stream), store, and claim so callbacks run during signing, embedding, and verification. Tests: Add unit tests for progress/cancel in context.rs and integration tests in builder.rs. Docs: Add docs/pro * feat: api update Replaced f32 pct with (u32 step, u32 total) in the callback type Renamed/replaced phases: Ingredients/Verification/RemoteFetch → Reading, VerifyingManifest, VerifyingSignature, VerifyingIngredient, VerifyingAssetHash, FetchingRemoteManifest (keeping Thumbnail, Hashing, Signing, Embedding) added builder_sample.rs — Updated to demonstrate the new API with per-phase elapsed timing output and assertions against the new phase names Added more instrumentation for phases. * add ProgressPhase::Writing * Updates progress/cancel documentation and adds unit tests * Add granular progress callbacks to hashing and verification paths Introduce hash_stream_by_alg_with_progress (pub(crate)) in hash_utils.rs; make hash_stream_by_alg a thin wrapper. Callbacks fire per HashRange. Add _with_progress variants to DataHash, BmffHash, and BoxHash for both signing (gen_hash) and verification (verify_stream_hash); public functions become thin wrappers passing None. Wire progress callbacks through builder.rs, store.rs, and claim.rs using context.check_progress(ProgressPhase::VerifyingAssetHash / Hashing, step, total). Add FetchingOCSP and FetchingTimestamp phases to ProgressPhase; fire FetchingOCSP per OCSP responder in ocsp/fetch.rs. * adds progress.rs example This can work with any file and supports both reading (with one param, and signing/embedding. * chore: review feedback, fix ordering on cancel remove changes to builder_sample< remove test_input.jpg cruft * Update docs/progress_callbacks.md
1 parent 88c1273 commit bbd3057

File tree

19 files changed

+1696
-92
lines changed

19 files changed

+1696
-92
lines changed

c2pa_c_ffi/src/c_api.rs

Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ use std::{
2121
use c2pa::Ingredient;
2222
use c2pa::{
2323
assertions::DataHash, identity::validator::CawgValidator, Builder as C2paBuilder,
24-
CallbackSigner, Context, Reader as C2paReader, Settings as C2paSettings, SigningAlg,
24+
CallbackSigner, Context, ProgressPhase, Reader as C2paReader, Settings as C2paSettings,
25+
SigningAlg,
2526
};
2627
use tokio::runtime::Builder;
2728

@@ -93,6 +94,23 @@ mod cbindgen_fix {
9394
type C2paContextBuilder = Context;
9495
type C2paContext = Arc<Context>;
9596

97+
/// Wraps a `*const c_void` (C `void*`) so it can be moved into a `Send + Sync` closure.
98+
///
99+
/// Accessing through [`as_ptr()`](SendPtr::as_ptr) rather than the raw `.0` field
100+
/// ensures the Rust 2021 disjoint-capture analysis captures the whole `SendPtr`
101+
/// (which is `Send + Sync`) rather than the inner `*const c_void` (which is not).
102+
///
103+
/// # Safety
104+
/// The caller must guarantee the pointer is valid for the closure's lifetime.
105+
struct SendPtr(*const c_void);
106+
unsafe impl Send for SendPtr {}
107+
unsafe impl Sync for SendPtr {}
108+
impl SendPtr {
109+
fn as_ptr(&self) -> *const c_void {
110+
self.0
111+
}
112+
}
113+
96114
/// List of supported signing algorithms.
97115
#[repr(C)]
98116
pub enum C2paSigningAlg {
@@ -495,6 +513,64 @@ pub unsafe extern "C" fn c2pa_context_builder_set_signer(
495513
0
496514
}
497515

516+
/// C-callable progress callback function type.
517+
///
518+
/// # Parameters
519+
/// * `context` – the opaque `user_data` pointer passed to
520+
/// `c2pa_context_builder_set_progress_callback`.
521+
/// * `phase` – numeric value of the [`ProgressPhase`] (see SDK header for constants).
522+
/// Callers should derive any user-visible text from this value in the appropriate language.
523+
/// * `step` – monotonically increasing counter within the current phase, starting at
524+
/// `1`. Resets to `1` at the start of each new phase. Use as a liveness heartbeat:
525+
/// a rising `step` means the SDK is making forward progress. The unit is
526+
/// phase-specific and should otherwise be treated as opaque.
527+
/// * `total` – `0` = indeterminate (show a spinner, use `step` as liveness signal);
528+
/// `1` = single-shot phase (the callback itself is the notification);
529+
/// `> 1` = determinate (`step / total` gives a completion fraction for a progress bar).
530+
///
531+
/// # Return value
532+
/// Return non-zero to continue the operation, zero to cancel.
533+
pub type ProgressCCallback =
534+
unsafe extern "C" fn(context: *const c_void, phase: u8, step: u32, total: u32) -> c_int;
535+
536+
/// Attaches a C progress callback to a context builder.
537+
///
538+
/// The `callback` is invoked at key checkpoints during signing and reading
539+
/// operations. Returning `0` from the callback requests cancellation; the SDK
540+
/// will return an error at the next safe stopping point.
541+
///
542+
/// # Parameters
543+
/// * `builder` – a valid `C2paContextBuilder` pointer.
544+
/// * `user_data` – opaque `void*` captured by the closure and passed as the first argument
545+
/// of every `callback` invocation. Pass `NULL` if the callback does not need user data.
546+
/// * `callback` – C function pointer matching [`ProgressCCallback`].
547+
///
548+
/// # Returns
549+
/// `0` on success, non-zero on error (check `c2pa_error()`).
550+
///
551+
/// # Safety
552+
/// * `builder` must be valid and not yet built.
553+
/// * `user_data` must remain valid for the entire lifetime of the built context.
554+
#[no_mangle]
555+
pub unsafe extern "C" fn c2pa_context_builder_set_progress_callback(
556+
builder: *mut C2paContextBuilder,
557+
user_data: *const c_void,
558+
callback: ProgressCCallback,
559+
) -> c_int {
560+
let builder = deref_mut_or_return_int!(builder, C2paContextBuilder);
561+
// Wrap user_data so the closure can be Send + Sync.
562+
// SAFETY: caller guarantees `user_data` outlives the context.
563+
let ud = SendPtr(user_data);
564+
// Both the C function pointer and user_data are captured in the closure, so
565+
// no separate context-pointer field is needed on Context.
566+
let c_callback = move |phase: ProgressPhase, step: u32, total: u32| {
567+
// SAFETY: caller guarantees `callback` and `ud` are valid.
568+
unsafe { (callback)(ud.as_ptr(), phase as u8, step, total) != 0 }
569+
};
570+
builder.set_progress_callback(c_callback);
571+
0
572+
}
573+
498574
/// Builds an immutable, shareable context from the builder.
499575
///
500576
/// The builder is consumed by this operation and becomes invalid.
@@ -542,6 +618,28 @@ pub unsafe extern "C" fn c2pa_context_new() -> *mut C2paContext {
542618
box_tracked!(Context::new().into_shared())
543619
}
544620

621+
/// Requests cancellation of any in-progress operation on this context.
622+
///
623+
/// Thread-safe — may be called from any thread that holds a valid `C2paContext`
624+
/// pointer. The SDK will return an `OperationCancelled` error at the next safe
625+
/// checkpoint inside the running operation.
626+
///
627+
/// # Parameters
628+
/// * `ctx` – a valid, non-null `C2paContext` pointer obtained from
629+
/// `c2pa_context_builder_build()` or `c2pa_context_new()`.
630+
///
631+
/// # Returns
632+
/// `0` on success, non-zero if `ctx` is null or invalid.
633+
///
634+
/// # Safety
635+
/// `ctx` must be a valid pointer and must not be freed concurrently with this call.
636+
#[no_mangle]
637+
pub unsafe extern "C" fn c2pa_context_cancel(ctx: *mut C2paContext) -> c_int {
638+
let ctx = deref_or_return_int!(ctx, C2paContext);
639+
ctx.cancel();
640+
0
641+
}
642+
545643
///
546644
/// # Errors
547645
/// Returns NULL if there were errors, otherwise returns a JSON string.
@@ -4515,4 +4613,113 @@ verify_after_sign = true
45154613

45164614
unsafe { c2pa_free(builder as *mut c_void) };
45174615
}
4616+
4617+
#[test]
4618+
fn test_c2pa_context_builder_set_progress_callback() {
4619+
use std::sync::atomic::{AtomicU32, Ordering};
4620+
4621+
let call_count = Arc::new(AtomicU32::new(0));
4622+
let raw_ptr = Arc::as_ptr(&call_count) as *const c_void;
4623+
4624+
unsafe extern "C" fn progress_cb(
4625+
context: *const c_void,
4626+
_phase: u8,
4627+
_step: u32,
4628+
_total: u32,
4629+
) -> c_int {
4630+
let counter = &*(context as *const AtomicU32);
4631+
counter.fetch_add(1, Ordering::SeqCst);
4632+
1
4633+
}
4634+
4635+
let builder = unsafe { c2pa_context_builder_new() };
4636+
assert!(!builder.is_null());
4637+
4638+
let result =
4639+
unsafe { c2pa_context_builder_set_progress_callback(builder, raw_ptr, progress_cb) };
4640+
assert_eq!(result, 0, "set_progress_callback should succeed");
4641+
4642+
let context = unsafe { c2pa_context_builder_build(builder) };
4643+
assert!(!context.is_null());
4644+
4645+
unsafe { c2pa_free(context as *mut c_void) };
4646+
// Arc still alive here so the AtomicU32 is valid throughout.
4647+
}
4648+
4649+
#[test]
4650+
fn test_c2pa_context_builder_set_progress_callback_null_user_data() {
4651+
unsafe extern "C" fn progress_cb(
4652+
_context: *const c_void,
4653+
_phase: u8,
4654+
_step: u32,
4655+
_total: u32,
4656+
) -> c_int {
4657+
1
4658+
}
4659+
4660+
let builder = unsafe { c2pa_context_builder_new() };
4661+
assert!(!builder.is_null());
4662+
4663+
let result = unsafe {
4664+
c2pa_context_builder_set_progress_callback(builder, std::ptr::null(), progress_cb)
4665+
};
4666+
assert_eq!(result, 0, "NULL user_data should be accepted");
4667+
4668+
let context = unsafe { c2pa_context_builder_build(builder) };
4669+
assert!(!context.is_null());
4670+
4671+
unsafe { c2pa_free(context as *mut c_void) };
4672+
}
4673+
4674+
#[test]
4675+
fn test_c2pa_context_builder_set_progress_callback_null_builder() {
4676+
unsafe extern "C" fn progress_cb(
4677+
_context: *const c_void,
4678+
_phase: u8,
4679+
_step: u32,
4680+
_total: u32,
4681+
) -> c_int {
4682+
1
4683+
}
4684+
4685+
let result = unsafe {
4686+
c2pa_context_builder_set_progress_callback(
4687+
std::ptr::null_mut(),
4688+
std::ptr::null(),
4689+
progress_cb,
4690+
)
4691+
};
4692+
assert_eq!(result, -1, "NULL builder should return error");
4693+
}
4694+
4695+
#[test]
4696+
fn test_c2pa_context_cancel() {
4697+
let context = unsafe { c2pa_context_new() };
4698+
assert!(!context.is_null());
4699+
4700+
let result = unsafe { c2pa_context_cancel(context) };
4701+
assert_eq!(result, 0, "cancel should succeed on a valid context");
4702+
4703+
unsafe { c2pa_free(context as *mut c_void) };
4704+
}
4705+
4706+
#[test]
4707+
fn test_c2pa_context_cancel_null() {
4708+
let result = unsafe { c2pa_context_cancel(std::ptr::null_mut()) };
4709+
assert_eq!(result, -1, "NULL context should return error");
4710+
}
4711+
4712+
#[test]
4713+
fn test_c2pa_context_cancel_via_builder() {
4714+
let builder = unsafe { c2pa_context_builder_new() };
4715+
assert!(!builder.is_null());
4716+
4717+
let context = unsafe { c2pa_context_builder_build(builder) };
4718+
assert!(!context.is_null());
4719+
4720+
let result = unsafe { c2pa_context_cancel(context) };
4721+
assert_eq!(result, 0, "cancel should work on a built context");
4722+
4723+
unsafe { c2pa_free(context as *mut c_void) };
4724+
}
45184725
}

0 commit comments

Comments
 (0)