use crate::background::{CheckpointWorkContext, CheckpointWorkError};
use hawdb_core::RuntimeMemoryPermit;
use std::alloc::{alloc, dealloc, Layout};
use std::ffi::c_void;
use std::io;
use std::ptr::{self, NonNull};
use zstd::stream::raw::{InBuffer, Operation, OutBuffer, WriteBuf};
use zstd::zstd_safe::zstd_sys as sys;
const QUALIFIED_VERSION: u32 = 10507;
const _: () = assert!(
sys::ZSTD_VERSION_NUMBER == QUALIFIED_VERSION,
"requalify the Zstd custom-memory ABI before updating its bindings"
);
#[repr(C)]
struct CustomMemory {
allocate: unsafe extern "C" fn(*mut c_void, usize) -> *mut c_void,
free: unsafe extern "C" fn(*mut c_void, *mut c_void),
opaque: *mut c_void,
}
unsafe extern "C" {
fn ZSTD_createCCtx_advanced(memory: CustomMemory) -> *mut sys::ZSTD_CCtx;
fn ZSTD_createDCtx_advanced(memory: CustomMemory) -> *mut sys::ZSTD_DCtx;
}
fn custom_memory() -> CustomMemory {
CustomMemory {
allocate,
free,
opaque: ptr::null_mut(),
}
}
struct MemoryContext {
work: CheckpointWorkContext,
failed: std::sync::atomic::AtomicBool,
_memory: Option<Box<dyn RuntimeMemoryPermit>>,
}
#[repr(C, align(16))]
struct AllocationHeader {
size: usize,
memory: Option<Box<dyn RuntimeMemoryPermit>>,
}
pub(super) unsafe extern "C" fn allocate(opaque: *mut c_void, size: usize) -> *mut c_void {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
unsafe { allocate_inner(opaque, size) }
})) {
Ok(pointer) => pointer,
Err(_) => {
unsafe { record_callback_panic(opaque, size, "allocation") };
ptr::null_mut()
}
}
}
unsafe fn record_callback_panic(opaque: *mut c_void, size: usize, phase: &str) {
if !opaque.is_null() {
let context = unsafe { &*opaque.cast::<MemoryContext>() };
context
.failed
.store(true, std::sync::atomic::Ordering::Release);
let work = &context.work;
work.record_failure(CheckpointWorkError::Allocation {
bytes: size as u64,
reason: format!("compression memory callback panicked during {phase}"),
});
}
}
unsafe fn allocate_inner(opaque: *mut c_void, size: usize) -> *mut c_void {
let Some(size) = size.checked_add(size_of::<AllocationHeader>()) else {
return ptr::null_mut();
};
let Ok(layout) = Layout::from_size_align(size, align_of::<AllocationHeader>()) else {
return ptr::null_mut();
};
let memory = if opaque.is_null() {
None
} else {
let work = unsafe { &(*opaque.cast::<MemoryContext>()).work };
match work.reserve_memory(size) {
Ok(memory) => memory,
Err(_) => return ptr::null_mut(),
}
};
#[cfg(test)]
if super::tests::reject_allocation() {
return ptr::null_mut();
}
let allocation = unsafe { alloc(layout) };
if allocation.is_null() {
return ptr::null_mut();
}
let header = allocation.cast::<AllocationHeader>();
unsafe { header.write(AllocationHeader { size, memory }) };
#[cfg(test)]
super::tests::record_allocation(size, true);
unsafe { header.add(1).cast() }
}
pub(super) unsafe extern "C" fn free(opaque: *mut c_void, address: *mut c_void) {
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
unsafe { free_inner(address) }
}))
.is_err()
{
unsafe { record_callback_panic(opaque, 0, "release") };
}
}
unsafe fn free_inner(address: *mut c_void) {
if address.is_null() {
return;
}
let header = unsafe { address.cast::<AllocationHeader>().sub(1) };
let ownership = unsafe { header.read() };
let size = ownership.size;
let layout = unsafe { Layout::from_size_align_unchecked(size, align_of::<AllocationHeader>()) };
unsafe { dealloc(header.cast(), layout) };
#[cfg(test)]
super::tests::record_allocation(size, false);
drop(ownership.memory);
}
fn require_version() -> io::Result<()> {
if zstd::zstd_safe::version_number() == QUALIFIED_VERSION {
Ok(())
} else {
Err(io::Error::other("unqualified Zstd custom-memory ABI"))
}
}
fn native_result(code: usize) -> io::Result<usize> {
if unsafe { sys::ZSTD_isError(code) } == 0 {
Ok(code)
} else {
Err(io::Error::other(zstd::zstd_safe::get_error_name(code)))
}
}
fn stream<C: WriteBuf + ?Sized>(
input: &mut InBuffer<'_>,
output: &mut OutBuffer<'_, C>,
run: impl FnOnce(*mut sys::ZSTD_inBuffer, *mut sys::ZSTD_outBuffer) -> usize,
) -> io::Result<usize> {
if input.pos() > input.src.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid Zstd input position",
));
}
let mut native_input = sys::ZSTD_inBuffer {
src: input.src.as_ptr().cast(),
size: input.src.len(),
pos: input.pos(),
};
let mut native_output = sys::ZSTD_outBuffer {
dst: output.as_mut_ptr().cast(),
size: output.capacity(),
pos: output.pos(),
};
let code = run(&mut native_input, &mut native_output);
input.set_pos(native_input.pos);
unsafe { output.set_pos(native_output.pos) };
native_result(code)
}
pub(super) struct CompressionContext {
pointer: Option<NonNull<sys::ZSTD_CCtx>>,
failed: bool,
_memory_context: Option<Box<MemoryContext>>,
}
impl CompressionContext {
pub(super) fn new(level: i32) -> io::Result<Self> {
Self::with_memory(level, None)
}
pub(super) fn new_with_work_context(
level: i32,
work: &CheckpointWorkContext,
) -> io::Result<Self> {
let memory = work
.reserve_memory(size_of::<MemoryContext>())
.map_err(io::Error::other)?;
let context = Box::new(MemoryContext {
work: work.clone(),
failed: std::sync::atomic::AtomicBool::new(false),
_memory: memory,
});
Self::with_memory(level, Some(context))
}
fn with_memory(level: i32, mut memory: Option<Box<MemoryContext>>) -> io::Result<Self> {
require_version()?;
let mut callbacks = custom_memory();
if let Some(context) = memory.as_mut() {
callbacks.opaque = (&mut **context as *mut MemoryContext).cast();
}
let pointer = NonNull::new(unsafe { ZSTD_createCCtx_advanced(callbacks) })
.ok_or_else(|| io::Error::from(io::ErrorKind::OutOfMemory))?;
let context = Self {
pointer: Some(pointer),
failed: false,
_memory_context: memory,
};
native_result(unsafe {
sys::ZSTD_CCtx_setParameter(
context.pointer(),
sys::ZSTD_cParameter::ZSTD_c_compressionLevel,
level,
)
})?;
Ok(context)
}
pub(super) fn set_pledged_src_size(&mut self, bytes: u64) -> io::Result<()> {
native_result(unsafe { sys::ZSTD_CCtx_setPledgedSrcSize(self.pointer(), bytes) })?;
Ok(())
}
fn pointer(&self) -> *mut sys::ZSTD_CCtx {
self.pointer
.expect("owned compression context is live")
.as_ptr()
}
fn release_native(&mut self) {
if let Some(pointer) = self.pointer.take() {
unsafe { sys::ZSTD_freeCCtx(pointer.as_ptr()) };
}
}
pub(super) fn close(mut self) -> io::Result<()> {
self.release_native();
if self
._memory_context
.as_ref()
.is_some_and(|context| context.failed.load(std::sync::atomic::Ordering::Acquire))
{
return Err(io::Error::other("compression memory callback failed"));
}
Ok(())
}
fn step<C: WriteBuf + ?Sized>(
&mut self,
input: &mut InBuffer<'_>,
output: &mut OutBuffer<'_, C>,
directive: sys::ZSTD_EndDirective,
) -> io::Result<usize> {
if self.failed {
return Err(io::Error::other(
"Zstd encoder requires reset after failure",
));
}
let result = stream(input, output, |input, output| {
unsafe {
match directive {
sys::ZSTD_EndDirective::ZSTD_e_continue => {
sys::ZSTD_compressStream(self.pointer(), output, input)
}
sys::ZSTD_EndDirective::ZSTD_e_flush => {
sys::ZSTD_flushStream(self.pointer(), output)
}
sys::ZSTD_EndDirective::ZSTD_e_end => {
sys::ZSTD_endStream(self.pointer(), output)
}
}
}
});
self.failed = result.is_err();
result
}
}
impl Operation for CompressionContext {
fn run<C: WriteBuf + ?Sized>(
&mut self,
input: &mut InBuffer<'_>,
output: &mut OutBuffer<'_, C>,
) -> io::Result<usize> {
self.step(input, output, sys::ZSTD_EndDirective::ZSTD_e_continue)
}
fn flush<C: WriteBuf + ?Sized>(&mut self, output: &mut OutBuffer<'_, C>) -> io::Result<usize> {
self.step(
&mut InBuffer::around(&[]),
output,
sys::ZSTD_EndDirective::ZSTD_e_flush,
)
}
fn finish<C: WriteBuf + ?Sized>(
&mut self,
output: &mut OutBuffer<'_, C>,
_finished_frame: bool,
) -> io::Result<usize> {
self.step(
&mut InBuffer::around(&[]),
output,
sys::ZSTD_EndDirective::ZSTD_e_end,
)
}
}
impl Drop for CompressionContext {
fn drop(&mut self) {
self.release_native();
}
}
pub struct DecompressionContext {
pointer: NonNull<sys::ZSTD_DCtx>,
failed: bool,
prefix: [u8; 4],
prefix_length: usize,
prefix_position: usize,
_memory_context: Option<Box<MemoryContext>>,
}
impl DecompressionContext {
pub fn new() -> io::Result<Self> {
Self::with_memory(None)
}
pub(crate) fn new_with_work_context(work: &CheckpointWorkContext) -> io::Result<Self> {
let memory = work
.reserve_memory(size_of::<MemoryContext>())
.map_err(io::Error::other)?;
let context = Box::new(MemoryContext {
work: work.clone(),
failed: std::sync::atomic::AtomicBool::new(false),
_memory: memory,
});
Self::with_memory(Some(context))
}
fn with_memory(mut memory: Option<Box<MemoryContext>>) -> io::Result<Self> {
require_version()?;
let mut callbacks = custom_memory();
if let Some(context) = memory.as_mut() {
callbacks.opaque = (&mut **context as *mut MemoryContext).cast();
}
let pointer = NonNull::new(unsafe { ZSTD_createDCtx_advanced(callbacks) })
.ok_or_else(|| io::Error::from(io::ErrorKind::OutOfMemory))?;
let context = Self {
pointer,
failed: false,
prefix: [0; 4],
prefix_length: 0,
prefix_position: 0,
_memory_context: memory,
};
native_result(unsafe { sys::ZSTD_initDStream(context.pointer.as_ptr()) })?;
Ok(context)
}
pub fn reset(&mut self) -> io::Result<()> {
native_result(unsafe {
sys::ZSTD_DCtx_reset(
self.pointer.as_ptr(),
sys::ZSTD_ResetDirective::ZSTD_reset_session_only,
)
})?;
self.failed = false;
self.prefix_length = 0;
self.prefix_position = 0;
Ok(())
}
pub fn sizeof(&self) -> usize {
unsafe { sys::ZSTD_sizeof_DCtx(self.pointer.as_ptr()) }
}
pub fn decompress_stream<C: WriteBuf + ?Sized>(
&mut self,
output: &mut OutBuffer<'_, C>,
input: &mut InBuffer<'_>,
) -> io::Result<usize> {
if self.failed {
return Err(io::Error::other(
"Zstd decoder requires reset after failure",
));
}
if input.pos() > input.src.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid Zstd input position",
));
}
if self.prefix_length < self.prefix.len() {
let count = (self.prefix.len() - self.prefix_length).min(input.src.len() - input.pos());
self.prefix[self.prefix_length..self.prefix_length + count]
.copy_from_slice(&input.src[input.pos()..input.pos() + count]);
self.prefix_length += count;
input.set_pos(input.pos() + count);
if self.prefix_length < self.prefix.len() {
return Ok(self.prefix.len() - self.prefix_length);
}
let magic = u32::from_le_bytes(self.prefix);
if magic != 0xfd2fb528 && magic & 0xfffffff0 != 0x184d2a50 {
self.failed = true;
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid Zstd frame magic",
));
}
}
if self.prefix_position < self.prefix.len() {
let mut prefix = InBuffer::around(&self.prefix);
prefix.set_pos(self.prefix_position);
let result = stream(&mut prefix, output, |input, output| {
unsafe { sys::ZSTD_decompressStream(self.pointer.as_ptr(), output, input) }
});
self.prefix_position = prefix.pos();
self.failed = result.is_err();
return result;
}
let result = stream(input, output, |input, output| {
unsafe { sys::ZSTD_decompressStream(self.pointer.as_ptr(), output, input) }
});
self.failed = result.is_err();
if matches!(result, Ok(0)) {
self.prefix_length = 0;
self.prefix_position = 0;
}
result
}
}
impl Drop for DecompressionContext {
fn drop(&mut self) {
unsafe { sys::ZSTD_freeDCtx(self.pointer.as_ptr()) };
}
}
unsafe impl Send for CompressionContext {}
unsafe impl Sync for CompressionContext {}
unsafe impl Send for DecompressionContext {}
unsafe impl Sync for DecompressionContext {}