diff --git a/lib/compress/zstd_cwksp.h b/lib/compress/zstd_cwksp.h index cb5156fd7..d0ac75bad 100644 --- a/lib/compress/zstd_cwksp.h +++ b/lib/compress/zstd_cwksp.h @@ -168,6 +168,75 @@ typedef struct { ZSTD_cwksp_static_alloc_e isStatic; } ZSTD_cwksp; +/* The workspace layout remains private to C. Rust receives field slots for + * initialization and opaque C callbacks for byte-level operations, allocator + * ownership, and sanitizer bookkeeping. */ +typedef void* (*ZSTD_rust_cwksp_allocate_f)(void* context, size_t size); +typedef void (*ZSTD_rust_cwksp_release_f)( + void* context, void* workspace, size_t workspaceSize); +typedef void (*ZSTD_rust_cwksp_callback_f)(void* context); +typedef void (*ZSTD_rust_cwksp_copy_f)(void* destination, const void* source); +typedef struct { + void* callbackContext; + void** workspace; + void** workspaceEnd; + void** objectEnd; + void** tableEnd; + void** tableValidEnd; + void** allocStart; + void** initOnceStart; + BYTE* allocFailed; + int* workspaceOversizedDuration; + int* phase; + int* isStatic; + void* allocatorContext; + ZSTD_rust_cwksp_allocate_f allocate; + void* releaseContext; + ZSTD_rust_cwksp_release_f release; + ZSTD_rust_cwksp_callback_f clear; + ZSTD_rust_cwksp_callback_f zero; +} ZSTD_rust_cwkspState; +typedef char ZSTD_rust_cwksp_enum_layout[ + (sizeof(ZSTD_cwksp_alloc_phase_e) == sizeof(int) + && sizeof(ZSTD_cwksp_static_alloc_e) == sizeof(int)) + ? 1 : -1]; +typedef char ZSTD_rust_cwksp_constants_layout[ + (ZSTD_CWKSP_ALIGNMENT_BYTES == 64) ? 1 : -1]; +typedef char ZSTD_rust_cwksp_state_layout[ + (offsetof(ZSTD_rust_cwkspState, callbackContext) == 0 + && offsetof(ZSTD_rust_cwkspState, workspace) == sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, workspaceEnd) == 2 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, objectEnd) == 3 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, tableEnd) == 4 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, tableValidEnd) == 5 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, allocStart) == 6 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, initOnceStart) == 7 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, allocFailed) == 8 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, workspaceOversizedDuration) + == 9 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, phase) == 10 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, isStatic) == 11 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, allocatorContext) + == 12 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, allocate) == 13 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, releaseContext) + == 14 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, release) == 15 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, clear) == 16 * sizeof(void*) + && offsetof(ZSTD_rust_cwkspState, zero) == 17 * sizeof(void*) + && sizeof(ZSTD_rust_cwkspState) == 18 * sizeof(void*)) + ? 1 : -1]; +size_t ZSTD_rust_cwkspInit( + const ZSTD_rust_cwkspState* state, + void* start, size_t size, int isStatic); +size_t ZSTD_rust_cwkspCreate( + const ZSTD_rust_cwkspState* state, size_t size); +void ZSTD_rust_cwkspFree(const ZSTD_rust_cwkspState* state); +void ZSTD_rust_cwkspMove( + void* destination, void* source, + ZSTD_rust_cwksp_copy_f copy, + ZSTD_rust_cwksp_callback_f zero); + /*-************************************* * Functions ***************************************/ @@ -657,6 +726,68 @@ MEM_STATIC void ZSTD_cwksp_clear(ZSTD_cwksp* ws) { ZSTD_cwksp_assert_internal_consistency(ws); } +MEM_STATIC void ZSTD_cwksp_rust_clear(void* context) +{ + ZSTD_cwksp_clear((ZSTD_cwksp*)context); +} + +MEM_STATIC void ZSTD_cwksp_rust_zero(void* context) +{ + ZSTD_memset(context, 0, sizeof(ZSTD_cwksp)); +} + +MEM_STATIC void ZSTD_cwksp_rust_copy(void* destination, const void* source) +{ + ZSTD_memcpy(destination, source, sizeof(ZSTD_cwksp)); +} + +MEM_STATIC void* ZSTD_cwksp_rust_allocate(void* context, size_t size) +{ + void* const workspace = ZSTD_customMalloc( + size, *(const ZSTD_customMem*)context); + if (workspace != NULL) { + assert(((size_t)workspace & (sizeof(void*) - 1)) == 0); + } + return workspace; +} + +MEM_STATIC void ZSTD_cwksp_rust_release( + void* context, void* workspace, size_t workspaceSize) +{ + ZSTD_customMem const customMem = *(const ZSTD_customMem*)context; + (void)workspaceSize; +#if ZSTD_MEMORY_SANITIZER && !defined(ZSTD_MSAN_DONT_POISON_WORKSPACE) + if (workspace != NULL && customMem.customFree != NULL) { + __msan_unpoison(workspace, workspaceSize); + } +#endif + ZSTD_customFree(workspace, customMem); +} + +MEM_STATIC ZSTD_rust_cwkspState ZSTD_cwksp_rust_state(ZSTD_cwksp* ws) +{ + ZSTD_rust_cwkspState state; + state.callbackContext = ws; + state.workspace = &ws->workspace; + state.workspaceEnd = &ws->workspaceEnd; + state.objectEnd = &ws->objectEnd; + state.tableEnd = &ws->tableEnd; + state.tableValidEnd = &ws->tableValidEnd; + state.allocStart = &ws->allocStart; + state.initOnceStart = &ws->initOnceStart; + state.allocFailed = &ws->allocFailed; + state.workspaceOversizedDuration = &ws->workspaceOversizedDuration; + state.phase = (int*)&ws->phase; + state.isStatic = (int*)&ws->isStatic; + state.allocatorContext = NULL; + state.allocate = NULL; + state.releaseContext = NULL; + state.release = NULL; + state.clear = ZSTD_cwksp_rust_clear; + state.zero = ZSTD_cwksp_rust_zero; + return state; +} + MEM_STATIC size_t ZSTD_cwksp_sizeof(const ZSTD_cwksp* ws) { return (size_t)((BYTE*)ws->workspaceEnd - (BYTE*)ws->workspace); } @@ -674,36 +805,38 @@ MEM_STATIC size_t ZSTD_cwksp_used(const ZSTD_cwksp* ws) { MEM_STATIC void ZSTD_cwksp_init(ZSTD_cwksp* ws, void* start, size_t size, ZSTD_cwksp_static_alloc_e isStatic) { DEBUGLOG(4, "cwksp: init'ing workspace with %zd bytes", size); assert(((size_t)start & (sizeof(void*)-1)) == 0); /* ensure correct alignment */ - ws->workspace = start; - ws->workspaceEnd = (BYTE*)start + size; - ws->objectEnd = ws->workspace; - ws->tableValidEnd = ws->objectEnd; - ws->initOnceStart = ZSTD_cwksp_initialAllocStart(ws); - ws->phase = ZSTD_cwksp_alloc_objects; - ws->isStatic = isStatic; - ZSTD_cwksp_clear(ws); - ws->workspaceOversizedDuration = 0; + { ZSTD_rust_cwkspState const state = ZSTD_cwksp_rust_state(ws); + size_t const result = ZSTD_rust_cwkspInit( + &state, start, size, (int)isStatic); + assert(!ZSTD_isError(result)); + (void)result; + } ZSTD_cwksp_assert_internal_consistency(ws); } MEM_STATIC size_t ZSTD_cwksp_create(ZSTD_cwksp* ws, size_t size, ZSTD_customMem customMem) { - void* workspace = ZSTD_customMalloc(size, customMem); DEBUGLOG(4, "cwksp: creating new workspace with %zd bytes", size); - RETURN_ERROR_IF(workspace == NULL, memory_allocation, "NULL pointer!"); - ZSTD_cwksp_init(ws, workspace, size, ZSTD_cwksp_dynamic_alloc); - return 0; + { ZSTD_rust_cwkspState state = ZSTD_cwksp_rust_state(ws); + state.allocatorContext = &customMem; + state.allocate = ZSTD_cwksp_rust_allocate; + state.releaseContext = &customMem; + state.release = ZSTD_cwksp_rust_release; + { size_t const result = ZSTD_rust_cwkspCreate(&state, size); + if (!ZSTD_isError(result)) { + ZSTD_cwksp_assert_internal_consistency(ws); + } + return result; + } + } } MEM_STATIC void ZSTD_cwksp_free(ZSTD_cwksp* ws, ZSTD_customMem customMem) { - void *ptr = ws->workspace; DEBUGLOG(4, "cwksp: freeing workspace"); -#if ZSTD_MEMORY_SANITIZER && !defined(ZSTD_MSAN_DONT_POISON_WORKSPACE) - if (ptr != NULL && customMem.customFree != NULL) { - __msan_unpoison(ptr, ZSTD_cwksp_sizeof(ws)); + { ZSTD_rust_cwkspState state = ZSTD_cwksp_rust_state(ws); + state.releaseContext = &customMem; + state.release = ZSTD_cwksp_rust_release; + ZSTD_rust_cwkspFree(&state); } -#endif - ZSTD_memset(ws, 0, sizeof(ZSTD_cwksp)); - ZSTD_customFree(ptr, customMem); } /** @@ -711,8 +844,8 @@ MEM_STATIC void ZSTD_cwksp_free(ZSTD_cwksp* ws, ZSTD_customMem customMem) { * is left in an invalid state (src must be re-init()'ed before it's used again). */ MEM_STATIC void ZSTD_cwksp_move(ZSTD_cwksp* dst, ZSTD_cwksp* src) { - *dst = *src; - ZSTD_memset(src, 0, sizeof(ZSTD_cwksp)); + ZSTD_rust_cwkspMove( + dst, src, ZSTD_cwksp_rust_copy, ZSTD_cwksp_rust_zero); } MEM_STATIC int ZSTD_cwksp_reserve_failed(const ZSTD_cwksp* ws) { diff --git a/rust/src/zstd_compress.rs b/rust/src/zstd_compress.rs index ccb9b2e40..ec27635a4 100644 --- a/rust/src/zstd_compress.rs +++ b/rust/src/zstd_compress.rs @@ -3247,6 +3247,214 @@ pub unsafe extern "C" fn ZSTD_rust_refThreadPool( 0 } +type CwkspAllocateFn = unsafe extern "C" fn(*mut c_void, usize) -> *mut c_void; +type CwkspReleaseFn = unsafe extern "C" fn(*mut c_void, *mut c_void, usize); +type CwkspCallbackFn = unsafe extern "C" fn(*mut c_void); +type CwkspCopyFn = unsafe extern "C" fn(*mut c_void, *const c_void); + +const ZSTD_CWKSP_ALLOC_OBJECTS: c_int = 0; +const ZSTD_CWKSP_DYNAMIC_ALLOC: c_int = 0; +const ZSTD_CWKSP_ALIGNMENT_BYTES: usize = 64; + +/// C-owned field slots and callbacks for the private compression workspace. +/// +/// Rust owns the lifecycle ordering and state publication. C retains the +/// `ZSTD_cwksp` layout, sanitizer-sensitive clear/zero/copy operations, and +/// custom allocator implementation behind these callbacks. All fields are +/// pointer-sized on the C side; nullable function pointers are represented as +/// `Option` so reading the C-side NULL slots is well-defined in Rust. +#[repr(C)] +pub struct ZSTD_rust_cwkspState { + callback_context: *mut c_void, + workspace: *mut *mut c_void, + workspace_end: *mut *mut c_void, + object_end: *mut *mut c_void, + table_end: *mut *mut c_void, + table_valid_end: *mut *mut c_void, + alloc_start: *mut *mut c_void, + init_once_start: *mut *mut c_void, + alloc_failed: *mut u8, + workspace_oversized_duration: *mut c_int, + phase: *mut c_int, + is_static: *mut c_int, + allocator_context: *mut c_void, + allocate: Option, + release_context: *mut c_void, + release: Option, + clear: Option, + zero: Option, +} + +const _: () = { + assert!(size_of::() == size_of::()); + assert!(size_of::() == size_of::()); + assert!(size_of::() == size_of::()); + assert!(size_of::() == size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, callback_context) == 0); + assert!(offset_of!(ZSTD_rust_cwkspState, workspace) == size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, workspace_end) == 2 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, object_end) == 3 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, table_end) == 4 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, table_valid_end) == 5 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, alloc_start) == 6 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, init_once_start) == 7 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, alloc_failed) == 8 * size_of::()); + assert!( + offset_of!(ZSTD_rust_cwkspState, workspace_oversized_duration) + == 9 * size_of::() + ); + assert!(offset_of!(ZSTD_rust_cwkspState, phase) == 10 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, is_static) == 11 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, allocator_context) == 12 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, allocate) == 13 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, release_context) == 14 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, release) == 15 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, clear) == 16 * size_of::()); + assert!(offset_of!(ZSTD_rust_cwkspState, zero) == 17 * size_of::()); + assert!(size_of::() == 18 * size_of::()); +}; + +#[inline] +fn cwksp_initial_alloc_start(workspace_end: *mut c_void) -> *mut c_void { + ((workspace_end as usize) & !(ZSTD_CWKSP_ALIGNMENT_BYTES - 1)) as *mut c_void +} + +#[inline] +fn cwksp_state_has_storage(state: &ZSTD_rust_cwkspState) -> bool { + !state.callback_context.is_null() + && !state.workspace.is_null() + && !state.workspace_end.is_null() + && !state.object_end.is_null() + && !state.table_end.is_null() + && !state.table_valid_end.is_null() + && !state.alloc_start.is_null() + && !state.init_once_start.is_null() + && !state.alloc_failed.is_null() + && !state.workspace_oversized_duration.is_null() + && !state.phase.is_null() + && !state.is_static.is_null() +} + +unsafe fn cwksp_init_state( + state: &ZSTD_rust_cwkspState, + start: *mut c_void, + size: usize, + is_static: c_int, + clear: CwkspCallbackFn, +) { + let workspace_end = unsafe { start.cast::().add(size).cast::() }; + unsafe { + *state.workspace = start; + *state.workspace_end = workspace_end; + *state.object_end = start; + *state.table_valid_end = start; + *state.init_once_start = cwksp_initial_alloc_start(workspace_end); + *state.phase = ZSTD_CWKSP_ALLOC_OBJECTS; + *state.is_static = is_static; + clear(state.callback_context); + *state.workspace_oversized_duration = 0; + } +} + +/// Initialize the private C workspace in the original field-publication order. +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_cwkspInit( + state: *const ZSTD_rust_cwkspState, + start: *mut c_void, + size: usize, + is_static: c_int, +) -> usize { + let Some(state) = (unsafe { state.as_ref() }) else { + return ERROR(ZstdErrorCode::Generic); + }; + let Some(clear) = state.clear else { + return ERROR(ZstdErrorCode::Generic); + }; + if !cwksp_state_has_storage(state) { + return ERROR(ZstdErrorCode::Generic); + } + unsafe { cwksp_init_state(state, start, size, is_static, clear) }; + 0 +} + +/// Allocate and initialize a dynamic private C workspace through its custom +/// allocator callback. Allocation happens before any workspace field changes. +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_cwkspCreate( + state: *const ZSTD_rust_cwkspState, + size: usize, +) -> usize { + let Some(state) = (unsafe { state.as_ref() }) else { + return ERROR(ZstdErrorCode::Generic); + }; + let Some(allocate) = state.allocate else { + return ERROR(ZstdErrorCode::Generic); + }; + let Some(clear) = state.clear else { + return ERROR(ZstdErrorCode::Generic); + }; + if !cwksp_state_has_storage(state) || state.allocator_context.is_null() { + return ERROR(ZstdErrorCode::Generic); + } + + let workspace = unsafe { allocate(state.allocator_context, size) }; + if workspace.is_null() { + return ERROR(ZstdErrorCode::MemoryAllocation); + } + unsafe { cwksp_init_state(state, workspace, size, ZSTD_CWKSP_DYNAMIC_ALLOC, clear) }; + 0 +} + +/// Clear the C workspace metadata and release its owned allocation in order. +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_cwkspFree(state: *const ZSTD_rust_cwkspState) { + let Some(state) = (unsafe { state.as_ref() }) else { + return; + }; + let Some(zero) = state.zero else { + return; + }; + let Some(release) = state.release else { + return; + }; + if state.callback_context.is_null() + || state.workspace.is_null() + || state.workspace_end.is_null() + || state.release_context.is_null() + { + return; + } + + let workspace = unsafe { *state.workspace }; + let workspace_size = if workspace.is_null() { + 0 + } else { + let workspace_end = unsafe { *state.workspace_end }; + (workspace_end as usize).wrapping_sub(workspace as usize) + }; + unsafe { + zero(state.callback_context); + release(state.release_context, workspace, workspace_size); + } +} + +/// Move workspace metadata and leave the source in a zeroed invalid state. +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_cwkspMove( + destination: *mut c_void, + source: *mut c_void, + copy: CwkspCopyFn, + zero: CwkspCallbackFn, +) { + if destination.is_null() || source.is_null() { + return; + } + unsafe { + copy(destination, source); + zero(source); + } +} + type InitCCtxCallbackFn = unsafe extern "C" fn(*mut c_void); type InitCCtxSetCustomMemFn = unsafe extern "C" fn(*mut c_void, *const c_void); type InitCCtxResetFn = unsafe extern "C" fn(*mut c_void) -> usize; @@ -11897,6 +12105,202 @@ mod tests { const ZSTD_BTULTRA2: c_int = 9; const ZSTD_F_ZSTD1_MAGICLESS: c_int = 1; + #[derive(Default)] + struct CwkspTestContext { + events: Vec<&'static str>, + allocation: *mut c_void, + requested_size: usize, + released_workspace: *mut c_void, + released_size: usize, + } + + #[derive(Default)] + struct CwkspTestSlots { + workspace: *mut c_void, + workspace_end: *mut c_void, + object_end: *mut c_void, + table_end: *mut c_void, + table_valid_end: *mut c_void, + alloc_start: *mut c_void, + init_once_start: *mut c_void, + alloc_failed: u8, + workspace_oversized_duration: c_int, + phase: c_int, + is_static: c_int, + } + + unsafe extern "C" fn cwksp_test_clear(context: *mut c_void) { + let context = unsafe { &mut *context.cast::() }; + context.events.push("clear"); + } + + unsafe extern "C" fn cwksp_test_allocate(context: *mut c_void, size: usize) -> *mut c_void { + let context = unsafe { &mut *context.cast::() }; + context.events.push("allocate"); + context.requested_size = size; + context.allocation + } + + unsafe extern "C" fn cwksp_test_zero(context: *mut c_void) { + let context = unsafe { &mut *context.cast::() }; + context.events.push("zero"); + } + + unsafe extern "C" fn cwksp_test_release( + context: *mut c_void, + workspace: *mut c_void, + workspace_size: usize, + ) { + let context = unsafe { &mut *context.cast::() }; + context.events.push("release"); + context.released_workspace = workspace; + context.released_size = workspace_size; + } + + fn cwksp_test_state( + context: &mut CwkspTestContext, + slots: &mut CwkspTestSlots, + ) -> ZSTD_rust_cwkspState { + let context = (context as *mut CwkspTestContext).cast(); + ZSTD_rust_cwkspState { + callback_context: context, + workspace: &mut slots.workspace, + workspace_end: &mut slots.workspace_end, + object_end: &mut slots.object_end, + table_end: &mut slots.table_end, + table_valid_end: &mut slots.table_valid_end, + alloc_start: &mut slots.alloc_start, + init_once_start: &mut slots.init_once_start, + alloc_failed: &mut slots.alloc_failed, + workspace_oversized_duration: &mut slots.workspace_oversized_duration, + phase: &mut slots.phase, + is_static: &mut slots.is_static, + allocator_context: context, + allocate: Some(cwksp_test_allocate), + release_context: context, + release: Some(cwksp_test_release), + clear: Some(cwksp_test_clear), + zero: Some(cwksp_test_zero), + } + } + + #[test] + fn cwksp_init_publishes_bounds_before_clear_and_resets_duration_afterward() { + let mut context = CwkspTestContext::default(); + let mut slots = CwkspTestSlots::default(); + let mut backing = [0usize; 32]; + let start = backing.as_mut_ptr().cast::(); + let size = backing.len() * size_of::(); + let state = cwksp_test_state(&mut context, &mut slots); + + let result = unsafe { ZSTD_rust_cwkspInit(&state, start, size, 1) }; + + let workspace_end = unsafe { start.cast::().add(size).cast::() }; + let initial_alloc_start = + ((workspace_end as usize) & !(ZSTD_CWKSP_ALIGNMENT_BYTES - 1)) as *mut c_void; + assert_eq!(result, 0); + assert_eq!(context.events, ["clear"]); + assert_eq!(slots.workspace, start); + assert_eq!(slots.workspace_end, workspace_end); + assert_eq!(slots.object_end, start); + assert_eq!(slots.table_valid_end, start); + assert_eq!(slots.init_once_start, initial_alloc_start); + assert_eq!(slots.phase, ZSTD_CWKSP_ALLOC_OBJECTS); + assert_eq!(slots.is_static, 1); + assert_eq!(slots.workspace_oversized_duration, 0); + } + + #[test] + fn cwksp_create_allocates_before_initializing_and_preserves_failure_order() { + let mut backing = [0usize; 16]; + let allocation = backing.as_mut_ptr().cast::(); + let mut context = CwkspTestContext { + allocation, + ..Default::default() + }; + let mut slots = CwkspTestSlots::default(); + let state = cwksp_test_state(&mut context, &mut slots); + + let result = unsafe { ZSTD_rust_cwkspCreate(&state, 1234) }; + + assert_eq!(result, 0); + assert_eq!(context.events, ["allocate", "clear"]); + assert_eq!(context.requested_size, 1234); + assert_eq!(slots.workspace, allocation); + assert_eq!(slots.is_static, ZSTD_CWKSP_DYNAMIC_ALLOC); + + let mut failure_context = CwkspTestContext::default(); + let mut failure_slots = CwkspTestSlots::default(); + let failure_state = cwksp_test_state(&mut failure_context, &mut failure_slots); + let result = unsafe { ZSTD_rust_cwkspCreate(&failure_state, 5678) }; + + assert_eq!(result, ERROR(ZstdErrorCode::MemoryAllocation)); + assert_eq!(failure_context.events, ["allocate"]); + assert!(failure_slots.workspace.is_null()); + } + + #[test] + fn cwksp_free_zeroes_metadata_before_releasing_the_owned_range() { + let mut backing = [0usize; 16]; + let workspace = backing.as_mut_ptr().cast::(); + let workspace_size = backing.len() * size_of::(); + let workspace_end = unsafe { workspace.cast::().add(workspace_size).cast() }; + let mut context = CwkspTestContext::default(); + let mut slots = CwkspTestSlots { + workspace, + workspace_end, + ..Default::default() + }; + let state = cwksp_test_state(&mut context, &mut slots); + + unsafe { ZSTD_rust_cwkspFree(&state) }; + + assert_eq!(context.events, ["zero", "release"]); + assert_eq!(context.released_workspace, workspace); + assert_eq!(context.released_size, workspace_size); + } + + #[repr(C)] + struct CwkspMoveTestValue { + values: [usize; 2], + } + + unsafe extern "C" fn cwksp_move_test_copy(destination: *mut c_void, source: *const c_void) { + unsafe { + ptr::copy_nonoverlapping( + source.cast::(), + destination.cast::(), + size_of::(), + ); + } + } + + unsafe extern "C" fn cwksp_move_test_zero(source: *mut c_void) { + unsafe { + ptr::write_bytes(source.cast::(), 0, size_of::()); + } + } + + #[test] + fn cwksp_move_copies_metadata_then_invalidates_the_source() { + let mut destination = CwkspMoveTestValue { values: [0, 0] }; + let mut source = CwkspMoveTestValue { + values: [0x1234, 0x5678], + }; + + unsafe { + ZSTD_rust_cwkspMove( + (&mut destination as *mut CwkspMoveTestValue).cast(), + (&mut source as *mut CwkspMoveTestValue).cast(), + cwksp_move_test_copy, + cwksp_move_test_zero, + ) + }; + + assert_eq!(destination.values, [0x1234, 0x5678]); + assert_eq!(source.values, [0, 0]); + } + struct EstimateCCtxSizeTestContext { compression_levels: Vec, source_size_hints: Vec,