diff --git a/lib/compress/zstd_compress.c b/lib/compress/zstd_compress.c index 9dee14ea8..14f3fb9a2 100644 --- a/lib/compress/zstd_compress.c +++ b/lib/compress/zstd_compress.c @@ -301,6 +301,27 @@ typedef char ZSTD_rust_ref_thread_pool_state_layout[ && offsetof(ZSTD_rust_refThreadPoolState, streamStage) == 2 * sizeof(void*) && sizeof(ZSTD_rust_refThreadPoolState) == 3 * sizeof(void*)) ? 1 : -1]; +typedef void (*ZSTD_rust_freeCCtxContent_f)(void* context); +typedef void (*ZSTD_rust_freeCCtxObject_f)(void* context); +typedef struct { + void* callbackContext; + size_t staticSize; + int cctxInWorkspace; + ZSTD_rust_freeCCtxContent_f freeContent; + ZSTD_rust_freeCCtxObject_f freeObject; +} ZSTD_rust_freeCCtxState; +size_t ZSTD_rust_freeCCtx(const ZSTD_rust_freeCCtxState* state); +typedef char ZSTD_rust_free_cctx_state_layout[ + (offsetof(ZSTD_rust_freeCCtxState, callbackContext) == 0 + && offsetof(ZSTD_rust_freeCCtxState, staticSize) == sizeof(void*) + && offsetof(ZSTD_rust_freeCCtxState, cctxInWorkspace) + == 2 * sizeof(void*) + && offsetof(ZSTD_rust_freeCCtxState, freeContent) + == 3 * sizeof(void*) + && offsetof(ZSTD_rust_freeCCtxState, freeObject) + == 4 * sizeof(void*) + && sizeof(ZSTD_rust_freeCCtxState) == 5 * sizeof(void*)) + ? 1 : -1]; typedef void (*ZSTD_rust_resetCCtxClearAllDicts_f)(void* context); typedef size_t (*ZSTD_rust_resetCCtxResetParams_f)(void* context); typedef struct { @@ -1660,17 +1681,29 @@ static void ZSTD_freeCCtxContent(ZSTD_CCtx* cctx) ZSTD_cwksp_free(&cctx->workspace, cctx->customMem); } +static void ZSTD_rust_freeCCtx_freeContent(void* context) +{ + ZSTD_freeCCtxContent((ZSTD_CCtx*)context); +} + +static void ZSTD_rust_freeCCtx_freeObject(void* context) +{ + ZSTD_CCtx* const cctx = (ZSTD_CCtx*)context; + ZSTD_customFree(cctx, cctx->customMem); +} + size_t ZSTD_freeCCtx(ZSTD_CCtx* cctx) { + ZSTD_rust_freeCCtxState state; DEBUGLOG(3, "ZSTD_freeCCtx (address: %p)", (void*)cctx); - if (cctx==NULL) return 0; /* support free on NULL */ - RETURN_ERROR_IF(cctx->staticSize, memory_allocation, - "not compatible with static CCtx"); - { int cctxInWorkspace = ZSTD_cwksp_owns_buffer(&cctx->workspace, cctx); - ZSTD_freeCCtxContent(cctx); - if (!cctxInWorkspace) ZSTD_customFree(cctx, cctx->customMem); - } - return 0; + state.callbackContext = cctx; + state.staticSize = cctx == NULL ? 0 : cctx->staticSize; + state.cctxInWorkspace = cctx != NULL && cctx->staticSize == 0 + ? ZSTD_cwksp_owns_buffer(&cctx->workspace, cctx) + : 0; + state.freeContent = ZSTD_rust_freeCCtx_freeContent; + state.freeObject = ZSTD_rust_freeCCtx_freeObject; + return ZSTD_rust_freeCCtx(&state); } diff --git a/rust/src/zstd_compress.rs b/rust/src/zstd_compress.rs index 541acac10..40f33a67b 100644 --- a/rust/src/zstd_compress.rs +++ b/rust/src/zstd_compress.rs @@ -1049,6 +1049,55 @@ pub unsafe extern "C" fn ZSTD_rust_refThreadPool( 0 } +type FreeCCtxContentFn = unsafe extern "C" fn(*mut c_void); +type FreeCCtxObjectFn = unsafe extern "C" fn(*mut c_void); + +/// Explicit projection for the public `ZSTD_freeCCtx` wrapper. +/// +/// Rust owns the null/static/embedded-object policy and callback ordering. +/// C retains the private content teardown and custom allocator callbacks. +#[repr(C)] +pub struct ZSTD_rust_freeCCtxState { + callback_context: *mut c_void, + static_size: usize, + cctx_in_workspace: c_int, + free_content: FreeCCtxContentFn, + free_object: FreeCCtxObjectFn, +} + +const _: () = { + assert!(size_of::() == size_of::()); + assert!(size_of::() == size_of::()); + assert!(offset_of!(ZSTD_rust_freeCCtxState, callback_context) == 0); + assert!(offset_of!(ZSTD_rust_freeCCtxState, static_size) == size_of::()); + assert!(offset_of!(ZSTD_rust_freeCCtxState, cctx_in_workspace) == 2 * size_of::()); + assert!(offset_of!(ZSTD_rust_freeCCtxState, free_content) == 3 * size_of::()); + assert!(offset_of!(ZSTD_rust_freeCCtxState, free_object) == 4 * size_of::()); + assert!(size_of::() == 5 * size_of::()); +}; + +/// Free the C-owned context content and, when applicable, its outer object. +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_freeCCtx(state: *const ZSTD_rust_freeCCtxState) -> usize { + if state.is_null() { + return ERROR(ZstdErrorCode::Generic); + } + let state = unsafe { &*state }; + if state.callback_context.is_null() { + return 0; + } + if state.static_size != 0 { + return ERROR(ZstdErrorCode::MemoryAllocation); + } + unsafe { + (state.free_content)(state.callback_context); + if state.cctx_in_workspace == 0 { + (state.free_object)(state.callback_context); + } + } + 0 +} + type ResetCCtxClearAllDictsFn = unsafe extern "C" fn(*mut c_void); type ResetCCtxResetParamsFn = unsafe extern "C" fn(*mut c_void) -> usize; @@ -10558,6 +10607,77 @@ mod tests { assert_eq!(result, ERROR(ZstdErrorCode::Generic)); } + #[derive(Default)] + struct FreeCCtxTestContext { + events: Vec<&'static str>, + } + + unsafe extern "C" fn free_cctx_test_content(context: *mut c_void) { + let context = unsafe { &mut *context.cast::() }; + context.events.push("content"); + } + + unsafe extern "C" fn free_cctx_test_object(context: *mut c_void) { + let context = unsafe { &mut *context.cast::() }; + context.events.push("object"); + } + + fn free_cctx_test_state( + context: *mut c_void, + static_size: usize, + cctx_in_workspace: c_int, + ) -> ZSTD_rust_freeCCtxState { + ZSTD_rust_freeCCtxState { + callback_context: context, + static_size, + cctx_in_workspace, + free_content: free_cctx_test_content, + free_object: free_cctx_test_object, + } + } + + #[test] + fn free_cctx_frees_content_before_an_external_object() { + let mut context = FreeCCtxTestContext::default(); + let state = free_cctx_test_state((&mut context as *mut FreeCCtxTestContext).cast(), 0, 0); + + let result = unsafe { ZSTD_rust_freeCCtx(&state) }; + + assert_eq!(result, 0); + assert_eq!(context.events, ["content", "object"]); + } + + #[test] + fn free_cctx_skips_the_external_object_when_embedded_in_workspace() { + let mut context = FreeCCtxTestContext::default(); + let state = free_cctx_test_state((&mut context as *mut FreeCCtxTestContext).cast(), 0, 1); + + let result = unsafe { ZSTD_rust_freeCCtx(&state) }; + + assert_eq!(result, 0); + assert_eq!(context.events, ["content"]); + } + + #[test] + fn free_cctx_rejects_static_contexts_before_any_callback() { + let mut context = FreeCCtxTestContext::default(); + let state = free_cctx_test_state((&mut context as *mut FreeCCtxTestContext).cast(), 1, 0); + + let result = unsafe { ZSTD_rust_freeCCtx(&state) }; + + assert_eq!(result, ERROR(ZstdErrorCode::MemoryAllocation)); + assert!(context.events.is_empty()); + } + + #[test] + fn free_cctx_accepts_a_null_context() { + let state = free_cctx_test_state(ptr::null_mut(), 0, 0); + + let result = unsafe { ZSTD_rust_freeCCtx(&state) }; + + assert_eq!(result, 0); + } + #[derive(Default)] struct SetCParamsTestContext { events: Vec<&'static str>,