rust_openzl/
lib.rs

1//! # OpenZL Rust Bindings
2//!
3//! Safe, ergonomic Rust bindings for OpenZL - a graph-based typed compression library
4//! optimized for structured data.
5//!
6//! ## What is OpenZL?
7//!
8//! **OpenZL is fundamentally different from generic compressors like zlib or zstd.**
9//!
10//! It's a **graph-based typed compression library** where compression graphs define
11//! *how* to compress specific data structures. This allows OpenZL to apply type-specific
12//! optimizations (delta encoding, bitpacking, transpose, etc.) that aren't possible with
13//! generic byte-stream compression.
14//!
15//! ## Quick Start
16//!
17//! ### Serial Compression (Generic Data)
18//!
19//! ```
20//! use rust_openzl::{compress_serial, decompress_serial};
21//!
22//! let data = b"Hello, OpenZL!";
23//! let compressed = compress_serial(data)?;
24//! let decompressed = decompress_serial(&compressed)?;
25//! assert_eq!(data.as_slice(), decompressed.as_slice());
26//! # Ok::<(), rust_openzl::Error>(())
27//! ```
28//!
29//! ### Numeric Compression (Type-Optimized)
30//!
31//! ```
32//! use rust_openzl::{compress_numeric, decompress_numeric};
33//!
34//! // Compress numeric arrays with specialized algorithms
35//! let data: Vec<u32> = (0..10000).collect();
36//! let compressed = compress_numeric(&data)?;
37//! let decompressed: Vec<u32> = decompress_numeric(&compressed)?;
38//! assert_eq!(data, decompressed);
39//! # Ok::<(), rust_openzl::Error>(())
40//! ```
41//!
42//! ### Graph-Based Compression
43//!
44//! ```
45//! use rust_openzl::{compress_with_graph, decompress_serial, ZstdGraph, NumericGraph};
46//!
47//! let data = b"Repeated data...".repeat(100);
48//!
49//! // Use specific compression graphs
50//! let compressed = compress_with_graph(&data, &ZstdGraph)?;
51//! let decompressed = decompress_serial(&compressed)?;
52//! # Ok::<(), rust_openzl::Error>(())
53//! ```
54//!
55//! ## Core Concepts
56//!
57//! ### Compression Graphs
58//!
59//! Compression graphs are the heart of OpenZL. They define the compression strategy:
60//!
61//! - **ZSTD**: General-purpose compression (similar to zstd)
62//! - **NUMERIC**: Optimized for numeric arrays (delta encoding, bitpacking)
63//! - **FIELD_LZ**: Field-level LZ compression for structured data
64//! - **STORE**: No compression (useful for testing)
65//!
66//! ### TypedRef and TypedBuffer
67//!
68//! - [`TypedRef`]: Borrowed reference to typed input data (with lifetime)
69//! - [`TypedBuffer`]: Owned decompression output buffer
70//!
71//! These provide type information to OpenZL, enabling type-specific optimizations.
72//!
73//! ## Architecture
74//!
75//! ```text
76//! High-level APIs (compress_numeric, etc.)
77//!        ↓
78//! Graph-based compression (compress_with_graph)
79//!        ↓
80//! TypedRef compression (compress_typed_ref)
81//!        ↓
82//! CCtx + Compressor (graph registration)
83//!        ↓
84//! OpenZL C library (via rust-openzl-sys)
85//! ```
86//!
87//! ## Examples
88//!
89//! See the `examples/` directory for complete examples:
90//!
91//! - `serial_compress.rs` - Basic serial compression
92//! - `numeric_compress.rs` - Numeric array compression with different types
93//! - `graph_compression.rs` - Using different compression graphs
94//! - `typed_compression.rs` - Advanced TypedRef usage
95//!
96//! ## Safety
97//!
98//! This crate provides safe abstractions over the unsafe FFI:
99//!
100//! - RAII wrappers with `Drop` for resource cleanup
101//! - Lifetime-checked `TypedRef` to prevent use-after-free
102//! - Type validation for numeric compression
103//! - Error handling via `Result<T, Error>`
104//!
105//! ## Performance
106//!
107//! OpenZL can achieve excellent compression ratios on structured data:
108//!
109//! - Sequential numeric data: 0.30% (400:1 ratio)
110//! - Timestamps: 2.16% (46:1 ratio)
111//! - Repetitive text: 1.96% (51:1 ratio)
112//!
113//! (Actual ratios depend on data patterns)
114
115use rust_openzl_sys as sys;
116use std::ffi::CStr;
117
118#[derive(Debug, thiserror::Error)]
119pub enum Error {
120    #[error("OpenZL error: {code} ({name}){context}")]
121    Report {
122        code: i32,
123        name: String,
124        context: String,
125    },
126}
127
128// ============================================================================
129// Helper functions
130// ============================================================================
131
132/// Returns the maximum compressed size for a given source size
133fn compress_bound(src_size: usize) -> usize {
134    unsafe { sys::openzl_compress_bound(src_size) }
135}
136
137/// Convert a ZL_Report to a Rust Error
138fn report_to_error(r: sys::ZL_Report) -> Error {
139    let code = sys::report_code(r);
140    let name = unsafe { CStr::from_ptr(sys::openzl_error_code_to_string(code)) }
141        .to_string_lossy()
142        .into_owned();
143    Error::Report {
144        code,
145        name,
146        context: String::new(),
147    }
148}
149
150/// OpenZL warning (non-fatal issue during compression/decompression)
151#[derive(Debug, Clone)]
152pub struct Warning {
153    pub code: i32,
154    pub name: String,
155}
156
157/// Opaque GraphID for identifying compression graphs
158#[derive(Debug, Copy, Clone)]
159pub struct GraphId(sys::ZL_GraphID);
160
161impl GraphId {
162    /// Check if this GraphID is valid
163    pub fn is_valid(&self) -> bool {
164        unsafe { sys::ZL_GraphID_isValid(self.0) != 0 }
165    }
166
167    #[allow(dead_code)] // Used for custom graph registration (Step 12)
168    pub(crate) fn as_raw(&self) -> sys::ZL_GraphID {
169        self.0
170    }
171
172    #[allow(dead_code)] // Used for custom graph registration (Step 12)
173    pub(crate) fn from_raw(id: sys::ZL_GraphID) -> Self {
174        GraphId(id)
175    }
176}
177
178impl PartialEq for GraphId {
179    fn eq(&self, other: &Self) -> bool {
180        self.0.gid == other.0.gid
181    }
182}
183
184impl Eq for GraphId {}
185
186/// Standard compression graphs provided by OpenZL
187pub mod graphs {
188    use super::*;
189
190    const fn make_graph_id(id: u32) -> GraphId {
191        GraphId(sys::ZL_GraphID { gid: id })
192    }
193
194    /// No compression - stores data as-is
195    pub const STORE: GraphId = make_graph_id(sys::ZL_StandardGraphID::ZL_StandardGraphID_store as u32);
196
197    /// ZSTD compression (general purpose)
198    pub const ZSTD: GraphId = make_graph_id(sys::ZL_StandardGraphID::ZL_StandardGraphID_zstd as u32);
199
200    /// Optimized for numeric data
201    pub const NUMERIC: GraphId = make_graph_id(sys::ZL_StandardGraphID::ZL_StandardGraphID_select_numeric as u32);
202
203    /// Field-level LZ compression
204    pub const FIELD_LZ: GraphId = make_graph_id(sys::ZL_StandardGraphID::ZL_StandardGraphID_field_lz as u32);
205
206    /// FSE entropy encoding
207    pub const FSE: GraphId = make_graph_id(sys::ZL_StandardGraphID::ZL_StandardGraphID_fse as u32);
208
209    /// Huffman entropy encoding
210    pub const HUFFMAN: GraphId = make_graph_id(sys::ZL_StandardGraphID::ZL_StandardGraphID_huffman as u32);
211
212    /// Combined entropy encoding (FSE/Huffman selection)
213    pub const ENTROPY: GraphId = make_graph_id(sys::ZL_StandardGraphID::ZL_StandardGraphID_entropy as u32);
214
215    /// Bitpacking compression
216    pub const BITPACK: GraphId = make_graph_id(sys::ZL_StandardGraphID::ZL_StandardGraphID_bitpack as u32);
217
218    /// Constant value compression
219    pub const CONSTANT: GraphId = make_graph_id(sys::ZL_StandardGraphID::ZL_StandardGraphID_constant as u32);
220}
221
222/// Compression graph builder and manager
223///
224/// Compressor is used to register and manage compression graphs, which define
225/// HOW to compress data. This is the heart of OpenZL's typed compression.
226pub struct Compressor(*mut sys::ZL_Compressor);
227
228impl Compressor {
229    /// Create a new Compressor for graph registration
230    pub fn new() -> Self {
231        let ptr = unsafe { sys::ZL_Compressor_create() };
232        assert!(!ptr.is_null(), "ZL_Compressor_create returned null");
233        Compressor(ptr)
234    }
235
236    /// Set a global compression parameter
237    pub fn set_parameter(&mut self, param: sys::ZL_CParam, value: i32) -> Result<(), Error> {
238        let r = unsafe { sys::ZL_Compressor_setParameter(self.0, param, value) };
239        if sys::report_is_error(r) {
240            let code = sys::report_code(r);
241            let name = unsafe { CStr::from_ptr(sys::openzl_error_code_to_string(code)) }
242                .to_string_lossy()
243                .into_owned();
244            return Err(Error::Report {
245                code,
246                name,
247                context: String::new(),
248            });
249        }
250        Ok(())
251    }
252
253    /// Get warnings generated during graph construction/validation
254    pub fn warnings(&self) -> Vec<Warning> {
255        let arr = unsafe { sys::ZL_Compressor_getWarnings(self.0) };
256        let slice = unsafe { std::slice::from_raw_parts(arr.errors, arr.size) };
257        slice
258            .iter()
259            .map(|e| {
260                let code = unsafe { sys::openzl_error_get_code(e) };
261                let name = unsafe { CStr::from_ptr(sys::openzl_error_get_name(e)) }
262                    .to_string_lossy()
263                    .into_owned();
264                Warning { code, name }
265            })
266            .collect()
267    }
268
269    pub(crate) fn as_ptr(&self) -> *const sys::ZL_Compressor {
270        self.0 as *const _
271    }
272
273    pub(crate) fn as_mut_ptr(&mut self) -> *mut sys::ZL_Compressor {
274        self.0
275    }
276}
277
278impl Drop for Compressor {
279    fn drop(&mut self) {
280        unsafe { sys::ZL_Compressor_free(self.0) }
281    }
282}
283
284impl Default for Compressor {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290// ============================================================================
291// Graph Function API
292// ============================================================================
293
294/// Trait for defining compression graphs.
295///
296/// Implementors define how to build a compression graph by registering
297/// nodes and edges with the Compressor.
298pub trait GraphFn {
299    /// Build the compression graph using the provided Compressor.
300    ///
301    /// Returns the starting GraphID for this graph.
302    fn build_graph(&self, compressor: &mut Compressor) -> GraphId;
303}
304
305/// Standard graph: ZSTD compression (general purpose)
306pub struct ZstdGraph;
307
308impl GraphFn for ZstdGraph {
309    fn build_graph(&self, _compressor: &mut Compressor) -> GraphId {
310        graphs::ZSTD
311    }
312}
313
314/// Standard graph: Numeric compression (optimized for numeric data)
315pub struct NumericGraph;
316
317impl GraphFn for NumericGraph {
318    fn build_graph(&self, _compressor: &mut Compressor) -> GraphId {
319        graphs::NUMERIC
320    }
321}
322
323/// Standard graph: Store (no compression, useful for testing)
324pub struct StoreGraph;
325
326impl GraphFn for StoreGraph {
327    fn build_graph(&self, _compressor: &mut Compressor) -> GraphId {
328        graphs::STORE
329    }
330}
331
332/// Standard graph: Field-level LZ compression
333pub struct FieldLzGraph;
334
335impl GraphFn for FieldLzGraph {
336    fn build_graph(&self, _compressor: &mut Compressor) -> GraphId {
337        graphs::FIELD_LZ
338    }
339}
340
341// Placeholder trampoline for custom graph functions (used in Step 12)
342#[allow(dead_code)]
343unsafe extern "C" fn graph_fn_trampoline(_compressor: *mut sys::ZL_Compressor) -> sys::ZL_GraphID {
344    // This is a placeholder for custom graph registration (Step 12).
345    // For now, standard graphs use dedicated callbacks below.
346    // Custom graphs will require thread-local storage or user data passing.
347    graphs::ZSTD.0
348}
349
350/// Compress data using a graph function.
351///
352/// This is a stateless compression function that uses the provided GraphFn
353/// to define the compression strategy.
354pub fn compress_with_graph<G: GraphFn>(src: &[u8], graph: &G) -> Result<Vec<u8>, Error> {
355    // Allocate output buffer (use compress_bound to estimate size)
356    let max_size = compress_bound(src.len());
357    let mut dst = vec![0u8; max_size];
358
359    // Create a temporary compressor to get the GraphID
360    let mut compressor = Compressor::new();
361    let graph_id = graph.build_graph(&mut compressor);
362
363    // Use ZL_compress_usingGraphFn with a C callback that selects the graph
364    // The callback will also set required parameters like format version
365    let graph_fn = make_graph_selector_fn(graph_id);
366
367    let r = unsafe {
368        sys::ZL_compress_usingGraphFn(
369            dst.as_mut_ptr() as *mut _,
370            dst.len(),
371            src.as_ptr() as *const _,
372            src.len(),
373            graph_fn,
374        )
375    };
376
377    if sys::report_is_error(r) {
378        return Err(report_to_error(r));
379    }
380
381    let compressed_size = sys::report_value(r);
382    dst.truncate(compressed_size);
383    Ok(dst)
384}
385
386// Helper to create a C callback that selects a specific GraphID
387fn make_graph_selector_fn(graph_id: GraphId) -> sys::ZL_GraphFn {
388    // For standard graphs, we can use dedicated callbacks
389    match graph_id.0.gid {
390        id if id == graphs::ZSTD.0.gid => Some(zstd_graph_callback),
391        id if id == graphs::NUMERIC.0.gid => Some(numeric_graph_callback),
392        id if id == graphs::STORE.0.gid => Some(store_graph_callback),
393        id if id == graphs::FIELD_LZ.0.gid => Some(field_lz_graph_callback),
394        _ => {
395            // For custom graphs, we'd need a different mechanism
396            // For now, fall back to ZSTD
397            Some(zstd_graph_callback)
398        }
399    }
400}
401
402// Dedicated C callbacks for standard graphs
403unsafe extern "C" fn zstd_graph_callback(compressor: *mut sys::ZL_Compressor) -> sys::ZL_GraphID {
404    // Set format version (required by OpenZL)
405    sys::ZL_Compressor_setParameter(compressor, sys::ZL_CParam::ZL_CParam_formatVersion, 21);
406    graphs::ZSTD.0
407}
408
409unsafe extern "C" fn numeric_graph_callback(compressor: *mut sys::ZL_Compressor) -> sys::ZL_GraphID {
410    // Set format version (required by OpenZL)
411    sys::ZL_Compressor_setParameter(compressor, sys::ZL_CParam::ZL_CParam_formatVersion, 21);
412    graphs::NUMERIC.0
413}
414
415unsafe extern "C" fn store_graph_callback(compressor: *mut sys::ZL_Compressor) -> sys::ZL_GraphID {
416    // Set format version (required by OpenZL)
417    sys::ZL_Compressor_setParameter(compressor, sys::ZL_CParam::ZL_CParam_formatVersion, 21);
418    graphs::STORE.0
419}
420
421unsafe extern "C" fn field_lz_graph_callback(compressor: *mut sys::ZL_Compressor) -> sys::ZL_GraphID {
422    // Set format version (required by OpenZL)
423    sys::ZL_Compressor_setParameter(compressor, sys::ZL_CParam::ZL_CParam_formatVersion, 21);
424    graphs::FIELD_LZ.0
425}
426
427// ============================================================================
428// TypedRef and TypedBuffer
429// ============================================================================
430
431/// Safe wrapper around ZL_TypedRef for typed input data.
432///
433/// IMPORTANT: TypedRef borrows the input data. The borrowed data must remain
434/// valid for the lifetime of the TypedRef and through any compression call
435/// that uses it.
436pub struct TypedRef<'a> {
437    ptr: *mut sys::ZL_TypedRef,
438    _marker: std::marker::PhantomData<&'a [u8]>,
439}
440
441impl<'a> TypedRef<'a> {
442    /// Create a TypedRef for serial (untyped byte array) data
443    pub fn serial(data: &'a [u8]) -> Self {
444        let ptr = unsafe {
445            sys::ZL_TypedRef_createSerial(data.as_ptr() as *const _, data.len())
446        };
447        assert!(!ptr.is_null(), "ZL_TypedRef_createSerial returned null");
448        TypedRef {
449            ptr,
450            _marker: std::marker::PhantomData,
451        }
452    }
453
454    /// Create a TypedRef for numeric data.
455    ///
456    /// T must have size 1, 2, 4, or 8 bytes (u8, u16, u32, u64, i8, i16, i32, i64, f32, f64)
457    pub fn numeric<T: Copy>(data: &'a [T]) -> Result<Self, Error> {
458        let width = std::mem::size_of::<T>();
459        if !matches!(width, 1 | 2 | 4 | 8) {
460            return Err(Error::Report {
461                code: -1,
462                name: "Invalid numeric type".into(),
463                context: format!("\nElement size must be 1, 2, 4, or 8 bytes, got {width}"),
464            });
465        }
466        let ptr = unsafe {
467            sys::ZL_TypedRef_createNumeric(
468                data.as_ptr() as *const _,
469                width,
470                data.len(),
471            )
472        };
473        assert!(!ptr.is_null(), "ZL_TypedRef_createNumeric returned null");
474        Ok(TypedRef {
475            ptr,
476            _marker: std::marker::PhantomData,
477        })
478    }
479
480    /// Create a TypedRef for string data (flat format with lengths array)
481    ///
482    /// - `flat`: concatenated string bytes
483    /// - `lens`: array of string lengths (u32)
484    pub fn strings(flat: &'a [u8], lens: &'a [u32]) -> Self {
485        let ptr = unsafe {
486            sys::ZL_TypedRef_createString(
487                flat.as_ptr() as *const _,
488                flat.len(),
489                lens.as_ptr(),
490                lens.len(),
491            )
492        };
493        assert!(!ptr.is_null(), "ZL_TypedRef_createString returned null");
494        TypedRef {
495            ptr,
496            _marker: std::marker::PhantomData,
497        }
498    }
499
500    /// Create a TypedRef for struct data (concatenated fields)
501    ///
502    /// - `bytes`: flattened struct data
503    /// - `width`: size of each struct element in bytes
504    /// - `count`: number of struct elements
505    pub fn structs(bytes: &'a [u8], width: usize, count: usize) -> Result<Self, Error> {
506        if width == 0 || count == 0 {
507            return Err(Error::Report {
508                code: -1,
509                name: "Invalid struct parameters".into(),
510                context: "\nWidth and count must be non-zero".into(),
511            });
512        }
513        if bytes.len() != width * count {
514            return Err(Error::Report {
515                code: -1,
516                name: "Invalid struct buffer size".into(),
517                context: format!("\nExpected {} bytes (width={width} * count={count}), got {}", width * count, bytes.len()),
518            });
519        }
520        let ptr = unsafe {
521            sys::ZL_TypedRef_createStruct(
522                bytes.as_ptr() as *const _,
523                width,
524                count,
525            )
526        };
527        assert!(!ptr.is_null(), "ZL_TypedRef_createStruct returned null");
528        Ok(TypedRef {
529            ptr,
530            _marker: std::marker::PhantomData,
531        })
532    }
533
534    pub(crate) fn as_ptr(&self) -> *const sys::ZL_TypedRef {
535        self.ptr as *const _
536    }
537}
538
539impl Drop for TypedRef<'_> {
540    fn drop(&mut self) {
541        unsafe { sys::ZL_TypedRef_free(self.ptr) }
542    }
543}
544
545/// Safe wrapper around ZL_TypedBuffer for typed decompression output.
546///
547/// TypedBuffer owns its internal buffer and frees it on Drop.
548pub struct TypedBuffer {
549    ptr: *mut sys::ZL_TypedBuffer,
550}
551
552impl TypedBuffer {
553    /// Create a new TypedBuffer for receiving decompressed data
554    pub fn new() -> Self {
555        let ptr = unsafe { sys::ZL_TypedBuffer_create() };
556        assert!(!ptr.is_null(), "ZL_TypedBuffer_create returned null");
557        TypedBuffer { ptr }
558    }
559
560    /// Get the data type of this buffer
561    pub fn data_type(&self) -> sys::ZL_Type {
562        unsafe { sys::ZL_TypedBuffer_type(self.ptr) }
563    }
564
565    /// Get the size of the buffer in bytes
566    pub fn byte_size(&self) -> usize {
567        unsafe { sys::ZL_TypedBuffer_byteSize(self.ptr) }
568    }
569
570    /// Get the number of elements
571    pub fn num_elts(&self) -> usize {
572        unsafe { sys::ZL_TypedBuffer_numElts(self.ptr) }
573    }
574
575    /// Get the element width in bytes (for struct/numeric types)
576    pub fn elt_width(&self) -> usize {
577        unsafe { sys::ZL_TypedBuffer_eltWidth(self.ptr) }
578    }
579
580    /// Get read-only access to the buffer as bytes
581    pub fn as_bytes(&self) -> &[u8] {
582        let ptr = unsafe { sys::ZL_TypedBuffer_rPtr(self.ptr) };
583        let len = self.byte_size();
584        if ptr.is_null() || len == 0 {
585            &[]
586        } else {
587            unsafe { std::slice::from_raw_parts(ptr as *const u8, len) }
588        }
589    }
590
591    /// Get read-only access to numeric data as a typed slice.
592    ///
593    /// Returns None if:
594    /// - The data type is not numeric
595    /// - The element width doesn't match T's size
596    /// - The buffer is not properly aligned for T
597    pub fn as_numeric<T: Copy>(&self) -> Option<&[T]> {
598        // Check if type is numeric
599        if self.data_type() != sys::ZL_Type::ZL_Type_numeric {
600            return None;
601        }
602
603        let width = std::mem::size_of::<T>();
604        if self.elt_width() != width {
605            return None;
606        }
607
608        let ptr = unsafe { sys::ZL_TypedBuffer_rPtr(self.ptr) };
609        if ptr.is_null() {
610            return None;
611        }
612
613        // Check alignment
614        if (ptr as usize) % std::mem::align_of::<T>() != 0 {
615            return None;
616        }
617
618        let len = self.num_elts();
619        if len == 0 {
620            return Some(&[]);
621        }
622
623        Some(unsafe { std::slice::from_raw_parts(ptr as *const T, len) })
624    }
625
626    /// Get the string lengths array (for string type)
627    ///
628    /// Returns None if the data type is not string
629    pub fn string_lens(&self) -> Option<&[u32]> {
630        if self.data_type() != sys::ZL_Type::ZL_Type_string {
631            return None;
632        }
633
634        let ptr = unsafe { sys::ZL_TypedBuffer_rStringLens(self.ptr) };
635        if ptr.is_null() {
636            return None;
637        }
638
639        let len = self.num_elts();
640        if len == 0 {
641            return Some(&[]);
642        }
643
644        Some(unsafe { std::slice::from_raw_parts(ptr, len) })
645    }
646
647    pub(crate) fn as_mut_ptr(&mut self) -> *mut sys::ZL_TypedBuffer {
648        self.ptr
649    }
650}
651
652impl Drop for TypedBuffer {
653    fn drop(&mut self) {
654        unsafe { sys::ZL_TypedBuffer_free(self.ptr) }
655    }
656}
657
658impl Default for TypedBuffer {
659    fn default() -> Self {
660        Self::new()
661    }
662}
663
664fn error_from_report_with_ctx(code: i32, name: String, ctx_str: Option<&CStr>) -> Error {
665    let context = match ctx_str.and_then(|s| s.to_str().ok()) {
666        Some(s) if !s.is_empty() => format!("\n{s}"),
667        _ => String::new(),
668    };
669    Error::Report { code, name, context }
670}
671
672pub struct CCtx(*mut sys::ZL_CCtx);
673impl CCtx {
674    pub fn new() -> Self {
675        let ptr = unsafe { sys::ZL_CCtx_create() };
676        assert!(!ptr.is_null(), "ZL_CCtx_create returned null");
677        CCtx(ptr)
678    }
679    pub fn set_parameter(&mut self, p: sys::ZL_CParam, v: i32) -> Result<(), Error> {
680        let r = unsafe { sys::ZL_CCtx_setParameter(self.0, p, v) };
681        if sys::report_is_error(r) {
682            let code = sys::report_code(r);
683            let name = unsafe { CStr::from_ptr(sys::openzl_error_code_to_string(code)) }
684                .to_string_lossy()
685                .into_owned();
686            let ctx = unsafe { CStr::from_ptr(sys::openzl_cctx_error_context(self.0, r)) };
687            return Err(error_from_report_with_ctx(code, name, Some(&ctx)));
688        }
689        Ok(())
690    }
691
692    /// Reference a Compressor for graph-based typed compression.
693    ///
694    /// This enables TypedRef compression by associating the CCtx with a Compressor
695    /// that has registered compression graphs.
696    ///
697    /// IMPORTANT: The Compressor must remain valid for the duration of its usage.
698    /// The Compressor must be validated before being referenced.
699    pub fn ref_compressor(&mut self, compressor: &Compressor) -> Result<(), Error> {
700        let r = unsafe { sys::ZL_CCtx_refCompressor(self.0, compressor.as_ptr()) };
701        if sys::report_is_error(r) {
702            let code = sys::report_code(r);
703            let name = unsafe { CStr::from_ptr(sys::openzl_error_code_to_string(code)) }
704                .to_string_lossy()
705                .into_owned();
706            let ctx = unsafe { CStr::from_ptr(sys::openzl_cctx_error_context(self.0, r)) };
707            return Err(error_from_report_with_ctx(code, name, Some(&ctx)));
708        }
709        Ok(())
710    }
711
712    /// Get warnings generated during compression operations
713    pub fn warnings(&self) -> Vec<Warning> {
714        let arr = unsafe { sys::openzl_cctx_get_warnings(self.0) };
715        let slice = unsafe { std::slice::from_raw_parts(arr.errors, arr.size) };
716        slice
717            .iter()
718            .map(|e| {
719                let code = unsafe { sys::openzl_error_get_code(e) };
720                let name = unsafe { CStr::from_ptr(sys::openzl_error_get_name(e)) }
721                    .to_string_lossy()
722                    .into_owned();
723                Warning { code, name }
724            })
725            .collect()
726    }
727
728    /// Compress a single typed input
729    pub fn compress_typed_ref(&mut self, input: &TypedRef, dst: &mut [u8]) -> Result<usize, Error> {
730        let r = unsafe {
731            sys::ZL_CCtx_compressTypedRef(
732                self.0,
733                dst.as_mut_ptr() as *mut _,
734                dst.len(),
735                input.as_ptr(),
736            )
737        };
738        if sys::report_is_error(r) {
739            let code = sys::report_code(r);
740            let name = unsafe { CStr::from_ptr(sys::openzl_error_code_to_string(code)) }
741                .to_string_lossy()
742                .into_owned();
743            let ctx = unsafe { CStr::from_ptr(sys::openzl_cctx_error_context(self.0, r)) };
744            return Err(error_from_report_with_ctx(code, name, Some(&ctx)));
745        }
746        Ok(sys::report_value(r))
747    }
748
749    /// Compress multiple typed inputs into a single frame
750    pub fn compress_multi_typed_ref(&mut self, inputs: &[&TypedRef], dst: &mut [u8]) -> Result<usize, Error> {
751        let mut ptrs: Vec<*const sys::ZL_TypedRef> = inputs.iter().map(|tr| tr.as_ptr()).collect();
752        let r = unsafe {
753            sys::ZL_CCtx_compressMultiTypedRef(
754                self.0,
755                dst.as_mut_ptr() as *mut _,
756                dst.len(),
757                ptrs.as_mut_ptr() as *mut _,
758                ptrs.len(),
759            )
760        };
761        if sys::report_is_error(r) {
762            let code = sys::report_code(r);
763            let name = unsafe { CStr::from_ptr(sys::openzl_error_code_to_string(code)) }
764                .to_string_lossy()
765                .into_owned();
766            let ctx = unsafe { CStr::from_ptr(sys::openzl_cctx_error_context(self.0, r)) };
767            return Err(error_from_report_with_ctx(code, name, Some(&ctx)));
768        }
769        Ok(sys::report_value(r))
770    }
771}
772impl Drop for CCtx { fn drop(&mut self) { unsafe { sys::ZL_CCtx_free(self.0) } } }
773
774pub struct DCtx(*mut sys::ZL_DCtx);
775impl DCtx {
776    pub fn new() -> Self {
777        let p = unsafe { sys::ZL_DCtx_create() };
778        assert!(!p.is_null());
779        DCtx(p)
780    }
781
782    /// Get warnings generated during decompression operations
783    pub fn warnings(&self) -> Vec<Warning> {
784        let arr = unsafe { sys::openzl_dctx_get_warnings(self.0) };
785        let slice = unsafe { std::slice::from_raw_parts(arr.errors, arr.size) };
786        slice
787            .iter()
788            .map(|e| {
789                let code = unsafe { sys::openzl_error_get_code(e) };
790                let name = unsafe { CStr::from_ptr(sys::openzl_error_get_name(e)) }
791                    .to_string_lossy()
792                    .into_owned();
793                Warning { code, name }
794            })
795            .collect()
796    }
797
798    /// Decompress into a TypedBuffer (auto-sized, single output)
799    pub fn decompress_typed_buffer(&mut self, compressed: &[u8], output: &mut TypedBuffer) -> Result<usize, Error> {
800        let r = unsafe {
801            sys::ZL_DCtx_decompressTBuffer(
802                self.0,
803                output.as_mut_ptr(),
804                compressed.as_ptr() as *const _,
805                compressed.len(),
806            )
807        };
808        if sys::report_is_error(r) {
809            let code = sys::report_code(r);
810            let name = unsafe { CStr::from_ptr(sys::openzl_error_code_to_string(code)) }
811                .to_string_lossy()
812                .into_owned();
813            let ctx = unsafe { CStr::from_ptr(sys::openzl_dctx_error_context(self.0, r)) };
814            return Err(error_from_report_with_ctx(code, name, Some(&ctx)));
815        }
816        Ok(sys::report_value(r))
817    }
818
819    /// Decompress into multiple TypedBuffers (multi-output frame)
820    pub fn decompress_multi_typed_buffer(&mut self, compressed: &[u8], outputs: &mut [&mut TypedBuffer]) -> Result<usize, Error> {
821        let mut ptrs: Vec<*mut sys::ZL_TypedBuffer> = outputs.iter_mut().map(|tb| tb.as_mut_ptr()).collect();
822        let r = unsafe {
823            sys::ZL_DCtx_decompressMultiTBuffer(
824                self.0,
825                ptrs.as_mut_ptr(),
826                ptrs.len(),
827                compressed.as_ptr() as *const _,
828                compressed.len(),
829            )
830        };
831        if sys::report_is_error(r) {
832            let code = sys::report_code(r);
833            let name = unsafe { CStr::from_ptr(sys::openzl_error_code_to_string(code)) }
834                .to_string_lossy()
835                .into_owned();
836            let ctx = unsafe { CStr::from_ptr(sys::openzl_dctx_error_context(self.0, r)) };
837            return Err(error_from_report_with_ctx(code, name, Some(&ctx)));
838        }
839        Ok(sys::report_value(r))
840    }
841}
842impl Drop for DCtx { fn drop(&mut self) { unsafe { sys::ZL_DCtx_free(self.0) } } }
843
844pub fn compress_serial(src: &[u8]) -> Result<Vec<u8>, Error> {
845    let mut cctx = CCtx::new();
846    // Set format version (required by OpenZL). Use latest version (21).
847    cctx.set_parameter(sys::ZL_CParam::ZL_CParam_formatVersion, 21)?;
848    // Capacity upper bound
849    let cap = unsafe { sys::openzl_compress_bound(src.len()) };
850    let mut dst = vec![0u8; cap];
851    let r = unsafe {
852        sys::ZL_CCtx_compress(
853            cctx.0,
854            dst.as_mut_ptr() as *mut _,
855            dst.len(),
856            src.as_ptr() as *const _,
857            src.len(),
858        )
859    };
860    if sys::report_is_error(r) {
861        let code = sys::report_code(r);
862        let name = unsafe { CStr::from_ptr(sys::openzl_error_code_to_string(code)) }
863            .to_string_lossy()
864            .into_owned();
865        let ctx = unsafe { CStr::from_ptr(sys::openzl_cctx_error_context(cctx.0, r)) };
866        return Err(error_from_report_with_ctx(code, name, Some(&ctx)));
867    }
868    let n = sys::report_value(r) as usize;
869    dst.truncate(n);
870    Ok(dst)
871}
872
873/// Compress a single TypedRef and return the compressed bytes.
874///
875/// This uses ZSTD graph by default. For better compression on specific data types,
876/// consider using type-specific compression functions or creating a custom Compressor
877/// with appropriate graphs.
878pub fn compress_typed_ref(input: &TypedRef) -> Result<Vec<u8>, Error> {
879    let mut cctx = CCtx::new();
880    // Set format version (required by OpenZL). Use latest version (21).
881    cctx.set_parameter(sys::ZL_CParam::ZL_CParam_formatVersion, 21)?;
882
883    // Create a Compressor with ZSTD graph (reasonable default for general use)
884    let mut compressor = Compressor::new();
885    compressor.set_parameter(sys::ZL_CParam::ZL_CParam_formatVersion, 21)?;
886
887    // Register ZSTD as the starting graph using the graph callback approach
888    let r = unsafe {
889        sys::ZL_Compressor_initUsingGraphFn(compressor.as_mut_ptr(), Some(zstd_graph_callback))
890    };
891    if sys::report_is_error(r) {
892        return Err(report_to_error(r));
893    }
894
895    // Reference the compressor in the CCtx
896    cctx.ref_compressor(&compressor)?;
897
898    // Estimate capacity based on input data (this is conservative)
899    // For TypedRef we can't easily get the size, so use a large upper bound
900    let cap = 1024 * 1024; // 1MB default, adjust as needed
901    let mut dst = vec![0u8; cap];
902
903    let n = cctx.compress_typed_ref(input, &mut dst)?;
904    dst.truncate(n);
905    Ok(dst)
906}
907
908/// Compress multiple TypedRefs into a single frame.
909///
910/// This uses ZSTD graph by default. For better compression on specific data types,
911/// consider creating a custom Compressor with appropriate graphs.
912pub fn compress_multi_typed_ref(inputs: &[&TypedRef]) -> Result<Vec<u8>, Error> {
913    let mut cctx = CCtx::new();
914    // Set format version (required by OpenZL). Use latest version (21).
915    cctx.set_parameter(sys::ZL_CParam::ZL_CParam_formatVersion, 21)?;
916
917    // Create a Compressor with ZSTD graph
918    let mut compressor = Compressor::new();
919    compressor.set_parameter(sys::ZL_CParam::ZL_CParam_formatVersion, 21)?;
920
921    let r = unsafe {
922        sys::ZL_Compressor_initUsingGraphFn(compressor.as_mut_ptr(), Some(zstd_graph_callback))
923    };
924    if sys::report_is_error(r) {
925        return Err(report_to_error(r));
926    }
927
928    cctx.ref_compressor(&compressor)?;
929
930    // Estimate capacity (conservative)
931    let cap = 1024 * 1024; // 1MB default
932    let mut dst = vec![0u8; cap];
933
934    let n = cctx.compress_multi_typed_ref(inputs, &mut dst)?;
935    dst.truncate(n);
936    Ok(dst)
937}
938
939pub fn decompress_serial(src: &[u8]) -> Result<Vec<u8>, Error> {
940    // Query decompressed size first
941    let rsize = unsafe { sys::ZL_getDecompressedSize(src.as_ptr() as *const _, src.len()) };
942    if sys::report_is_error(rsize) {
943        let code = sys::report_code(rsize);
944        let name = unsafe { CStr::from_ptr(sys::openzl_error_code_to_string(code)) }
945            .to_string_lossy()
946            .into_owned();
947        return Err(error_from_report_with_ctx(code, name, None));
948    }
949    let mut dst = vec![0u8; sys::report_value(rsize) as usize];
950    let r = unsafe { sys::ZL_decompress(dst.as_mut_ptr() as *mut _, dst.len(), src.as_ptr() as *const _, src.len()) };
951    if sys::report_is_error(r) {
952        let code = sys::report_code(r);
953        let name = unsafe { CStr::from_ptr(sys::openzl_error_code_to_string(code)) }
954            .to_string_lossy()
955            .into_owned();
956        // No context without DCtx; use empty
957        return Err(error_from_report_with_ctx(code, name, None));
958    }
959    let n = sys::report_value(r) as usize;
960    dst.truncate(n);
961    Ok(dst)
962}
963
964/// Decompress compressed data into a TypedBuffer (auto-allocates and determines type)
965pub fn decompress_typed_buffer(compressed: &[u8]) -> Result<TypedBuffer, Error> {
966    let mut dctx = DCtx::new();
967    let mut output = TypedBuffer::new();
968    dctx.decompress_typed_buffer(compressed, &mut output)?;
969    Ok(output)
970}
971
972// ============================================================================
973// High-level ergonomic APIs (Step 9 - MVP completion)
974// ============================================================================
975
976/// Compress numeric data using the NUMERIC graph (optimized for numeric arrays).
977///
978/// Supports all numeric types: u8, u16, u32, u64, i8, i16, i32, i64, f32, f64.
979///
980/// # Example
981/// ```no_run
982/// # use rust_openzl::compress_numeric;
983/// let data: Vec<u32> = (0..10000).collect();
984/// let compressed = compress_numeric(&data).expect("compression failed");
985/// ```
986pub fn compress_numeric<T: Copy>(data: &[T]) -> Result<Vec<u8>, Error> {
987    // Validate that T is a supported numeric type (1, 2, 4, or 8 bytes)
988    let width = std::mem::size_of::<T>();
989    if !matches!(width, 1 | 2 | 4 | 8) {
990        return Err(Error::Report {
991            code: -1,
992            name: "Invalid numeric type".into(),
993            context: format!("\nElement size must be 1, 2, 4, or 8 bytes, got {width}"),
994        });
995    }
996
997    // Create TypedRef for numeric data
998    let tref = TypedRef::numeric(data)?;
999
1000    // Create CCtx and Compressor with NUMERIC graph
1001    let mut cctx = CCtx::new();
1002    cctx.set_parameter(sys::ZL_CParam::ZL_CParam_formatVersion, 21)?;
1003
1004    let mut compressor = Compressor::new();
1005    compressor.set_parameter(sys::ZL_CParam::ZL_CParam_formatVersion, 21)?;
1006
1007    // Initialize with NUMERIC graph
1008    let r = unsafe {
1009        sys::ZL_Compressor_initUsingGraphFn(compressor.as_mut_ptr(), Some(numeric_graph_callback))
1010    };
1011    if sys::report_is_error(r) {
1012        return Err(report_to_error(r));
1013    }
1014
1015    cctx.ref_compressor(&compressor)?;
1016
1017    // Compress
1018    let cap = compress_bound(data.len() * width);
1019    let mut dst = vec![0u8; cap];
1020
1021    let n = cctx.compress_typed_ref(&tref, &mut dst)?;
1022    dst.truncate(n);
1023    Ok(dst)
1024}
1025
1026/// Decompress numeric data that was compressed with `compress_numeric`.
1027///
1028/// Returns a Vec<T> containing the decompressed numeric values.
1029///
1030/// # Example
1031/// ```no_run
1032/// # use rust_openzl::{compress_numeric, decompress_numeric};
1033/// let data: Vec<u32> = (0..10000).collect();
1034/// let compressed = compress_numeric(&data).expect("compression failed");
1035/// let decompressed: Vec<u32> = decompress_numeric(&compressed).expect("decompression failed");
1036/// assert_eq!(data, decompressed);
1037/// ```
1038pub fn decompress_numeric<T: Copy>(compressed: &[u8]) -> Result<Vec<T>, Error> {
1039    let width = std::mem::size_of::<T>();
1040    if !matches!(width, 1 | 2 | 4 | 8) {
1041        return Err(Error::Report {
1042            code: -1,
1043            name: "Invalid numeric type".into(),
1044            context: format!("\nElement size must be 1, 2, 4, or 8 bytes, got {width}"),
1045        });
1046    }
1047
1048    // Decompress into TypedBuffer
1049    let tbuf = decompress_typed_buffer(compressed)?;
1050
1051    // Verify it's numeric type
1052    if tbuf.data_type() != sys::ZL_Type::ZL_Type_numeric {
1053        return Err(Error::Report {
1054            code: -1,
1055            name: "Type mismatch".into(),
1056            context: format!("\nExpected numeric type, got {:?}", tbuf.data_type()),
1057        });
1058    }
1059
1060    // Verify element width matches T
1061    if tbuf.elt_width() != width {
1062        return Err(Error::Report {
1063            code: -1,
1064            name: "Width mismatch".into(),
1065            context: format!(
1066                "\nExpected element width {}, got {}",
1067                width,
1068                tbuf.elt_width()
1069            ),
1070        });
1071    }
1072
1073    // Extract numeric data
1074    let slice = tbuf.as_numeric::<T>().ok_or_else(|| Error::Report {
1075        code: -1,
1076        name: "Failed to extract numeric data".into(),
1077        context: "\nAlignment or type mismatch".into(),
1078    })?;
1079
1080    Ok(slice.to_vec())
1081}