Skip to main content

blosc2_pure_rs/
codecs.rs

1//! Compression codec dispatch for Blosc2 blocks.
2//!
3//! Provides a uniform interface over the bundled codecs — BloscLZ, LZ4,
4//! LZ4HC, Zlib and Zstd — together with optional zstd/LZ4 dictionary
5//! variants and registries for plugin/user-defined codecs (IDs >= 32).
6//!
7//! The actual BloscLZ implementation lives in the [`blosclz`] sub-module;
8//! the other codecs are delegated to their respective Rust crates.
9
10pub mod blosclz;
11
12use crate::b2nd::B2ndMeta;
13use crate::constants::*;
14use std::cell::RefCell;
15use std::collections::HashMap;
16use std::ffi::{c_char, c_void, CStr};
17use std::sync::{OnceLock, RwLock};
18#[cfg(feature = "plugin-zfp")]
19use zfp_rs::{
20    types::ZFP_MAX_PREC, ZfpBitStream, ZfpConfig, ZfpDimensionality, ZfpField, ZfpFieldMut,
21    ZfpScalarType, ZfpStreamAlignment,
22};
23use zstd_pure_rs::common::error::{ErrorCode, ERROR};
24use zstd_pure_rs::common::xxhash::XXH64_state_t;
25use zstd_pure_rs::decompress::zstd_ddict::{
26    ZSTD_DDict, ZSTD_DDict_dictBuffer, ZSTD_DDict_dictContent, ZSTD_DDict_entropyPresent,
27    ZSTD_createDDict, ZSTD_getDictID_fromDDict,
28};
29use zstd_pure_rs::decompress::zstd_decompress::{
30    ZSTD_decompressFrame_withOpStart, ZSTD_loadDEntropy,
31};
32use zstd_pure_rs::decompress::zstd_decompress_block::{ZSTD_DCtx, ZSTD_decoder_entropy_rep};
33use zstd_pure_rs::prelude::*;
34
35/// Opaque C-ABI prefilter callback slot in `blosc2_cparams`.
36pub type Blosc2PrefilterCb = Option<unsafe extern "C" fn(params: *mut c_void) -> i32>;
37
38/// Opaque C-ABI postfilter callback slot in `blosc2_dparams`.
39pub type Blosc2PostfilterCb = Option<unsafe extern "C" fn(params: *mut c_void) -> i32>;
40
41/// Signature for a user-defined compression function registered via
42/// [`register_codec`].
43pub type CodecCompressFn = fn(clevel: u8, meta: u8, src: &[u8], dest: &mut [u8]) -> i32;
44/// Signature for a user-defined decompression function registered via
45/// [`register_codec`].
46pub type CodecDecompressFn = fn(meta: u8, src: &[u8], dest: &mut [u8]) -> i32;
47
48/// C-compatible callback return codes used by richer codec callbacks.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50#[repr(i32)]
51pub enum PluginCallbackStatus {
52    Success = 0,
53    Failure = 1,
54}
55
56/// Compression-parameter snapshot exposed to codec plugin callbacks.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct CodecCParamsContext {
59    pub compcode: u8,
60    pub compcode_meta: u8,
61    pub clevel: u8,
62    pub use_dict: i32,
63    pub typesize: i32,
64    pub blocksize: i32,
65    pub splitmode: i32,
66    pub filters: [u8; BLOSC2_MAX_FILTERS],
67    pub filters_meta: [u8; BLOSC2_MAX_FILTERS],
68    pub nthreads: i16,
69    pub nchunk: i64,
70    pub user_data: usize,
71    pub instr_codec: bool,
72    pub codec_params: usize,
73}
74
75/// Decompression-parameter snapshot exposed to codec plugin callbacks.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct CodecDParamsContext {
78    pub nthreads: i16,
79    pub typesize: i32,
80    pub nchunk: i64,
81    pub user_data: usize,
82}
83
84/// Per-block context exposed to C-compatible codec plugin callbacks.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct CodecChunkContext {
87    pub schunk: usize,
88    pub nchunk: i64,
89    pub nblock: i32,
90    pub chunk_source: usize,
91    pub block_offset: usize,
92    pub blocksize: usize,
93    pub bsize: usize,
94}
95
96/// Rich codec callback parameters, modeled after C-Blosc2 plugin callbacks.
97#[derive(Debug, Clone, Copy)]
98pub struct CodecCallbackContext<'a> {
99    pub compcode: u8,
100    pub complib: Option<u8>,
101    pub meta: u8,
102    pub clevel: u8,
103    pub cparams: Option<&'a CodecCParamsContext>,
104    pub dparams: Option<&'a CodecDParamsContext>,
105    pub chunk: CodecChunkContext,
106    pub b2nd_metalayer: Option<&'a [u8]>,
107    pub user_data: usize,
108}
109
110/// C-ABI compression parameters passed to raw `blosc2_codec` callbacks.
111#[derive(Clone, Copy)]
112#[repr(C)]
113pub struct Blosc2CParams {
114    pub compcode: u8,
115    pub compcode_meta: u8,
116    pub clevel: u8,
117    pub use_dict: i32,
118    pub typesize: i32,
119    pub nthreads: i16,
120    pub blocksize: i32,
121    pub splitmode: i32,
122    pub schunk: *mut c_void,
123    pub filters: [u8; BLOSC2_MAX_FILTERS],
124    pub filters_meta: [u8; BLOSC2_MAX_FILTERS],
125    pub prefilter: Blosc2PrefilterCb,
126    pub preparams: *mut c_void,
127    pub tuner_params: *mut c_void,
128    pub tuner_id: i32,
129    pub instr_codec: bool,
130    pub codec_params: *mut c_void,
131    pub filter_params: [*mut c_void; BLOSC2_MAX_FILTERS],
132}
133
134impl Blosc2CParams {
135    fn from_context(ctx: &CodecCParamsContext, schunk: usize) -> Self {
136        Self {
137            compcode: ctx.compcode,
138            compcode_meta: ctx.compcode_meta,
139            clevel: ctx.clevel,
140            use_dict: ctx.use_dict,
141            typesize: ctx.typesize,
142            nthreads: ctx.nthreads,
143            blocksize: ctx.blocksize,
144            splitmode: ctx.splitmode,
145            schunk: schunk as *mut c_void,
146            filters: ctx.filters,
147            filters_meta: ctx.filters_meta,
148            prefilter: None,
149            preparams: std::ptr::null_mut(),
150            tuner_params: std::ptr::null_mut(),
151            tuner_id: 0,
152            instr_codec: ctx.instr_codec,
153            codec_params: ctx.codec_params as *mut c_void,
154            filter_params: [std::ptr::null_mut(); BLOSC2_MAX_FILTERS],
155        }
156    }
157
158    fn from_pipeline(ctx: &CodecCallbackContext<'_>) -> Self {
159        ctx.cparams.map_or_else(
160            || Self {
161                compcode: ctx.compcode,
162                compcode_meta: ctx.meta,
163                clevel: ctx.clevel,
164                use_dict: 0,
165                typesize: 8,
166                nthreads: 1,
167                blocksize: 0,
168                splitmode: BLOSC_FORWARD_COMPAT_SPLIT,
169                schunk: ctx.chunk.schunk as *mut c_void,
170                filters: [
171                    BLOSC_NOFILTER,
172                    BLOSC_NOFILTER,
173                    BLOSC_NOFILTER,
174                    BLOSC_NOFILTER,
175                    BLOSC_NOFILTER,
176                    BLOSC_SHUFFLE,
177                ],
178                filters_meta: [0; BLOSC2_MAX_FILTERS],
179                prefilter: None,
180                preparams: std::ptr::null_mut(),
181                tuner_params: std::ptr::null_mut(),
182                tuner_id: 0,
183                instr_codec: false,
184                codec_params: std::ptr::null_mut(),
185                filter_params: [std::ptr::null_mut(); BLOSC2_MAX_FILTERS],
186            },
187            |cparams| Self::from_context(cparams, ctx.chunk.schunk),
188        )
189    }
190}
191
192/// C-ABI decompression parameters passed to raw `blosc2_codec` callbacks.
193#[derive(Clone, Copy)]
194#[repr(C)]
195pub struct Blosc2DParams {
196    pub nthreads: i16,
197    pub schunk: *mut c_void,
198    pub postfilter: Blosc2PostfilterCb,
199    pub postparams: *mut c_void,
200    pub typesize: i32,
201}
202
203impl Blosc2DParams {
204    fn from_context(ctx: &CodecDParamsContext, schunk: usize) -> Self {
205        Self {
206            nthreads: ctx.nthreads,
207            schunk: schunk as *mut c_void,
208            postfilter: None,
209            postparams: std::ptr::null_mut(),
210            typesize: ctx.typesize,
211        }
212    }
213
214    fn from_pipeline(ctx: &CodecCallbackContext<'_>) -> Self {
215        ctx.dparams.map_or_else(
216            || Self {
217                nthreads: 1,
218                schunk: ctx.chunk.schunk as *mut c_void,
219                postfilter: None,
220                postparams: std::ptr::null_mut(),
221                typesize: 8,
222            },
223            |dparams| Self::from_context(dparams, ctx.chunk.schunk),
224        )
225    }
226}
227
228/// Rich compression callback signature for C-compatible codecs.
229pub type ContextCodecCompressFn =
230    for<'a> fn(&mut CodecCallbackContext<'a>, src: &[u8], dest: &mut [u8]) -> i32;
231/// Rich decompression callback signature for C-compatible codecs.
232pub type ContextCodecDecompressFn =
233    for<'a> fn(&mut CodecCallbackContext<'a>, src: &[u8], dest: &mut [u8]) -> i32;
234
235/// Raw C-ABI encoder callback signature for a `blosc2_codec`.
236pub type Blosc2CodecEncoderCb = unsafe extern "C" fn(
237    input: *const u8,
238    input_len: i32,
239    output: *mut u8,
240    output_len: i32,
241    meta: u8,
242    cparams: *mut Blosc2CParams,
243    chunk: *const c_void,
244) -> i32;
245
246/// Raw C-ABI decoder callback signature for a `blosc2_codec`.
247pub type Blosc2CodecDecoderCb = unsafe extern "C" fn(
248    input: *const u8,
249    input_len: i32,
250    output: *mut u8,
251    output_len: i32,
252    meta: u8,
253    dparams: *mut Blosc2DParams,
254    chunk: *const c_void,
255) -> i32;
256
257/// Rust-shaped user codec descriptor for ergonomic source-level registration.
258#[derive(Clone, Copy)]
259pub struct Blosc2Codec {
260    pub compcode: u8,
261    pub compname: &'static str,
262    pub complib: u8,
263    pub version: u8,
264    pub encoder: CodecCompressFn,
265    pub decoder: CodecDecompressFn,
266}
267
268/// Raw C-shaped `blosc2_codec` descriptor.
269#[derive(Clone, Copy)]
270#[repr(C)]
271pub struct Blosc2CodecAbi {
272    pub compcode: u8,
273    pub compname: *const c_char,
274    pub complib: u8,
275    pub version: u8,
276    pub encoder: Option<Blosc2CodecEncoderCb>,
277    pub decoder: Option<Blosc2CodecDecoderCb>,
278}
279
280#[derive(Clone, Copy)]
281struct UserCodec {
282    name: Option<&'static str>,
283    complib: Option<u8>,
284    version: Option<u8>,
285    compress: UserCodecCompress,
286    decompress: UserCodecDecompress,
287}
288
289#[derive(Clone, Copy)]
290enum UserCodecCompress {
291    Legacy(CodecCompressFn),
292    Context(ContextCodecCompressFn),
293    CAbi(Option<Blosc2CodecEncoderCb>),
294}
295
296impl UserCodecCompress {
297    fn same_callback(self, other: Self) -> bool {
298        match (self, other) {
299            (Self::Legacy(a), Self::Legacy(b)) => a as usize == b as usize,
300            (Self::Context(a), Self::Context(b)) => a as usize == b as usize,
301            (Self::CAbi(a), Self::CAbi(b)) => {
302                a.map(|callback| callback as usize) == b.map(|callback| callback as usize)
303            }
304            _ => false,
305        }
306    }
307
308    fn run(
309        self,
310        ctx: &mut CodecCallbackContext<'_>,
311        clevel: u8,
312        meta: u8,
313        src: &[u8],
314        dest: &mut [u8],
315    ) -> i32 {
316        match self {
317            Self::Legacy(callback) => callback(clevel, meta, src, dest),
318            Self::Context(callback) => callback(ctx, src, dest),
319            Self::CAbi(Some(callback)) => {
320                let Ok(input_len) = i32::try_from(src.len()) else {
321                    return BLOSC2_ERROR_2GB_LIMIT;
322                };
323                let Ok(output_len) = i32::try_from(dest.len()) else {
324                    return BLOSC2_ERROR_2GB_LIMIT;
325                };
326                let mut cparams = Blosc2CParams::from_pipeline(ctx);
327                let chunk = codec_chunk_arg(src.as_ptr(), ctx);
328                unsafe {
329                    callback(
330                        src.as_ptr(),
331                        input_len,
332                        dest.as_mut_ptr(),
333                        output_len,
334                        meta,
335                        &mut cparams,
336                        chunk,
337                    )
338                }
339            }
340            Self::CAbi(None) => missing_dynamic_codec_callback(),
341        }
342    }
343}
344
345#[derive(Clone, Copy)]
346enum UserCodecDecompress {
347    Legacy(CodecDecompressFn),
348    Context(ContextCodecDecompressFn),
349    CAbi(Option<Blosc2CodecDecoderCb>),
350}
351
352impl UserCodecDecompress {
353    fn same_callback(self, other: Self) -> bool {
354        match (self, other) {
355            (Self::Legacy(a), Self::Legacy(b)) => a as usize == b as usize,
356            (Self::Context(a), Self::Context(b)) => a as usize == b as usize,
357            (Self::CAbi(a), Self::CAbi(b)) => {
358                a.map(|callback| callback as usize) == b.map(|callback| callback as usize)
359            }
360            _ => false,
361        }
362    }
363
364    fn run(self, ctx: &mut CodecCallbackContext<'_>, meta: u8, src: &[u8], dest: &mut [u8]) -> i32 {
365        match self {
366            Self::Legacy(callback) => callback(meta, src, dest),
367            Self::Context(callback) => callback(ctx, src, dest),
368            Self::CAbi(Some(callback)) => {
369                let Ok(input_len) = i32::try_from(src.len()) else {
370                    return BLOSC2_ERROR_DATA;
371                };
372                let Ok(output_len) = i32::try_from(dest.len()) else {
373                    return BLOSC2_ERROR_DATA;
374                };
375                let mut dparams = Blosc2DParams::from_pipeline(ctx);
376                let chunk = codec_chunk_arg(src.as_ptr(), ctx);
377                unsafe {
378                    callback(
379                        src.as_ptr(),
380                        input_len,
381                        dest.as_mut_ptr(),
382                        output_len,
383                        meta,
384                        &mut dparams,
385                        chunk,
386                    )
387                }
388            }
389            Self::CAbi(None) => missing_dynamic_codec_callback(),
390        }
391    }
392}
393
394fn missing_dynamic_codec_callback() -> i32 {
395    // C-Blosc2 may dynamically resolve null codec callbacks. This crate has no
396    // codec plugin loader, so preserve null callbacks and fail at invocation.
397    BLOSC2_ERROR_CODEC_SUPPORT
398}
399
400fn codec_chunk_arg(block_ptr: *const u8, ctx: &CodecCallbackContext<'_>) -> *const c_void {
401    if ctx.chunk.chunk_source != 0 {
402        ctx.chunk.chunk_source as *const c_void
403    } else {
404        (block_ptr as usize)
405            .checked_sub(ctx.chunk.block_offset)
406            .map_or(std::ptr::null(), |addr| addr as *const c_void)
407    }
408}
409
410#[derive(Clone, Copy)]
411struct KnownGlobalCodec {
412    compcode: u8,
413    name: &'static str,
414    version: u8,
415}
416
417const KNOWN_GLOBAL_CODECS: &[KnownGlobalCodec] = &[
418    KnownGlobalCodec {
419        compcode: BLOSC_CODEC_NDLZ,
420        name: "ndlz",
421        version: 1,
422    },
423    KnownGlobalCodec {
424        compcode: BLOSC_CODEC_ZFP_FIXED_ACCURACY,
425        name: "zfp_acc",
426        version: 1,
427    },
428    KnownGlobalCodec {
429        compcode: BLOSC_CODEC_ZFP_FIXED_PRECISION,
430        name: "zfp_prec",
431        version: 1,
432    },
433    KnownGlobalCodec {
434        compcode: BLOSC_CODEC_ZFP_FIXED_RATE,
435        name: "zfp_rate",
436        version: 1,
437    },
438    KnownGlobalCodec {
439        compcode: BLOSC_CODEC_OPENHTJ2K,
440        name: "openhtj2k",
441        version: 1,
442    },
443    KnownGlobalCodec {
444        compcode: BLOSC_CODEC_GROK,
445        name: "grok",
446        version: 1,
447    },
448    KnownGlobalCodec {
449        compcode: BLOSC_CODEC_OPENZL,
450        name: "openzl",
451        version: 1,
452    },
453];
454
455impl UserCodec {
456    fn same_callbacks(self, other: Self) -> bool {
457        self.name == other.name
458            && self.complib == other.complib
459            && self.version == other.version
460            && self.compress.same_callback(other.compress)
461            && self.decompress.same_callback(other.decompress)
462    }
463}
464
465fn known_global_codec_by_code(compcode: u8) -> Option<KnownGlobalCodec> {
466    KNOWN_GLOBAL_CODECS
467        .iter()
468        .copied()
469        .find(|codec| codec.compcode == compcode)
470}
471
472fn known_global_codec_by_name(name: &str) -> Option<KnownGlobalCodec> {
473    KNOWN_GLOBAL_CODECS
474        .iter()
475        .copied()
476        .find(|codec| codec.name == name)
477}
478
479fn builtin_complib_info(complib: u8) -> Option<(&'static str, &'static str)> {
480    match complib {
481        BLOSC_BLOSCLZ_LIB => Some((BLOSC_BLOSCLZ_LIBNAME, "2.5.3")),
482        BLOSC_LZ4_LIB => Some((BLOSC_LZ4_LIBNAME, "1.10.0")),
483        BLOSC_ZLIB_LIB => Some((BLOSC_ZLIB_LIBNAME, "2.0.7")),
484        BLOSC_ZSTD_LIB => Some((BLOSC_ZSTD_LIBNAME, "1.5.7")),
485        _ => None,
486    }
487}
488
489/// Returns `true` for C-Blosc2 global plugin codec IDs known by this crate.
490///
491/// These entries provide C-compatible IDs and metadata. They do not imply that
492/// the codec implementation has been ported.
493pub fn is_known_global_codec(compcode: u8) -> bool {
494    known_global_codec_by_code(compcode).is_some()
495}
496
497/// Returns `true` for known C-Blosc2 ZFP plugin codec modes.
498pub fn is_known_zfp_codec(compcode: u8) -> bool {
499    matches!(
500        compcode,
501        BLOSC_CODEC_ZFP_FIXED_ACCURACY
502            | BLOSC_CODEC_ZFP_FIXED_PRECISION
503            | BLOSC_CODEC_ZFP_FIXED_RATE
504    )
505}
506
507pub fn is_static_global_codec_enabled(compcode: u8) -> bool {
508    (cfg!(feature = "plugin-ndlz") && compcode == BLOSC_CODEC_NDLZ)
509        || (cfg!(feature = "plugin-zfp") && is_known_zfp_codec(compcode))
510}
511
512static USER_CODECS: OnceLock<RwLock<HashMap<u8, UserCodec>>> = OnceLock::new();
513static USER_CODEC_ORDER: OnceLock<RwLock<Vec<u8>>> = OnceLock::new();
514
515struct Lz4Stream(*mut lz4_pure::sys::LZ4StreamEncode);
516
517impl Drop for Lz4Stream {
518    fn drop(&mut self) {
519        unsafe {
520            lz4_pure::sys::LZ4_freeStream(self.0);
521        }
522    }
523}
524
525struct Lz4hcStream(*mut lz4_pure::sys::LZ4StreamHC);
526
527impl Drop for Lz4hcStream {
528    fn drop(&mut self) {
529        unsafe {
530            lz4_pure::sys::LZ4_freeStreamHC(self.0);
531        }
532    }
533}
534
535thread_local! {
536    static LZ4_STREAM: RefCell<Option<Lz4Stream>> = const { RefCell::new(None) };
537    static LZ4HC_STREAM: RefCell<Option<Lz4hcStream>> = const { RefCell::new(None) };
538    static ZSTD_CCTX: RefCell<Option<Box<ZSTD_CCtx>>> = const { RefCell::new(None) };
539    static ZSTD_DICT_DCTX: RefCell<Box<ZSTD_DCtx>> = RefCell::new(ZSTD_createDCtx());
540    static ZSTD_DCTX: RefCell<(Box<ZSTD_DCtx>, ZSTD_decoder_entropy_rep, XXH64_state_t)> =
541        RefCell::new((
542            ZSTD_createDCtx(),
543            ZSTD_decoder_entropy_rep::default(),
544            XXH64_state_t::default(),
545        ));
546}
547
548/// Lazily-initialized registry of user-defined codecs, keyed by codec ID.
549fn user_codecs() -> &'static RwLock<HashMap<u8, UserCodec>> {
550    USER_CODECS.get_or_init(|| RwLock::new(HashMap::new()))
551}
552
553fn user_codec_order() -> &'static RwLock<Vec<u8>> {
554    USER_CODEC_ORDER.get_or_init(|| RwLock::new(Vec::new()))
555}
556
557fn remember_codec_order(compcode: u8) -> Result<(), &'static str> {
558    let mut order = user_codec_order()
559        .write()
560        .map_err(|_| "Codec registry poisoned")?;
561    if !order.contains(&compcode) {
562        order.push(compcode);
563    }
564    Ok(())
565}
566
567fn known_global_registration_status(
568    compcode: u8,
569    name: Option<&str>,
570    duplicate_error: &'static str,
571) -> Result<bool, &'static str> {
572    let Some(known) = known_global_codec_by_code(compcode) else {
573        return Ok(false);
574    };
575    if name != Some(known.name) {
576        return Err(duplicate_error);
577    }
578    Ok(true)
579}
580
581/// Register a user-defined codec under `compcode`.
582///
583/// C-Blosc2 reserves IDs 32..=159 for global plugin codecs and 160..=255 for
584/// user-defined codecs. Lower IDs are reserved for built-in codecs. Duplicate
585/// IDs are rejected so existing chunks do not change behavior after accidental
586/// callback replacement.
587pub fn register_codec(
588    compcode: u8,
589    compress: CodecCompressFn,
590    decompress: CodecDecompressFn,
591) -> Result<(), &'static str> {
592    register_codec_impl(
593        compcode,
594        None,
595        compress,
596        decompress,
597        BLOSC2_USER_DEFINED_CODECS_START..=u8::MAX,
598    )
599}
600
601/// Register a named user-defined codec under `compcode`.
602///
603/// The name is used by C-Blosc2-style compressor name/code lookup helpers.
604pub fn register_named_codec(
605    compcode: u8,
606    name: &'static str,
607    compress: CodecCompressFn,
608    decompress: CodecDecompressFn,
609) -> Result<(), &'static str> {
610    if name.is_empty() {
611        return Err("User-defined codec name cannot be empty");
612    }
613    register_codec_impl(
614        compcode,
615        Some(name),
616        compress,
617        decompress,
618        BLOSC2_USER_DEFINED_CODECS_START..=u8::MAX,
619    )
620}
621
622/// C-name registration wrapper for [`Blosc2Codec`].
623pub fn blosc2_register_codec(codec: &Blosc2Codec) -> i32 {
624    blosc2_register_codec_c(Some(codec))
625}
626
627/// C-style nullable registration wrapper for [`Blosc2Codec`].
628pub fn blosc2_register_codec_c(codec: Option<&Blosc2Codec>) -> i32 {
629    let Some(codec) = codec else {
630        return BLOSC2_ERROR_INVALID_PARAM;
631    };
632    match register_blosc2_codec_impl(codec) {
633        Ok(()) => BLOSC2_ERROR_SUCCESS,
634        Err("Codec IDs must be >= 32")
635        | Err("User-defined codec IDs must be >= 160")
636        | Err("Codec ID outside allowed range")
637        | Err("User-defined codec ID already registered")
638        | Err("User-defined codec name already registered") => BLOSC2_ERROR_CODEC_PARAM,
639        Err("User-defined codec name cannot be empty") => BLOSC2_ERROR_INVALID_PARAM,
640        Err(_) => BLOSC2_ERROR_FAILURE,
641    }
642}
643
644/// C-name registration wrapper for raw `blosc2_codec` descriptors.
645///
646/// This accepts the C callback shape:
647/// `(input, input_len, output, output_len, meta, cparams/dparams, chunk)`.
648pub fn blosc2_register_codec_abi(codec: *const Blosc2CodecAbi) -> i32 {
649    if codec.is_null() {
650        return BLOSC2_ERROR_INVALID_PARAM;
651    }
652    let codec = unsafe { &*codec };
653    match register_blosc2_codec_abi_impl(codec) {
654        Ok(()) => BLOSC2_ERROR_SUCCESS,
655        Err("Codec IDs must be >= 32")
656        | Err("User-defined codec IDs must be >= 160")
657        | Err("Codec ID outside allowed range")
658        | Err("User-defined codec ID already registered")
659        | Err("User-defined codec name already registered") => BLOSC2_ERROR_CODEC_PARAM,
660        Err("User-defined codec name cannot be empty") => BLOSC2_ERROR_INVALID_PARAM,
661        Err(_) => BLOSC2_ERROR_FAILURE,
662    }
663}
664
665fn register_blosc2_codec_impl(codec: &Blosc2Codec) -> Result<(), &'static str> {
666    if codec.compcode < BLOSC2_USER_DEFINED_CODECS_START {
667        return Err("User-defined codec IDs must be >= 160");
668    }
669    let mut codecs = user_codecs()
670        .write()
671        .map_err(|_| "Codec registry poisoned")?;
672
673    let registered = UserCodec {
674        name: Some(codec.compname),
675        complib: Some(codec.complib),
676        version: Some(codec.version),
677        compress: UserCodecCompress::Legacy(codec.encoder),
678        decompress: UserCodecDecompress::Legacy(codec.decoder),
679    };
680    if let Some(existing) = codecs.get(&codec.compcode) {
681        if existing.name == registered.name {
682            return Ok(());
683        }
684        return Err("User-defined codec ID already registered");
685    }
686    codecs.insert(codec.compcode, registered);
687    drop(codecs);
688    remember_codec_order(codec.compcode)?;
689    Ok(())
690}
691
692fn blosc2_codec_abi_name(name: *const c_char) -> Result<&'static str, &'static str> {
693    if name.is_null() {
694        return Err("User-defined codec name cannot be empty");
695    }
696    let c_name = unsafe { CStr::from_ptr(name) };
697    let name = match c_name.to_str() {
698        Ok(name) => name.to_owned(),
699        Err(_) => c_name
700            .to_bytes()
701            .iter()
702            .map(|&byte| char::from(byte))
703            .collect(),
704    };
705    Ok(Box::leak(name.into_boxed_str()))
706}
707
708fn register_blosc2_codec_abi_impl(codec: &Blosc2CodecAbi) -> Result<(), &'static str> {
709    if codec.compcode < BLOSC2_USER_DEFINED_CODECS_START {
710        return Err("User-defined codec IDs must be >= 160");
711    }
712    let name = blosc2_codec_abi_name(codec.compname)?;
713    let mut codecs = user_codecs()
714        .write()
715        .map_err(|_| "Codec registry poisoned")?;
716    let registered = UserCodec {
717        name: Some(name),
718        complib: Some(codec.complib),
719        version: Some(codec.version),
720        compress: UserCodecCompress::CAbi(codec.encoder),
721        decompress: UserCodecDecompress::CAbi(codec.decoder),
722    };
723    if let Some(existing) = codecs.get(&codec.compcode) {
724        if existing.name == Some(name) {
725            return Ok(());
726        }
727        return Err("User-defined codec ID already registered");
728    }
729    codecs.insert(codec.compcode, registered);
730    drop(codecs);
731    remember_codec_order(codec.compcode)?;
732    Ok(())
733}
734
735fn register_codec_impl(
736    compcode: u8,
737    name: Option<&'static str>,
738    compress: CodecCompressFn,
739    decompress: CodecDecompressFn,
740    allowed: std::ops::RangeInclusive<u8>,
741) -> Result<(), &'static str> {
742    if compcode < BLOSC2_USER_DEFINED_CODECS_START {
743        return Err("User-defined codec IDs must be >= 160");
744    }
745    if !allowed.contains(&compcode) {
746        return Err("Codec ID outside allowed range");
747    }
748    let mut codecs = user_codecs()
749        .write()
750        .map_err(|_| "Codec registry poisoned")?;
751    let codec = UserCodec {
752        name,
753        complib: None,
754        version: None,
755        compress: UserCodecCompress::Legacy(compress),
756        decompress: UserCodecDecompress::Legacy(decompress),
757    };
758    if let Some(existing) = codecs.get(&compcode) {
759        if name.is_some() && existing.name == name {
760            return Ok(());
761        }
762        if name.is_some() && existing.name != name {
763            return Err("User-defined codec ID already registered");
764        }
765        return if existing.same_callbacks(codec) {
766            Ok(())
767        } else {
768            Err("User-defined codec ID already registered")
769        };
770    }
771    codecs.insert(compcode, codec);
772    drop(codecs);
773    remember_codec_order(compcode)?;
774    Ok(())
775}
776
777/// Register a global plugin codec under `compcode`.
778///
779/// This mirrors C-Blosc2's internal plugin registration path: IDs 32..=159 are
780/// accepted for globally registered plugins, while user-defined IDs still use
781/// [`register_codec`]. Duplicate unnamed IDs are rejected because C-Blosc2's
782/// idempotent re-registration check is based on the codec name.
783pub fn register_global_codec(
784    compcode: u8,
785    compress: CodecCompressFn,
786    decompress: CodecDecompressFn,
787) -> Result<(), &'static str> {
788    register_global_codec_impl(compcode, None, None, None, compress, decompress)
789}
790
791/// Register a named global plugin codec under `compcode`.
792pub fn register_named_global_codec(
793    compcode: u8,
794    name: &'static str,
795    compress: CodecCompressFn,
796    decompress: CodecDecompressFn,
797) -> Result<(), &'static str> {
798    if name.is_empty() {
799        return Err("Global plugin codec name cannot be empty");
800    }
801    register_global_codec_impl(compcode, Some(name), None, None, compress, decompress)
802}
803
804/// Register a named global plugin codec with C-style metadata.
805///
806/// `complib` and `version` are used by compressor name/library lookups and by
807/// chunk headers, matching C's globally registered codec descriptors.
808pub fn register_global_codec_with_metadata(
809    compcode: u8,
810    name: &'static str,
811    complib: u8,
812    version: u8,
813    compress: CodecCompressFn,
814    decompress: CodecDecompressFn,
815) -> Result<(), &'static str> {
816    if name.is_empty() {
817        return Err("Global plugin codec name cannot be empty");
818    }
819    register_global_codec_impl(
820        compcode,
821        Some(name),
822        Some(complib),
823        Some(version),
824        compress,
825        decompress,
826    )
827}
828
829/// Register a codec through the C-Blosc2 private registration range.
830///
831/// This mirrors C's private `register_codec_private` path, which accepts any
832/// non-built-in codec ID (`32..=255`), including the user-defined range.
833pub fn register_private_codec(
834    compcode: u8,
835    compress: CodecCompressFn,
836    decompress: CodecDecompressFn,
837) -> Result<(), &'static str> {
838    register_private_codec_impl(compcode, None, None, None, compress, decompress)
839}
840
841/// Register a named codec through the C-Blosc2 private registration range.
842pub fn register_named_private_codec(
843    compcode: u8,
844    name: &'static str,
845    compress: CodecCompressFn,
846    decompress: CodecDecompressFn,
847) -> Result<(), &'static str> {
848    if name.is_empty() {
849        return Err("Private codec name cannot be empty");
850    }
851    register_private_codec_impl(compcode, Some(name), None, None, compress, decompress)
852}
853
854/// Register a named private codec with C-style metadata.
855pub fn register_private_codec_with_metadata(
856    compcode: u8,
857    name: &'static str,
858    complib: u8,
859    version: u8,
860    compress: CodecCompressFn,
861    decompress: CodecDecompressFn,
862) -> Result<(), &'static str> {
863    if name.is_empty() {
864        return Err("Private codec name cannot be empty");
865    }
866    register_private_codec_impl(
867        compcode,
868        Some(name),
869        Some(complib),
870        Some(version),
871        compress,
872        decompress,
873    )
874}
875
876/// Register a user-defined codec with C-compatible contextual callbacks.
877pub fn register_context_codec(
878    compcode: u8,
879    compress: ContextCodecCompressFn,
880    decompress: ContextCodecDecompressFn,
881) -> Result<(), &'static str> {
882    register_context_codec_impl(
883        compcode,
884        None,
885        None,
886        None,
887        compress,
888        decompress,
889        BLOSC2_USER_DEFINED_CODECS_START..=u8::MAX,
890        "User-defined codec IDs must be >= 160",
891        "User-defined codec ID already registered",
892    )
893}
894
895/// Register a global plugin codec with C-compatible contextual callbacks.
896pub fn register_global_context_codec(
897    compcode: u8,
898    compress: ContextCodecCompressFn,
899    decompress: ContextCodecDecompressFn,
900) -> Result<(), &'static str> {
901    register_context_codec_impl(
902        compcode,
903        None,
904        None,
905        None,
906        compress,
907        decompress,
908        BLOSC2_GLOBAL_REGISTERED_CODECS_START..=BLOSC2_GLOBAL_REGISTERED_CODECS_STOP,
909        "Global plugin codec IDs must be in 32..=159",
910        "Global plugin codec ID already registered",
911    )
912}
913
914/// Register a named global plugin codec with C-style metadata and contextual callbacks.
915pub fn register_global_context_codec_with_metadata(
916    compcode: u8,
917    name: &'static str,
918    complib: u8,
919    version: u8,
920    compress: ContextCodecCompressFn,
921    decompress: ContextCodecDecompressFn,
922) -> Result<(), &'static str> {
923    if name.is_empty() {
924        return Err("Global plugin codec name cannot be empty");
925    }
926    register_context_codec_impl(
927        compcode,
928        Some(name),
929        Some(complib),
930        Some(version),
931        compress,
932        decompress,
933        BLOSC2_GLOBAL_REGISTERED_CODECS_START..=BLOSC2_GLOBAL_REGISTERED_CODECS_STOP,
934        "Global plugin codec IDs must be in 32..=159",
935        "Global plugin codec ID already registered",
936    )
937}
938
939fn register_global_codec_impl(
940    compcode: u8,
941    name: Option<&'static str>,
942    complib: Option<u8>,
943    version: Option<u8>,
944    compress: CodecCompressFn,
945    decompress: CodecDecompressFn,
946) -> Result<(), &'static str> {
947    if !(BLOSC2_GLOBAL_REGISTERED_CODECS_START..=BLOSC2_GLOBAL_REGISTERED_CODECS_STOP)
948        .contains(&compcode)
949    {
950        return Err("Global plugin codec IDs must be in 32..=159");
951    }
952    if known_global_registration_status(
953        compcode,
954        name,
955        "Global plugin codec ID already registered",
956    )? {
957        return Ok(());
958    }
959    let mut codecs = user_codecs()
960        .write()
961        .map_err(|_| "Codec registry poisoned")?;
962    let codec = UserCodec {
963        name,
964        complib,
965        version,
966        compress: UserCodecCompress::Legacy(compress),
967        decompress: UserCodecDecompress::Legacy(decompress),
968    };
969    if let Some(existing) = codecs.get(&compcode) {
970        if name.is_some() && existing.name == name {
971            return Ok(());
972        }
973        return Err("Global plugin codec ID already registered");
974    }
975    codecs.insert(compcode, codec);
976    drop(codecs);
977    remember_codec_order(compcode)?;
978    Ok(())
979}
980
981fn register_private_codec_impl(
982    compcode: u8,
983    name: Option<&'static str>,
984    complib: Option<u8>,
985    version: Option<u8>,
986    compress: CodecCompressFn,
987    decompress: CodecDecompressFn,
988) -> Result<(), &'static str> {
989    if compcode < BLOSC2_GLOBAL_REGISTERED_CODECS_START {
990        return Err("Private codec IDs must be >= 32");
991    }
992    if known_global_registration_status(compcode, name, "Private codec ID already registered")? {
993        return Ok(());
994    }
995    let mut codecs = user_codecs()
996        .write()
997        .map_err(|_| "Codec registry poisoned")?;
998    let codec = UserCodec {
999        name,
1000        complib,
1001        version,
1002        compress: UserCodecCompress::Legacy(compress),
1003        decompress: UserCodecDecompress::Legacy(decompress),
1004    };
1005    if let Some(existing) = codecs.get(&compcode) {
1006        if name.is_some() && existing.name == name {
1007            return Ok(());
1008        }
1009        return Err("Private codec ID already registered");
1010    }
1011    codecs.insert(compcode, codec);
1012    drop(codecs);
1013    remember_codec_order(compcode)?;
1014    Ok(())
1015}
1016
1017#[allow(clippy::too_many_arguments)]
1018fn register_context_codec_impl(
1019    compcode: u8,
1020    name: Option<&'static str>,
1021    complib: Option<u8>,
1022    version: Option<u8>,
1023    compress: ContextCodecCompressFn,
1024    decompress: ContextCodecDecompressFn,
1025    allowed: std::ops::RangeInclusive<u8>,
1026    range_error: &'static str,
1027    duplicate_error: &'static str,
1028) -> Result<(), &'static str> {
1029    if !allowed.contains(&compcode) {
1030        return Err(range_error);
1031    }
1032    if known_global_registration_status(compcode, name, duplicate_error)? {
1033        return Ok(());
1034    }
1035    let mut codecs = user_codecs()
1036        .write()
1037        .map_err(|_| "Codec registry poisoned")?;
1038    let codec = UserCodec {
1039        name,
1040        complib,
1041        version,
1042        compress: UserCodecCompress::Context(compress),
1043        decompress: UserCodecDecompress::Context(decompress),
1044    };
1045    if let Some(existing) = codecs.get(&compcode) {
1046        if name.is_some() && existing.name == name {
1047            return Ok(());
1048        }
1049        return Err(duplicate_error);
1050    }
1051    codecs.insert(compcode, codec);
1052    drop(codecs);
1053    remember_codec_order(compcode)?;
1054    Ok(())
1055}
1056
1057/// Returns `true` if `compcode` corresponds to a registered global descriptor
1058/// or a user-defined codec currently present in the registry.
1059pub fn is_registered_codec(compcode: u8) -> bool {
1060    known_global_codec_by_code(compcode).is_some()
1061        || user_codecs()
1062            .read()
1063            .is_ok_and(|codecs| codecs.contains_key(&compcode))
1064}
1065
1066/// Return the registered codec name for a plugin/user-defined codec.
1067pub fn registered_codec_name(compcode: u8) -> Option<&'static str> {
1068    if known_global_codec_by_code(compcode).is_some() {
1069        return known_global_codec_by_code(compcode).map(|codec| codec.name);
1070    }
1071    user_codecs()
1072        .read()
1073        .ok()
1074        .and_then(|codecs| codecs.get(&compcode).and_then(|codec| codec.name))
1075}
1076
1077/// Return C-Blosc2-style compression library info for a registered codec name.
1078pub fn registered_codec_complib_info(name: &str) -> Option<(u8, &'static str, &'static str)> {
1079    if let Some(codec) = known_global_codec_by_name(name) {
1080        return Some((codec.compcode, codec.name, "unknown"));
1081    }
1082    let codecs = user_codecs().read().ok()?;
1083    let order = user_codec_order().read().ok()?;
1084    order.iter().find_map(|code| {
1085        let codec = codecs.get(code)?;
1086        let codec_name = codec.name?;
1087        if codec_name != name {
1088            return None;
1089        }
1090        let complib = codec.complib?;
1091        if let Some((libname, version)) = builtin_complib_info(complib) {
1092            return Some((complib, libname, version));
1093        }
1094        let libname = order
1095            .iter()
1096            .find_map(|code| {
1097                let codec = codecs.get(code)?;
1098                (codec.complib == Some(complib)).then_some(codec.name?)
1099            })
1100            .unwrap_or(codec_name);
1101        codec.version.map(|_version| (complib, libname, "unknown"))
1102    })
1103}
1104
1105/// Return the registered codec name for a compressor-library code.
1106pub fn registered_codec_name_by_complib(complib: u8) -> Option<&'static str> {
1107    if let Some((libname, _version)) = builtin_complib_info(complib) {
1108        return Some(libname);
1109    }
1110    if let Some(codec) = known_global_codec_by_code(complib) {
1111        return Some(codec.name);
1112    }
1113    let codecs = user_codecs().read().ok()?;
1114    let order = user_codec_order().read().ok()?;
1115    order.iter().find_map(|code| {
1116        let codec = codecs.get(code)?;
1117        (codec.complib == Some(complib)).then_some(codec.name?)
1118    })
1119}
1120
1121/// Return the codec-format version registered for a plugin/user-defined codec.
1122pub fn registered_codec_version(compcode: u8) -> Option<u8> {
1123    if let Some(codec) = known_global_codec_by_code(compcode) {
1124        return Some(codec.version);
1125    }
1126    user_codecs()
1127        .read()
1128        .ok()
1129        .and_then(|codecs| codecs.get(&compcode).and_then(|codec| codec.version))
1130}
1131
1132/// Return the registered codec ID for a plugin/user-defined codec name.
1133pub fn registered_codec_code(name: &str) -> Option<u8> {
1134    if let Some(codec) = known_global_codec_by_name(name) {
1135        return Some(codec.compcode);
1136    }
1137    let codecs = user_codecs().read().ok()?;
1138    let order = user_codec_order().read().ok()?;
1139    order
1140        .iter()
1141        .find_map(|&code| (codecs.get(&code)?.name == Some(name)).then_some(code))
1142}
1143
1144/// Returns `true` if `compcode` supports compression and decompression
1145/// using a preset dictionary (currently LZ4, LZ4HC and Zstd).
1146pub fn codec_supports_dict(compcode: u8) -> bool {
1147    matches!(compcode, BLOSC_LZ4 | BLOSC_LZ4HC | BLOSC_ZSTD)
1148}
1149
1150/// Compress a single block using the codec identified by `compcode`.
1151///
1152/// `clevel` is the Blosc compression level (0–9); it is mapped to each
1153/// codec's native level inside the codec-specific wrappers.
1154/// Returns the number of compressed bytes, or 0 if the data is not
1155/// compressible (or compression failed and the caller should store the
1156/// block uncompressed).
1157pub fn compress_block(compcode: u8, clevel: u8, src: &[u8], dest: &mut [u8]) -> i32 {
1158    compress_block_with_meta(compcode, clevel, 0, src, dest)
1159}
1160
1161/// Compress a block, passing an additional `meta` byte to plugin/user-defined codecs.
1162///
1163/// `meta` is forwarded only to user codecs; the built-in codecs ignore it.
1164/// Returns the number of compressed bytes, or 0 if the block does not compress.
1165pub fn compress_block_with_meta(
1166    compcode: u8,
1167    clevel: u8,
1168    meta: u8,
1169    src: &[u8],
1170    dest: &mut [u8],
1171) -> i32 {
1172    compress_block_with_context(compcode, clevel, meta, src, dest, None)
1173}
1174
1175/// Compress a block, forwarding rich context to registered plugin codecs.
1176pub fn compress_block_with_context(
1177    compcode: u8,
1178    clevel: u8,
1179    meta: u8,
1180    src: &[u8],
1181    dest: &mut [u8],
1182    context: Option<CodecCallbackContext<'_>>,
1183) -> i32 {
1184    match compcode {
1185        BLOSC_BLOSCLZ => blosclz::compress(clevel as i32, src, dest),
1186        BLOSC_LZ4 => lz4_compress(clevel, src, dest),
1187        BLOSC_LZ4HC => lz4hc_compress(clevel, src, dest),
1188        BLOSC_ZLIB => zlib_compress(src, dest, clevel),
1189        BLOSC_ZSTD => zstd_compress(src, dest, clevel),
1190        #[cfg(feature = "plugin-ndlz")]
1191        BLOSC_CODEC_NDLZ => compress_ndlz_plugin_block(meta, src, dest, context.as_ref()),
1192        #[cfg(feature = "plugin-zfp")]
1193        BLOSC_CODEC_ZFP_FIXED_ACCURACY
1194        | BLOSC_CODEC_ZFP_FIXED_PRECISION
1195        | BLOSC_CODEC_ZFP_FIXED_RATE => {
1196            compress_zfp_plugin_block(compcode, meta, src, dest, context.as_ref())
1197        }
1198        _ => match user_codecs()
1199            .read()
1200            .ok()
1201            .and_then(|codecs| codecs.get(&compcode).copied())
1202        {
1203            Some(codec) => {
1204                if matches!(codec.compress, UserCodecCompress::CAbi(None)) {
1205                    return missing_dynamic_codec_callback();
1206                }
1207                let mut callback_context = context.unwrap_or(CodecCallbackContext {
1208                    compcode,
1209                    complib: codec.complib,
1210                    meta,
1211                    clevel,
1212                    cparams: None,
1213                    dparams: None,
1214                    chunk: CodecChunkContext {
1215                        schunk: 0,
1216                        nchunk: -1,
1217                        nblock: -1,
1218                        chunk_source: 0,
1219                        block_offset: 0,
1220                        blocksize: src.len(),
1221                        bsize: src.len(),
1222                    },
1223                    b2nd_metalayer: None,
1224                    user_data: 0,
1225                });
1226                callback_context.compcode = compcode;
1227                callback_context.complib = codec.complib;
1228                callback_context.meta = meta;
1229                callback_context.clevel = clevel;
1230                let result = codec
1231                    .compress
1232                    .run(&mut callback_context, clevel, meta, src, dest);
1233                normalize_user_compress_result(result, dest.len())
1234            }
1235            None => BLOSC2_ERROR_CODEC_SUPPORT,
1236        },
1237    }
1238}
1239
1240/// Compress a block using a preset dictionary.
1241///
1242/// Falls back to plain [`compress_block`] for codecs that do not support
1243/// dictionaries (see [`codec_supports_dict`]).
1244pub fn compress_block_with_dict(
1245    compcode: u8,
1246    clevel: u8,
1247    src: &[u8],
1248    dest: &mut [u8],
1249    dict: &[u8],
1250) -> i32 {
1251    match compcode {
1252        BLOSC_LZ4 => lz4_compress_with_dict(clevel, src, dest, dict),
1253        BLOSC_LZ4HC => lz4hc_compress_with_dict(clevel, src, dest, dict),
1254        BLOSC_ZSTD => zstd_compress_with_dict(src, dest, clevel, dict),
1255        _ => compress_block(compcode, clevel, src, dest),
1256    }
1257}
1258
1259/// Decompress a single block using the codec identified by `compcode`.
1260///
1261/// Returns the number of decompressed bytes written to `dest`, or a
1262/// negative value on error (corrupted input, undersized output, etc.).
1263pub fn decompress_block(compcode: u8, src: &[u8], dest: &mut [u8]) -> i32 {
1264    decompress_block_with_meta(compcode, 0, src, dest)
1265}
1266
1267/// Decompress a block, passing an additional `meta` byte to user-defined codecs.
1268///
1269/// `meta` is forwarded only to user codecs; the built-in codecs ignore it.
1270/// Returns the number of decompressed bytes, or a negative value on error.
1271pub fn decompress_block_with_meta(compcode: u8, meta: u8, src: &[u8], dest: &mut [u8]) -> i32 {
1272    decompress_block_with_context(compcode, meta, src, dest, None)
1273}
1274
1275/// Decompress a block, forwarding rich context to registered plugin codecs.
1276pub fn decompress_block_with_context(
1277    compcode: u8,
1278    meta: u8,
1279    src: &[u8],
1280    dest: &mut [u8],
1281    context: Option<CodecCallbackContext<'_>>,
1282) -> i32 {
1283    match compcode {
1284        BLOSC_BLOSCLZ => blosclz::decompress(src, dest),
1285        BLOSC_LZ4 | BLOSC_LZ4HC => lz4_decompress(src, dest),
1286        BLOSC_ZLIB => zlib_decompress(src, dest),
1287        BLOSC_ZSTD => zstd_decompress(src, dest),
1288        #[cfg(feature = "plugin-ndlz")]
1289        BLOSC_CODEC_NDLZ => decompress_ndlz_plugin_block(meta, src, dest),
1290        #[cfg(feature = "plugin-zfp")]
1291        BLOSC_CODEC_ZFP_FIXED_ACCURACY
1292        | BLOSC_CODEC_ZFP_FIXED_PRECISION
1293        | BLOSC_CODEC_ZFP_FIXED_RATE => {
1294            decompress_zfp_plugin_block(compcode, meta, src, dest, context.as_ref())
1295        }
1296        _ => match user_codecs()
1297            .read()
1298            .ok()
1299            .and_then(|codecs| codecs.get(&compcode).copied())
1300        {
1301            Some(codec) => {
1302                if matches!(codec.decompress, UserCodecDecompress::CAbi(None)) {
1303                    return missing_dynamic_codec_callback();
1304                }
1305                // C-Blosc2 block sizes are int32-sized; user callbacks should
1306                // not see a direct decompression request that cannot be
1307                // represented by the C callback contract.
1308                if i32::try_from(dest.len()).is_err() {
1309                    return BLOSC2_ERROR_DATA;
1310                }
1311                let mut callback_context = context.unwrap_or(CodecCallbackContext {
1312                    compcode,
1313                    complib: codec.complib,
1314                    meta,
1315                    clevel: 0,
1316                    cparams: None,
1317                    dparams: None,
1318                    chunk: CodecChunkContext {
1319                        schunk: 0,
1320                        nchunk: -1,
1321                        nblock: -1,
1322                        chunk_source: 0,
1323                        block_offset: 0,
1324                        blocksize: dest.len(),
1325                        bsize: dest.len(),
1326                    },
1327                    b2nd_metalayer: None,
1328                    user_data: 0,
1329                });
1330                callback_context.compcode = compcode;
1331                callback_context.complib = codec.complib;
1332                callback_context.meta = meta;
1333                normalize_user_decompress_result(
1334                    codec.decompress.run(&mut callback_context, meta, src, dest),
1335                    dest.len(),
1336                )
1337            }
1338            None => BLOSC2_ERROR_CODEC_SUPPORT,
1339        },
1340    }
1341}
1342
1343fn normalize_user_compress_result(result: i32, dest_len: usize) -> i32 {
1344    if result > i32::try_from(dest_len).unwrap_or(i32::MAX) {
1345        BLOSC2_ERROR_WRITE_BUFFER
1346    } else if result < 0 {
1347        BLOSC2_ERROR_DATA
1348    } else {
1349        result
1350    }
1351}
1352
1353fn normalize_user_decompress_result(result: i32, dest_len: usize) -> i32 {
1354    let Ok(dest_len) = i32::try_from(dest_len) else {
1355        return BLOSC2_ERROR_DATA;
1356    };
1357    (result == dest_len)
1358        .then_some(result)
1359        .unwrap_or(BLOSC2_ERROR_DATA)
1360}
1361
1362/// Decompress a block that was compressed with a preset dictionary.
1363///
1364/// Returns 0 for codecs that do not support dictionary decompression.
1365pub fn decompress_block_with_dict(compcode: u8, src: &[u8], dest: &mut [u8], dict: &[u8]) -> i32 {
1366    match compcode {
1367        BLOSC_LZ4 | BLOSC_LZ4HC => lz4_decompress_with_dict(src, dest, dict),
1368        BLOSC_ZSTD => zstd_decompress_with_dict(src, dest, dict),
1369        _ => 0,
1370    }
1371}
1372
1373/// LZ4 fast-mode compression of a single block.
1374fn lz4_compress(clevel: u8, src: &[u8], dest: &mut [u8]) -> i32 {
1375    use lz4_pure::sys::LZ4_compress_fast;
1376
1377    let Some(src_len) = len_as_c_int(src.len()) else {
1378        return 0;
1379    };
1380    let Some(dest_len) = len_as_c_int(dest.len()) else {
1381        return 0;
1382    };
1383    let accel = lz4_acceleration(clevel);
1384    unsafe {
1385        LZ4_compress_fast(
1386            src.as_ptr() as *const c_char,
1387            dest.as_mut_ptr() as *mut c_char,
1388            src_len,
1389            dest_len,
1390            accel,
1391        )
1392    }
1393}
1394
1395fn lz4_acceleration(clevel: u8) -> i32 {
1396    let _ = clevel;
1397    // c-blosc2 computes 10 - clevel in get_accel(), but the non-IPP LZ4
1398    // wrapper overrides that value to 1 before calling LZ4_compress_fast.
1399    1
1400}
1401
1402/// LZ4HC high-compression-mode compression of a single block, parameterized
1403/// by `clevel` (mapped directly to the LZ4HC compression level).
1404fn lz4hc_compress(clevel: u8, src: &[u8], dest: &mut [u8]) -> i32 {
1405    use lz4_pure::sys::LZ4_compress_HC;
1406
1407    if let Some(error) = lz4hc_2gb_limit_result(src.len()) {
1408        return error;
1409    }
1410    let Some(src_len) = len_as_c_int(src.len()) else {
1411        return 0;
1412    };
1413    let Some(dest_len) = len_as_c_int(dest.len()) else {
1414        return 0;
1415    };
1416
1417    unsafe {
1418        LZ4_compress_HC(
1419            src.as_ptr() as *const c_char,
1420            dest.as_mut_ptr() as *mut c_char,
1421            src_len,
1422            dest_len,
1423            i32::from(clevel),
1424        )
1425    }
1426}
1427
1428fn lz4hc_2gb_limit_result(src_len: usize) -> Option<i32> {
1429    (src_len > (2usize << 30)).then_some(BLOSC2_ERROR_2GB_LIMIT)
1430}
1431
1432/// Decompress a block produced by either the LZ4 fast or LZ4HC encoder
1433/// (both share the same wire format).
1434fn lz4_decompress(src: &[u8], dest: &mut [u8]) -> i32 {
1435    match lz4_pure::block::decompress_to_buffer(src, Some(dest.len() as i32), dest) {
1436        Ok(n) if n == dest.len() => n as i32,
1437        Ok(_) | Err(_) => 0,
1438    }
1439}
1440
1441/// Convert a buffer length to the `c_int` type used by the LZ4 C API,
1442/// returning `None` if the length does not fit.
1443fn len_as_c_int(len: usize) -> Option<lz4_pure::sys::c_int> {
1444    lz4_pure::sys::c_int::try_from(len).ok()
1445}
1446
1447/// LZ4 fast-mode compression seeded with a preset dictionary.
1448fn lz4_compress_with_dict(clevel: u8, src: &[u8], dest: &mut [u8], dict: &[u8]) -> i32 {
1449    use lz4_pure::sys::{c_char, LZ4_compress_fast_continue, LZ4_createStream, LZ4_loadDict};
1450
1451    let Some(src_len) = len_as_c_int(src.len()) else {
1452        return 0;
1453    };
1454    let Some(dest_len) = len_as_c_int(dest.len()) else {
1455        return 0;
1456    };
1457    let Some(dict_len) = len_as_c_int(dict.len()) else {
1458        return 0;
1459    };
1460    let accel = lz4_acceleration(clevel);
1461
1462    LZ4_STREAM.with(|slot| {
1463        let mut slot = slot.borrow_mut();
1464        if slot.is_none() {
1465            let stream = unsafe { LZ4_createStream() };
1466            if stream.is_null() {
1467                return 0;
1468            }
1469            *slot = Some(Lz4Stream(stream));
1470        }
1471        let stream = slot
1472            .as_mut()
1473            .map(|stream| stream.0)
1474            .unwrap_or(std::ptr::null_mut());
1475        if stream.is_null() {
1476            return 0;
1477        }
1478        unsafe {
1479            LZ4_loadDict(stream, dict.as_ptr() as *const c_char, dict_len);
1480        }
1481        let written = unsafe {
1482            LZ4_compress_fast_continue(
1483                stream,
1484                src.as_ptr() as *const c_char,
1485                dest.as_mut_ptr() as *mut c_char,
1486                src_len,
1487                dest_len,
1488                accel,
1489            )
1490        };
1491        written
1492    })
1493}
1494
1495/// LZ4HC high-compression-mode compression seeded with a preset dictionary.
1496fn lz4hc_compress_with_dict(clevel: u8, src: &[u8], dest: &mut [u8], dict: &[u8]) -> i32 {
1497    use lz4_pure::sys::{
1498        c_char, LZ4_compress_HC_continue, LZ4_createStreamHC, LZ4_loadDictHC,
1499        LZ4_resetStreamHC_fast,
1500    };
1501
1502    if let Some(error) = lz4hc_2gb_limit_result(src.len()) {
1503        return error;
1504    }
1505    let Some(src_len) = len_as_c_int(src.len()) else {
1506        return 0;
1507    };
1508    let Some(dest_len) = len_as_c_int(dest.len()) else {
1509        return 0;
1510    };
1511    let Some(dict_len) = len_as_c_int(dict.len()) else {
1512        return 0;
1513    };
1514
1515    LZ4HC_STREAM.with(|slot| {
1516        let mut slot = slot.borrow_mut();
1517        if slot.is_none() {
1518            let stream = unsafe { LZ4_createStreamHC() };
1519            if stream.is_null() {
1520                return 0;
1521            }
1522            *slot = Some(Lz4hcStream(stream));
1523        }
1524        let stream = slot
1525            .as_mut()
1526            .map(|stream| stream.0)
1527            .unwrap_or(std::ptr::null_mut());
1528        if stream.is_null() {
1529            return 0;
1530        }
1531        unsafe {
1532            LZ4_resetStreamHC_fast(stream, i32::from(clevel));
1533            LZ4_loadDictHC(stream, dict.as_ptr() as *const c_char, dict_len);
1534        }
1535        let written = unsafe {
1536            LZ4_compress_HC_continue(
1537                stream,
1538                src.as_ptr() as *const c_char,
1539                dest.as_mut_ptr() as *mut c_char,
1540                src_len,
1541                dest_len,
1542            )
1543        };
1544        written
1545    })
1546}
1547
1548/// Decompress an LZ4 block produced against a preset dictionary.
1549fn lz4_decompress_with_dict(src: &[u8], dest: &mut [u8], dict: &[u8]) -> i32 {
1550    use lz4_pure::sys::{c_char, LZ4_decompress_safe_usingDict};
1551
1552    let Some(src_len) = len_as_c_int(src.len()) else {
1553        return 0;
1554    };
1555    let Some(dest_len) = len_as_c_int(dest.len()) else {
1556        return 0;
1557    };
1558    let Some(dict_len) = len_as_c_int(dict.len()) else {
1559        return 0;
1560    };
1561
1562    let written = unsafe {
1563        LZ4_decompress_safe_usingDict(
1564            src.as_ptr() as *const c_char,
1565            dest.as_mut_ptr() as *mut c_char,
1566            src_len,
1567            dest_len,
1568            dict.as_ptr() as *const c_char,
1569            dict_len,
1570        )
1571    };
1572    if written == dest_len {
1573        written
1574    } else {
1575        0
1576    }
1577}
1578
1579/// Zlib (deflate) compression of a single block.
1580///
1581/// `clevel` (0–9) is passed directly to flate2's `Compression`.
1582fn zlib_compress(src: &[u8], dest: &mut [u8], clevel: u8) -> i32 {
1583    use flate2::Compression;
1584
1585    // Use compress directly into dest buffer via flate2's low-level API
1586    let level = Compression::new(clevel as u32);
1587    let mut compress = flate2::Compress::new(level, true);
1588
1589    let status = compress.compress(src, dest, flate2::FlushCompress::Finish);
1590
1591    match status {
1592        Ok(flate2::Status::StreamEnd) => compress.total_out() as i32,
1593        Ok(flate2::Status::Ok | flate2::Status::BufError) => {
1594            // Output buffer too small or incomplete
1595            0
1596        }
1597        Err(_) => 0,
1598    }
1599}
1600
1601/// Decompress a single zlib-encoded block.
1602fn zlib_decompress(src: &[u8], dest: &mut [u8]) -> i32 {
1603    use flate2::Decompress;
1604    use flate2::FlushDecompress;
1605
1606    let mut decompress = Decompress::new(true);
1607    match decompress.decompress(src, dest, FlushDecompress::Finish) {
1608        Ok(flate2::Status::StreamEnd) => decompress.total_out() as i32,
1609        Ok(_) => 0,
1610        Err(_) => 0,
1611    }
1612}
1613
1614/// Map blosc clevel (0..=9) to the underlying zstd compression level,
1615/// matching `zstd_wrap_compress` in c-blosc2/blosc/blosc2.c:543.
1616///
1617/// C formula: `clevel = (clevel < 9) ? clevel * 2 - 1 : ZSTD_maxCLevel();`
1618/// which gives: 0→-1, 1→1, 2→3, 3→5, 4→7, 5→9, 6→11, 7→13, 8→15, 9→22.
1619fn blosc_clevel_to_zstd(clevel: u8) -> i32 {
1620    if clevel < 9 {
1621        // Signed to accommodate blosc 0 → zstd -1 (fastest / negative-level).
1622        (clevel as i32) * 2 - 1
1623    } else {
1624        // ZSTD_maxCLevel() is 22 in upstream zstd (has been stable since 1.0).
1625        22
1626    }
1627}
1628
1629/// Normalize a pure-Rust zstd compressor return code to the blosc convention
1630/// (positive byte count, or 0 on any error / overflow).
1631fn zstd_normalize_result(n: Option<usize>) -> i32 {
1632    match n {
1633        Some(n) if !ERR_isError(n) && n <= i32::MAX as usize => n as i32,
1634        _ => 0,
1635    }
1636}
1637
1638/// Zstd compression of a single block, using a thread-local pure-Rust context
1639/// to avoid repeated allocations across calls.
1640fn zstd_compress(src: &[u8], dest: &mut [u8], clevel: u8) -> i32 {
1641    let n = ZSTD_CCTX.with(|slot| -> Option<usize> {
1642        let mut slot = slot.borrow_mut();
1643        if slot.is_none() {
1644            *slot = ZSTD_createCCtx();
1645        }
1646        let cctx = slot.as_mut()?;
1647        Some(ZSTD_compressCCtx(
1648            cctx,
1649            dest,
1650            src,
1651            blosc_clevel_to_zstd(clevel),
1652        ))
1653    });
1654    zstd_normalize_result(n)
1655}
1656
1657/// Zstd compression of a single block, seeded with a preset dictionary.
1658fn zstd_compress_with_dict(src: &[u8], dest: &mut [u8], _clevel: u8, dict: &[u8]) -> i32 {
1659    let n = ZSTD_CCTX.with(|slot| -> Option<usize> {
1660        let mut slot = slot.borrow_mut();
1661        if slot.is_none() {
1662            *slot = ZSTD_createCCtx();
1663        }
1664        let cctx = slot.as_mut()?;
1665        Some(ZSTD_compress_usingDict(cctx, dest, src, dict, 1))
1666    });
1667    zstd_normalize_result(n)
1668}
1669
1670/// Decompress a single zstd-encoded block via a thread-local decoder context.
1671fn zstd_decompress(src: &[u8], dest: &mut [u8]) -> i32 {
1672    let n = ZSTD_DCTX.with(|slot| {
1673        let mut slot = slot.borrow_mut();
1674        let (dctx, entropy_rep, xxh) = &mut *slot;
1675        *entropy_rep = ZSTD_decoder_entropy_rep::default();
1676        ZSTD_decompressDCtx(dctx, entropy_rep, xxh, dest, src)
1677    });
1678    if ERR_isError(n) {
1679        0
1680    } else {
1681        n as i32
1682    }
1683}
1684
1685/// Decompress a zstd block that was compressed with the given preset dictionary.
1686fn zstd_decompress_with_dict(src: &[u8], dest: &mut [u8], dict: &[u8]) -> i32 {
1687    let n = ZSTD_DICT_DCTX.with(|slot| {
1688        let mut dctx = slot.borrow_mut();
1689        let Some(ddict) = ZSTD_createDDict(dict) else {
1690            return ERROR(ErrorCode::DictionaryCorrupted);
1691        };
1692        let n = zstd_decompress_using_ddict_with_active_entropy(&mut dctx, dest, src, &ddict);
1693        if ERR_isError(n) {
1694            if zstd_dict_has_magic(dict) {
1695                *dctx = ZSTD_createDCtx();
1696                let load = ZSTD_DCtx_loadDictionary(&mut dctx, dict);
1697                if !ERR_isError(load) {
1698                    let mut entropy_rep = ZSTD_decoder_entropy_rep::default();
1699                    let mut xxh = XXH64_state_t::default();
1700                    let n = ZSTD_decompressDCtx(&mut dctx, &mut entropy_rep, &mut xxh, dest, src);
1701                    if !ERR_isError(n) {
1702                        return n;
1703                    }
1704                }
1705            }
1706            let n = ZSTD_decompress_usingDict(&mut dctx, dest, src, dict);
1707            if ERR_isError(n) {
1708                let content = ZSTD_DDict_dictContent(&ddict);
1709                ZSTD_decompress_usingDict(&mut dctx, dest, src, content)
1710            } else {
1711                n
1712            }
1713        } else {
1714            n
1715        }
1716    });
1717    if ERR_isError(n) {
1718        0
1719    } else {
1720        n as i32
1721    }
1722}
1723
1724fn zstd_decompress_using_ddict_with_active_entropy(
1725    dctx: &mut ZSTD_DCtx,
1726    dest: &mut [u8],
1727    src: &[u8],
1728    ddict: &ZSTD_DDict,
1729) -> usize {
1730    let frame_dict_id = ZSTD_getDictID_fromFrame(src);
1731    let ddict_id = ZSTD_getDictID_fromDDict(ddict);
1732    if frame_dict_id != 0 && ddict_id != 0 && frame_dict_id != ddict_id {
1733        return ERROR(ErrorCode::DictionaryWrong);
1734    }
1735
1736    let declared = ZSTD_getFrameContentSize(src);
1737    let out_size = if declared == ZSTD_CONTENTSIZE_UNKNOWN || declared == ZSTD_CONTENTSIZE_ERROR {
1738        dest.len()
1739    } else {
1740        declared as usize
1741    };
1742    if out_size > dest.len() {
1743        return ERROR(ErrorCode::DstSizeTooSmall);
1744    }
1745
1746    let rc = ZSTD_decompressBegin(dctx);
1747    if ERR_isError(rc) {
1748        return rc;
1749    }
1750    let content = ZSTD_DDict_dictContent(ddict);
1751    dctx.stream_dict = content.to_vec();
1752    dctx.dictID = ddict_id;
1753    if ZSTD_DDict_entropyPresent(ddict) != 0 {
1754        let mut rep = [0u32; 3];
1755        let rc = ZSTD_loadDEntropy(dctx, &mut rep, ZSTD_DDict_dictBuffer(ddict));
1756        if ERR_isError(rc) {
1757            return rc;
1758        }
1759        dctx.ddict_rep = rep;
1760        dctx.litEntropy = 1;
1761        dctx.fseEntropy = 1;
1762        dctx.fse_ll_fresh = true;
1763        dctx.fse_of_fresh = true;
1764        dctx.fse_ml_fresh = true;
1765        dctx.ll_default_active = false;
1766        dctx.of_default_active = false;
1767        dctx.ml_default_active = false;
1768    } else {
1769        dctx.litEntropy = 0;
1770        dctx.fseEntropy = 0;
1771        dctx.fse_ll_fresh = false;
1772        dctx.fse_of_fresh = false;
1773        dctx.fse_ml_fresh = false;
1774        dctx.ll_default_active = true;
1775        dctx.of_default_active = true;
1776        dctx.ml_default_active = true;
1777    }
1778
1779    let mut combined = vec![0u8; content.len() + out_size];
1780    combined[..content.len()].copy_from_slice(content);
1781    let mut rep = ZSTD_decoder_entropy_rep {
1782        rep: dctx.ddict_rep,
1783    };
1784    let mut xxh = XXH64_state_t::default();
1785    let mut consumed = 0usize;
1786    let decoded = ZSTD_decompressFrame_withOpStart(
1787        dctx,
1788        &mut rep,
1789        &mut xxh,
1790        &mut combined,
1791        content.len(),
1792        src,
1793        &mut consumed,
1794    );
1795    if ERR_isError(decoded) {
1796        return decoded;
1797    }
1798    dest[..decoded].copy_from_slice(&combined[content.len()..content.len() + decoded]);
1799    decoded
1800}
1801
1802fn zstd_dict_has_magic(dict: &[u8]) -> bool {
1803    const ZSTD_MAGIC_DICTIONARY: u32 = 0xEC30_A437;
1804    dict.get(..4)
1805        .and_then(|bytes| bytes.try_into().ok())
1806        .is_some_and(|bytes| u32::from_le_bytes(bytes) == ZSTD_MAGIC_DICTIONARY)
1807}
1808
1809fn read_u16_le(src: &[u8], pos: usize) -> Option<u16> {
1810    let bytes: [u8; 2] = src.get(pos..pos + 2)?.try_into().ok()?;
1811    Some(u16::from_le_bytes(bytes))
1812}
1813
1814fn read_i32_le(src: &[u8], pos: usize) -> Option<i32> {
1815    let bytes: [u8; 4] = src.get(pos..pos + 4)?.try_into().ok()?;
1816    Some(i32::from_le_bytes(bytes))
1817}
1818
1819fn xxh32_seed1(input: &[u8]) -> u32 {
1820    const PRIME32_1: u32 = 2_654_435_761;
1821    const PRIME32_2: u32 = 2_246_822_519;
1822    const PRIME32_3: u32 = 3_266_489_917;
1823    const PRIME32_4: u32 = 668_265_263;
1824    const PRIME32_5: u32 = 374_761_393;
1825
1826    fn round(acc: u32, lane: u32) -> u32 {
1827        acc.wrapping_add(lane.wrapping_mul(PRIME32_2))
1828            .rotate_left(13)
1829            .wrapping_mul(PRIME32_1)
1830    }
1831
1832    let mut pos = 0usize;
1833    let mut hash;
1834    if input.len() >= 16 {
1835        let mut v1 = 1u32.wrapping_add(PRIME32_1).wrapping_add(PRIME32_2);
1836        let mut v2 = 1u32.wrapping_add(PRIME32_2);
1837        let mut v3 = 1u32;
1838        let mut v4 = 1u32.wrapping_sub(PRIME32_1);
1839        while pos <= input.len() - 16 {
1840            v1 = round(
1841                v1,
1842                u32::from_le_bytes(input[pos..pos + 4].try_into().unwrap()),
1843            );
1844            pos += 4;
1845            v2 = round(
1846                v2,
1847                u32::from_le_bytes(input[pos..pos + 4].try_into().unwrap()),
1848            );
1849            pos += 4;
1850            v3 = round(
1851                v3,
1852                u32::from_le_bytes(input[pos..pos + 4].try_into().unwrap()),
1853            );
1854            pos += 4;
1855            v4 = round(
1856                v4,
1857                u32::from_le_bytes(input[pos..pos + 4].try_into().unwrap()),
1858            );
1859            pos += 4;
1860        }
1861        hash = v1
1862            .rotate_left(1)
1863            .wrapping_add(v2.rotate_left(7))
1864            .wrapping_add(v3.rotate_left(12))
1865            .wrapping_add(v4.rotate_left(18));
1866    } else {
1867        hash = 1u32.wrapping_add(PRIME32_5);
1868    }
1869
1870    hash = hash.wrapping_add(input.len() as u32);
1871    while pos + 4 <= input.len() {
1872        hash = hash
1873            .wrapping_add(
1874                u32::from_le_bytes(input[pos..pos + 4].try_into().unwrap()).wrapping_mul(PRIME32_3),
1875            )
1876            .rotate_left(17)
1877            .wrapping_mul(PRIME32_4);
1878        pos += 4;
1879    }
1880    while pos < input.len() {
1881        hash = hash
1882            .wrapping_add((input[pos] as u32).wrapping_mul(PRIME32_5))
1883            .rotate_left(11)
1884            .wrapping_mul(PRIME32_1);
1885        pos += 1;
1886    }
1887    hash ^= hash >> 15;
1888    hash = hash.wrapping_mul(PRIME32_2);
1889    hash ^= hash >> 13;
1890    hash = hash.wrapping_mul(PRIME32_3);
1891    hash ^ (hash >> 16)
1892}
1893
1894fn compress_ndlz_plugin_block(
1895    meta: u8,
1896    src: &[u8],
1897    dest: &mut [u8],
1898    context: Option<&CodecCallbackContext<'_>>,
1899) -> i32 {
1900    let Some(context) = context else {
1901        return 0;
1902    };
1903    let Some(b2nd_metalayer) = context.b2nd_metalayer else {
1904        return -1;
1905    };
1906    let Ok(b2nd_meta) = B2ndMeta::deserialize(b2nd_metalayer) else {
1907        return -1;
1908    };
1909    if b2nd_meta.shape.len() != 2 || b2nd_meta.blockshape.len() != 2 {
1910        return -1;
1911    }
1912    compress_ndlz_2d_block(
1913        meta,
1914        [b2nd_meta.blockshape[0], b2nd_meta.blockshape[1]],
1915        src,
1916        dest,
1917    )
1918}
1919
1920fn decompress_ndlz_plugin_block(meta: u8, src: &[u8], dest: &mut [u8]) -> i32 {
1921    match meta {
1922        4 | 8 => decompress_ndlz_cell_stream(meta as usize, src, dest),
1923        _ => -1,
1924    }
1925}
1926
1927#[cfg(feature = "plugin-zfp")]
1928fn compress_zfp_plugin_block(
1929    compcode: u8,
1930    meta: u8,
1931    src: &[u8],
1932    dest: &mut [u8],
1933    context: Option<&CodecCallbackContext<'_>>,
1934) -> i32 {
1935    let Some(context) = context else {
1936        return 0;
1937    };
1938    let Some(cparams) = context.cparams else {
1939        return 0;
1940    };
1941    let Ok(desc) = describe_zfp_block(context.b2nd_metalayer, cparams.typesize, src.len()) else {
1942        return BLOSC2_ERROR_FAILURE;
1943    };
1944    let config = zfp_config_for_mode(compcode, meta, desc.scalar_type, desc.dimensionality);
1945    let capacity = config
1946        .maximum_size(desc.scalar_type, &desc.dims[..desc.ndim])
1947        .max(dest.len());
1948    let field =
1949        unsafe { ZfpField::from_raw(src.as_ptr(), src.len(), desc.scalar_type, desc.dims, [0; 4]) };
1950    let mut stream = ZfpBitStream::new(capacity);
1951    let Ok(cbytes) = stream.compress(&config, &field) else {
1952        return 0;
1953    };
1954    if cbytes == 0 || cbytes >= src.len() {
1955        return 0;
1956    }
1957    if cbytes > dest.len() || i32::try_from(cbytes).is_err() {
1958        return 0;
1959    }
1960    dest[..cbytes].copy_from_slice(&stream.as_bytes()[..cbytes]);
1961    cbytes as i32
1962}
1963
1964#[cfg(feature = "plugin-zfp")]
1965fn decompress_zfp_plugin_block(
1966    compcode: u8,
1967    meta: u8,
1968    src: &[u8],
1969    dest: &mut [u8],
1970    context: Option<&CodecCallbackContext<'_>>,
1971) -> i32 {
1972    let Some(context) = context else {
1973        return 0;
1974    };
1975    let Some(dparams) = context.dparams else {
1976        return 0;
1977    };
1978    let Ok(desc) = describe_zfp_block(context.b2nd_metalayer, dparams.typesize, dest.len()) else {
1979        return BLOSC2_ERROR_FAILURE;
1980    };
1981    let config = zfp_config_for_mode(compcode, meta, desc.scalar_type, desc.dimensionality);
1982    let mut field = unsafe {
1983        ZfpFieldMut::from_raw(
1984            dest.as_mut_ptr(),
1985            dest.len(),
1986            desc.scalar_type,
1987            desc.dims,
1988            [0; 4],
1989        )
1990    };
1991    let mut stream = ZfpBitStream::from_bytes(src);
1992    match stream.decompress(&config, &mut field) {
1993        Ok(0) | Err(_) => 0,
1994        Ok(_) => i32::try_from(dest.len()).unwrap_or(BLOSC2_ERROR_DATA),
1995    }
1996}
1997
1998#[cfg(feature = "plugin-zfp")]
1999#[derive(Clone, Copy)]
2000struct ZfpBlockDescriptor {
2001    ndim: usize,
2002    dims: [usize; 4],
2003    scalar_type: ZfpScalarType,
2004    dimensionality: ZfpDimensionality,
2005}
2006
2007#[cfg(feature = "plugin-zfp")]
2008fn describe_zfp_block(
2009    b2nd_metalayer: Option<&[u8]>,
2010    typesize: i32,
2011    data_len: usize,
2012) -> Result<ZfpBlockDescriptor, ()> {
2013    let b2nd_metalayer = b2nd_metalayer.ok_or(())?;
2014    let b2nd_meta = B2ndMeta::deserialize(b2nd_metalayer).map_err(|_| ())?;
2015    let ndim = b2nd_meta.blockshape.len();
2016    let dimensionality = match ndim {
2017        1 => ZfpDimensionality::D1,
2018        2 => ZfpDimensionality::D2,
2019        3 => ZfpDimensionality::D3,
2020        4 => ZfpDimensionality::D4,
2021        _ => return Err(()),
2022    };
2023    let scalar_type = match typesize {
2024        4 => ZfpScalarType::Float,
2025        8 => ZfpScalarType::Double,
2026        _ => return Err(()),
2027    };
2028    let mut dims = [0usize; 4];
2029    let mut values = 1usize;
2030    for i in 0..ndim {
2031        let block_dim = b2nd_meta.blockshape[i];
2032        if block_dim < 4 {
2033            return Err(());
2034        }
2035        let dim = usize::try_from(block_dim).map_err(|_| ())?;
2036        dims[i] = usize::try_from(b2nd_meta.blockshape[ndim - 1 - i]).map_err(|_| ())?;
2037        values = values.checked_mul(dim).ok_or(())?;
2038    }
2039    let expected = values
2040        .checked_mul(usize::try_from(typesize).map_err(|_| ())?)
2041        .ok_or(())?;
2042    if data_len < expected {
2043        return Err(());
2044    }
2045    Ok(ZfpBlockDescriptor {
2046        ndim,
2047        dims,
2048        scalar_type,
2049        dimensionality,
2050    })
2051}
2052
2053#[cfg(feature = "plugin-zfp")]
2054fn zfp_config_for_mode(
2055    compcode: u8,
2056    meta: u8,
2057    scalar_type: ZfpScalarType,
2058    dimensionality: ZfpDimensionality,
2059) -> ZfpConfig {
2060    match compcode {
2061        BLOSC_CODEC_ZFP_FIXED_ACCURACY => ZfpConfig::fixed_accuracy(10f64.powi(meta as i8 as i32)),
2062        BLOSC_CODEC_ZFP_FIXED_PRECISION => {
2063            let offset = 2 * u32::from(dimensionality) + 3;
2064            ZfpConfig::fixed_precision((u32::from(meta) + offset).min(ZFP_MAX_PREC))
2065        }
2066        BLOSC_CODEC_ZFP_FIXED_RATE => {
2067            let ratio = f64::from(meta) / 100.0;
2068            let rate = ratio * scalar_type.size() as f64 * 8.0;
2069            ZfpConfig::fixed_rate(rate, scalar_type, dimensionality, ZfpStreamAlignment::None)
2070        }
2071        _ => ZfpConfig::new(),
2072    }
2073}
2074
2075const NDLZ_HASH_TABLE_SIZE: usize = 1 << 12;
2076
2077fn ndlz_rows_key(cell: &[u8], cell_shape: usize, rows: impl IntoIterator<Item = usize>) -> Vec<u8> {
2078    let mut key = Vec::new();
2079    for row in rows {
2080        let start = row * cell_shape;
2081        key.extend_from_slice(&cell[start..start + cell_shape]);
2082    }
2083    key
2084}
2085
2086fn ndlz_hash_bucket(bytes: &[u8]) -> usize {
2087    (xxh32_seed1(bytes) >> 20) as usize
2088}
2089
2090fn ndlz_table_match(
2091    table: &[usize; NDLZ_HASH_TABLE_SIZE],
2092    bucket: usize,
2093    bytes: &[u8],
2094    dest: &[u8],
2095) -> Option<usize> {
2096    let start = table[bucket];
2097    if start == 0 {
2098        return None;
2099    }
2100    let stored = dest.get(start..start + bytes.len())?;
2101    (stored == bytes).then_some(start)
2102}
2103
2104/// Compress one 2D NDLZ block using C-Blosc2's block wire format.
2105///
2106/// This emits literal cells, same-value cells, full-cell back references, and
2107/// row pair/triple back references to previously emitted literal rows.
2108pub fn compress_ndlz_2d_block(meta: u8, blockshape: [i32; 2], src: &[u8], dest: &mut [u8]) -> i32 {
2109    let cell_shape = match meta {
2110        4 | 8 => meta as usize,
2111        _ => return -1,
2112    };
2113    if blockshape[0] < 0 || blockshape[1] < 0 {
2114        return -1;
2115    }
2116    let rows = blockshape[0] as usize;
2117    let cols = blockshape[1] as usize;
2118    let Some(expected_len) = rows.checked_mul(cols) else {
2119        return -1;
2120    };
2121    if src.len() != expected_len {
2122        return -1;
2123    }
2124    if dest.len() < 1 + 2 * std::mem::size_of::<i32>() {
2125        return -1;
2126    }
2127    if expected_len < cell_shape * cell_shape {
2128        return 0;
2129    }
2130    let min_output_len = 17 + expected_len / (cell_shape * cell_shape) * 2 - 2;
2131    if dest.len() < min_output_len {
2132        return 0;
2133    }
2134
2135    let stop_rows = rows.div_ceil(cell_shape);
2136    let stop_cols = cols.div_ceil(cell_shape);
2137    let mut op = 0usize;
2138    dest[op] = 2;
2139    op += 1;
2140    dest[op..op + 4].copy_from_slice(&blockshape[0].to_le_bytes());
2141    op += 4;
2142    dest[op..op + 4].copy_from_slice(&blockshape[1].to_le_bytes());
2143    op += 4;
2144    let mut tab_cell = [0usize; NDLZ_HASH_TABLE_SIZE];
2145    let mut tab_pair = [0usize; NDLZ_HASH_TABLE_SIZE];
2146    let mut tab_triple = [0usize; NDLZ_HASH_TABLE_SIZE];
2147
2148    for cell_row in 0..stop_rows {
2149        'cell_cols: for cell_col in 0..stop_cols {
2150            let pad_rows = if cell_row == stop_rows - 1 && !rows.is_multiple_of(cell_shape) {
2151                rows % cell_shape
2152            } else {
2153                cell_shape
2154            };
2155            let pad_cols = if cell_col == stop_cols - 1 && !cols.is_multiple_of(cell_shape) {
2156                cols % cell_shape
2157            } else {
2158                cell_shape
2159            };
2160            let orig = cell_row * cell_shape * cols + cell_col * cell_shape;
2161            let full_cell = pad_rows == cell_shape && pad_cols == cell_shape;
2162            if op + cell_shape * cell_shape + 1 > dest.len() {
2163                return 0;
2164            }
2165            let mut cell = Vec::new();
2166            if full_cell {
2167                cell.reserve_exact(cell_shape * cell_shape);
2168                for row in 0..cell_shape {
2169                    let start = orig + row * cols;
2170                    cell.extend_from_slice(&src[start..start + cell_shape]);
2171                }
2172            }
2173
2174            if full_cell && cell.iter().all(|&byte| byte == cell[0]) {
2175                if op + 2 > dest.len() {
2176                    return 0;
2177                }
2178                dest[op] = 0x40;
2179                dest[op + 1] = cell[0];
2180                op += 2;
2181                if op > src.len() {
2182                    return 0;
2183                }
2184                continue;
2185            }
2186
2187            let hash_cell = full_cell.then(|| ndlz_hash_bucket(&cell));
2188            let mut update_triple = [0usize; 6];
2189            let mut hash_triple = [0usize; 6];
2190            let mut update_pair = [0usize; 7];
2191            let mut hash_pair = [0usize; 7];
2192
2193            if full_cell {
2194                if let Some(literal_start) =
2195                    ndlz_table_match(&tab_cell, hash_cell.unwrap(), &cell, dest)
2196                {
2197                    let Some(offset) = op.checked_sub(literal_start) else {
2198                        return -1;
2199                    };
2200                    if offset > 0 && offset < u16::MAX as usize {
2201                        if op + 3 > dest.len() {
2202                            return 0;
2203                        }
2204                        dest[op] = 0xc0;
2205                        dest[op + 1..op + 3].copy_from_slice(&(offset as u16).to_le_bytes());
2206                        op += 3;
2207                        if op > src.len() {
2208                            return 0;
2209                        }
2210                        continue;
2211                    }
2212                }
2213            }
2214
2215            if full_cell {
2216                let token_pos = op;
2217                if cell_shape == 8 {
2218                    for row in 0..=cell_shape - 3 {
2219                        let key = ndlz_rows_key(&cell, cell_shape, row..row + 3);
2220                        let bucket = ndlz_hash_bucket(&key);
2221                        if let Some(ref_start) = ndlz_table_match(&tab_triple, bucket, &key, dest) {
2222                            let Some(offset) = token_pos.checked_sub(ref_start) else {
2223                                return -1;
2224                            };
2225                            let distance = token_pos + row * cell_shape - ref_start;
2226                            if distance > 0 && distance < u16::MAX as usize {
2227                                let literal_rows = cell_shape - 3;
2228                                if op + 3 + literal_rows * cell_shape > dest.len() {
2229                                    return 0;
2230                                }
2231                                dest[op] = (21 << 3) | row as u8;
2232                                dest[op + 1..op + 3]
2233                                    .copy_from_slice(&(offset as u16).to_le_bytes());
2234                                op += 3;
2235                                for literal_row in 0..cell_shape {
2236                                    if literal_row < row || literal_row >= row + 3 {
2237                                        let start = literal_row * cell_shape;
2238                                        dest[op..op + cell_shape]
2239                                            .copy_from_slice(&cell[start..start + cell_shape]);
2240                                        op += cell_shape;
2241                                    }
2242                                }
2243                                if op > src.len() {
2244                                    return 0;
2245                                }
2246                                continue 'cell_cols;
2247                            }
2248                        } else {
2249                            update_triple[row] = token_pos + 1 + row * cell_shape;
2250                            hash_triple[row] = bucket;
2251                        }
2252                    }
2253
2254                    for row in 0..=cell_shape - 2 {
2255                        let key = ndlz_rows_key(&cell, cell_shape, row..row + 2);
2256                        let bucket = ndlz_hash_bucket(&key);
2257                        if let Some(ref_start) = ndlz_table_match(&tab_pair, bucket, &key, dest) {
2258                            let Some(offset) = token_pos.checked_sub(ref_start) else {
2259                                return -1;
2260                            };
2261                            let distance = token_pos + row * cell_shape - ref_start;
2262                            if distance > 0 && distance < u16::MAX as usize {
2263                                let literal_rows = cell_shape - 2;
2264                                if op + 3 + literal_rows * cell_shape > dest.len() {
2265                                    return 0;
2266                                }
2267                                dest[op] = (17 << 3) | row as u8;
2268                                dest[op + 1..op + 3]
2269                                    .copy_from_slice(&(offset as u16).to_le_bytes());
2270                                op += 3;
2271                                for literal_row in 0..cell_shape {
2272                                    if literal_row < row || literal_row >= row + 2 {
2273                                        let start = literal_row * cell_shape;
2274                                        dest[op..op + cell_shape]
2275                                            .copy_from_slice(&cell[start..start + cell_shape]);
2276                                        op += cell_shape;
2277                                    }
2278                                }
2279                                if op > src.len() {
2280                                    return 0;
2281                                }
2282                                continue 'cell_cols;
2283                            }
2284                        } else {
2285                            update_pair[row] = token_pos + 1 + row * cell_shape;
2286                            hash_pair[row] = bucket;
2287                        }
2288                    }
2289                } else {
2290                    for j in 1..cell_shape {
2291                        let first_key = ndlz_rows_key(&cell, cell_shape, [0, j]);
2292                        let first_bucket = ndlz_hash_bucket(&first_key);
2293                        let Some(first_ref_start) =
2294                            ndlz_table_match(&tab_pair, first_bucket, &first_key, dest)
2295                        else {
2296                            continue;
2297                        };
2298                        let Some(first_offset) = token_pos.checked_sub(first_ref_start) else {
2299                            return -1;
2300                        };
2301                        if first_offset == 0 || first_offset >= u16::MAX as usize {
2302                            continue;
2303                        }
2304
2305                        let mut remaining = (1..cell_shape).filter(|&row| row != j);
2306                        let Some(l) = remaining.next() else {
2307                            return -1;
2308                        };
2309                        let Some(m) = remaining.next() else {
2310                            return -1;
2311                        };
2312                        let second_key = ndlz_rows_key(&cell, cell_shape, [l, m]);
2313                        let second_bucket = ndlz_hash_bucket(&second_key);
2314                        let Some(second_ref_start) =
2315                            ndlz_table_match(&tab_pair, second_bucket, &second_key, dest)
2316                        else {
2317                            continue;
2318                        };
2319                        let Some(second_offset) = token_pos.checked_sub(second_ref_start) else {
2320                            return -1;
2321                        };
2322                        let second_distance = token_pos + l * cell_shape - second_ref_start;
2323                        if second_distance == 0 || second_distance >= u16::MAX as usize {
2324                            continue;
2325                        }
2326                        if op + 5 > dest.len() {
2327                            return 0;
2328                        }
2329                        dest[op] = (1 << 5) | ((j as u8) << 3);
2330                        dest[op + 1..op + 3].copy_from_slice(&(first_offset as u16).to_le_bytes());
2331                        dest[op + 3..op + 5].copy_from_slice(&(second_offset as u16).to_le_bytes());
2332                        op += 5;
2333                        if op > src.len() {
2334                            return 0;
2335                        }
2336                        continue 'cell_cols;
2337                    }
2338
2339                    for rows in [[0usize, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]] {
2340                        let key = ndlz_rows_key(&cell, cell_shape, rows);
2341                        let bucket = ndlz_hash_bucket(&key);
2342                        if let Some(ref_start) = ndlz_table_match(&tab_triple, bucket, &key, dest) {
2343                            let Some(offset) = token_pos.checked_sub(ref_start) else {
2344                                return -1;
2345                            };
2346                            let distance = token_pos + rows[0] * cell_shape - ref_start;
2347                            if distance > 0 && distance < u16::MAX as usize {
2348                                if op + 3 + cell_shape > dest.len() {
2349                                    return 0;
2350                                }
2351                                dest[op] = match rows {
2352                                    [1, 2, 3] => 7 << 5,
2353                                    [0, 1, 2] => (7 << 5) | (1 << 3),
2354                                    [0, 1, 3] => (7 << 5) | (2 << 3),
2355                                    [0, 2, 3] => (7 << 5) | (3 << 3),
2356                                    _ => unreachable!(),
2357                                };
2358                                dest[op + 1..op + 3]
2359                                    .copy_from_slice(&(offset as u16).to_le_bytes());
2360                                op += 3;
2361                                for literal_row in 0..cell_shape {
2362                                    if !rows.contains(&literal_row) {
2363                                        let start = literal_row * cell_shape;
2364                                        dest[op..op + cell_shape]
2365                                            .copy_from_slice(&cell[start..start + cell_shape]);
2366                                        op += cell_shape;
2367                                        break;
2368                                    }
2369                                }
2370                                if op > src.len() {
2371                                    return 0;
2372                                }
2373                                continue 'cell_cols;
2374                            }
2375                        } else if rows[1] - rows[0] == 1 && rows[2] - rows[1] == 1 {
2376                            update_triple[rows[0]] = token_pos + 1 + rows[0] * cell_shape;
2377                            hash_triple[rows[0]] = bucket;
2378                        }
2379                    }
2380
2381                    for rows in [[0usize, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]] {
2382                        let key = ndlz_rows_key(&cell, cell_shape, rows);
2383                        let bucket = ndlz_hash_bucket(&key);
2384                        if let Some(ref_start) = ndlz_table_match(&tab_pair, bucket, &key, dest) {
2385                            let Some(offset) = token_pos.checked_sub(ref_start) else {
2386                                return -1;
2387                            };
2388                            let distance = token_pos + rows[0] * cell_shape - ref_start;
2389                            if distance > 0 && distance < u16::MAX as usize {
2390                                if op + 3 + 2 * cell_shape > dest.len() {
2391                                    return 0;
2392                                }
2393                                dest[op] = if rows == [2, 3] {
2394                                    1 << 7
2395                                } else {
2396                                    (1 << 7) | ((rows[0] as u8) << 5) | ((rows[1] as u8) << 3)
2397                                };
2398                                dest[op + 1..op + 3]
2399                                    .copy_from_slice(&(offset as u16).to_le_bytes());
2400                                op += 3;
2401                                for literal_row in 0..cell_shape {
2402                                    if !rows.contains(&literal_row) {
2403                                        let start = literal_row * cell_shape;
2404                                        dest[op..op + cell_shape]
2405                                            .copy_from_slice(&cell[start..start + cell_shape]);
2406                                        op += cell_shape;
2407                                    }
2408                                }
2409                                if op > src.len() {
2410                                    return 0;
2411                                }
2412                                continue 'cell_cols;
2413                            }
2414                        } else if rows[1] - rows[0] == 1 {
2415                            update_pair[rows[0]] = token_pos + 1 + rows[0] * cell_shape;
2416                            hash_pair[rows[0]] = bucket;
2417                        }
2418                    }
2419                }
2420            }
2421
2422            let literal_len = pad_rows * pad_cols;
2423            if op + 1 + literal_len > dest.len() {
2424                return 0;
2425            }
2426            dest[op] = 0;
2427            op += 1;
2428            let literal_start = op;
2429            for row in 0..pad_rows {
2430                let start = orig + row * cols;
2431                dest[op..op + pad_cols].copy_from_slice(&src[start..start + pad_cols]);
2432                op += pad_cols;
2433            }
2434            if full_cell {
2435                tab_cell[hash_cell.unwrap()] = literal_start;
2436                if update_triple[0] != 0 {
2437                    let staged_triples = cell_shape - 2;
2438                    for h in 0..staged_triples {
2439                        tab_triple[hash_triple[h]] = update_triple[h];
2440                    }
2441                }
2442                if update_pair[0] != 0 {
2443                    let staged_pairs = cell_shape - 1;
2444                    for h in 0..staged_pairs {
2445                        tab_pair[hash_pair[h]] = update_pair[h];
2446                    }
2447                }
2448            }
2449
2450            if op > src.len() {
2451                return 0;
2452            }
2453        }
2454    }
2455
2456    op as i32
2457}
2458
2459fn ndlz_copy_rows_from_stream(
2460    cell: &mut [u8],
2461    cell_shape: usize,
2462    src: &[u8],
2463    start: usize,
2464    rows: impl IntoIterator<Item = usize>,
2465) -> Option<()> {
2466    for (row_idx, row) in rows.into_iter().enumerate() {
2467        let src_start = start.checked_add(row_idx.checked_mul(cell_shape)?)?;
2468        let src_end = src_start.checked_add(cell_shape)?;
2469        let dst_start = row.checked_mul(cell_shape)?;
2470        let dst_end = dst_start.checked_add(cell_shape)?;
2471        cell.get_mut(dst_start..dst_end)?
2472            .copy_from_slice(src.get(src_start..src_end)?);
2473    }
2474    Some(())
2475}
2476
2477fn decompress_ndlz_cell_stream(cell_shape: usize, src: &[u8], dest: &mut [u8]) -> i32 {
2478    if src.len() < 8 {
2479        return 0;
2480    }
2481    if src.len() < 9 || src[0] != 2 {
2482        return -1;
2483    }
2484    let Some(rows) = read_i32_le(src, 1) else {
2485        return -1;
2486    };
2487    let Some(cols) = read_i32_le(src, 5) else {
2488        return -1;
2489    };
2490    if rows < 0 || cols < 0 {
2491        return -1;
2492    }
2493    let rows = rows as usize;
2494    let cols = cols as usize;
2495    let Some(expected_len) = rows.checked_mul(cols) else {
2496        return -1;
2497    };
2498    if expected_len > dest.len() {
2499        return 0;
2500    }
2501    dest[..expected_len].fill(0);
2502
2503    let cell_size = cell_shape * cell_shape;
2504    let stop_rows = rows.div_ceil(cell_shape);
2505    let stop_cols = cols.div_ceil(cell_shape);
2506    let mut ip = 9usize;
2507    let mut last_end = 0usize;
2508
2509    for cell_row in 0..stop_rows {
2510        for cell_col in 0..stop_cols {
2511            if ip >= src.len() {
2512                return -1;
2513            }
2514            let pad_rows = if cell_row == stop_rows - 1 && !rows.is_multiple_of(cell_shape) {
2515                rows % cell_shape
2516            } else {
2517                cell_shape
2518            };
2519            let pad_cols = if cell_col == stop_cols - 1 && !cols.is_multiple_of(cell_shape) {
2520                cols % cell_shape
2521            } else {
2522                cell_shape
2523            };
2524
2525            let token_pos = ip;
2526            let token = src[ip];
2527            ip += 1;
2528            let mut cell = vec![0u8; cell_size];
2529
2530            if token == 0 {
2531                let literal_len = pad_rows * pad_cols;
2532                let Some(literal) = src.get(ip..ip + literal_len) else {
2533                    return -1;
2534                };
2535                for row in 0..pad_rows {
2536                    let lit_start = row * pad_cols;
2537                    let dst_start = row * cell_shape;
2538                    cell[dst_start..dst_start + pad_cols]
2539                        .copy_from_slice(&literal[lit_start..lit_start + pad_cols]);
2540                }
2541                ip += literal_len;
2542            } else if token == 0xc0 {
2543                let Some(offset) = read_u16_le(src, ip).map(usize::from) else {
2544                    return -1;
2545                };
2546                let Some(ref_start) = ip.checked_sub(offset + 1) else {
2547                    return -1;
2548                };
2549                if ndlz_copy_rows_from_stream(&mut cell, cell_shape, src, ref_start, 0..cell_shape)
2550                    .is_none()
2551                {
2552                    return -1;
2553                }
2554                ip += 2;
2555            } else if token == 0x40 {
2556                let Some(&value) = src.get(ip) else {
2557                    return -1;
2558                };
2559                cell.fill(value);
2560                ip += 1;
2561            } else if cell_shape == 4 {
2562                if token >= 224 {
2563                    let Some(offset) = read_u16_le(src, ip).map(|v| usize::from(v) + 3) else {
2564                        return -1;
2565                    };
2566                    ip += 2;
2567                    let (i, j, k) = if token >> 3 == 28 {
2568                        (1, 2, 3)
2569                    } else {
2570                        let i = 0;
2571                        if token >> 3 < 30 {
2572                            (i, 1, 2)
2573                        } else if token >> 3 == 30 {
2574                            (i, 1, 3)
2575                        } else {
2576                            (i, 2, 3)
2577                        }
2578                    };
2579                    let Some(ref_start) = ip.checked_sub(offset) else {
2580                        return -1;
2581                    };
2582                    if ndlz_copy_rows_from_stream(&mut cell, cell_shape, src, ref_start, [i, j, k])
2583                        .is_none()
2584                    {
2585                        return -1;
2586                    }
2587                    for row in 0..cell_shape {
2588                        if row != i && row != j && row != k {
2589                            let Some(literal) = src.get(ip..ip + cell_shape) else {
2590                                return -1;
2591                            };
2592                            cell[row * cell_shape..(row + 1) * cell_shape].copy_from_slice(literal);
2593                            ip += cell_shape;
2594                            break;
2595                        }
2596                    }
2597                } else if (128..=191).contains(&token) {
2598                    let Some(offset) = read_u16_le(src, ip).map(|v| usize::from(v) + 3) else {
2599                        return -1;
2600                    };
2601                    ip += 2;
2602                    let (i, j) = if token == 128 {
2603                        (2, 3)
2604                    } else {
2605                        let i = ((token - 128) >> 5) as usize;
2606                        let j = (((token - 128) >> 3) - ((i as u8) << 2)) as usize;
2607                        (i, j)
2608                    };
2609                    let Some(ref_start) = ip.checked_sub(offset) else {
2610                        return -1;
2611                    };
2612                    if ndlz_copy_rows_from_stream(&mut cell, cell_shape, src, ref_start, [i, j])
2613                        .is_none()
2614                    {
2615                        return -1;
2616                    }
2617                    for row in 0..cell_shape {
2618                        if row != i && row != j {
2619                            let Some(literal) = src.get(ip..ip + cell_shape) else {
2620                                return -1;
2621                            };
2622                            cell[row * cell_shape..(row + 1) * cell_shape].copy_from_slice(literal);
2623                            ip += cell_shape;
2624                        }
2625                    }
2626                } else if (40..=63).contains(&token) {
2627                    let Some(offset_1) = read_u16_le(src, ip).map(|v| usize::from(v) + 5) else {
2628                        return -1;
2629                    };
2630                    ip += 2;
2631                    let Some(offset_2) = read_u16_le(src, ip).map(|v| usize::from(v) + 5) else {
2632                        return -1;
2633                    };
2634                    ip += 2;
2635                    let i = 0usize;
2636                    let j = ((token - 32) >> 3) as usize;
2637                    let mut rest = (1..cell_shape).filter(|&row| row != j);
2638                    let Some(l) = rest.next() else {
2639                        return -1;
2640                    };
2641                    let Some(m) = rest.next() else {
2642                        return -1;
2643                    };
2644                    let Some(ref_start_1) = ip.checked_sub(offset_1) else {
2645                        return -1;
2646                    };
2647                    let Some(ref_start_2) = ip.checked_sub(offset_2) else {
2648                        return -1;
2649                    };
2650                    if ndlz_copy_rows_from_stream(&mut cell, cell_shape, src, ref_start_1, [i, j])
2651                        .is_none()
2652                    {
2653                        return -1;
2654                    }
2655                    if ndlz_copy_rows_from_stream(&mut cell, cell_shape, src, ref_start_2, [l, m])
2656                        .is_none()
2657                    {
2658                        return -1;
2659                    }
2660                } else {
2661                    return -1;
2662                }
2663            } else {
2664                let match_type = token >> 3;
2665                if match_type == 21 || match_type == 17 {
2666                    let row = (token & 7) as usize;
2667                    let matched = if match_type == 21 { 3 } else { 2 };
2668                    if row + matched > cell_shape {
2669                        return -1;
2670                    }
2671                    let Some(offset) = read_u16_le(src, ip).map(usize::from) else {
2672                        return -1;
2673                    };
2674                    ip += 2;
2675                    let Some(ref_start) = ip.checked_sub(3 + offset) else {
2676                        return -1;
2677                    };
2678                    if ndlz_copy_rows_from_stream(
2679                        &mut cell,
2680                        cell_shape,
2681                        src,
2682                        ref_start,
2683                        row..row + matched,
2684                    )
2685                    .is_none()
2686                    {
2687                        return -1;
2688                    }
2689                    for literal_row in 0..cell_shape {
2690                        if literal_row < row || literal_row >= row + matched {
2691                            let Some(literal) = src.get(ip..ip + cell_shape) else {
2692                                return -1;
2693                            };
2694                            cell[literal_row * cell_shape..(literal_row + 1) * cell_shape]
2695                                .copy_from_slice(literal);
2696                            ip += cell_shape;
2697                        }
2698                    }
2699                } else {
2700                    return -1;
2701                }
2702            }
2703
2704            let orig = cell_row * cell_shape * cols + cell_col * cell_shape;
2705            for row in 0..pad_rows {
2706                let dst_start = orig + row * cols;
2707                let dst_end = dst_start + pad_cols;
2708                let src_start = row * cell_shape;
2709                dest[dst_start..dst_end].copy_from_slice(&cell[src_start..src_start + pad_cols]);
2710                last_end = dst_end;
2711            }
2712            if ip < token_pos || last_end > dest.len() {
2713                return -1;
2714            }
2715        }
2716    }
2717
2718    if last_end != expected_len {
2719        return -1;
2720    }
2721    expected_len as i32
2722}
2723
2724#[cfg(test)]
2725mod tests {
2726    use super::*;
2727    use std::sync::{
2728        atomic::{AtomicBool, AtomicI32, AtomicU8, AtomicUsize, Ordering},
2729        Mutex,
2730    };
2731
2732    static C_ABI_TEST_LOCK: Mutex<()> = Mutex::new(());
2733    static C_ABI_COMPRESS_INPUT_LEN: AtomicI32 = AtomicI32::new(0);
2734    static C_ABI_COMPRESS_OUTPUT_LEN: AtomicI32 = AtomicI32::new(0);
2735    static C_ABI_COMPRESS_META: AtomicU8 = AtomicU8::new(0);
2736    static C_ABI_COMPRESS_CLEVEL: AtomicU8 = AtomicU8::new(0);
2737    static C_ABI_COMPRESS_COMP_META: AtomicU8 = AtomicU8::new(0);
2738    static C_ABI_COMPRESS_USE_DICT: AtomicI32 = AtomicI32::new(0);
2739    static C_ABI_COMPRESS_TYPESIZE: AtomicI32 = AtomicI32::new(0);
2740    static C_ABI_COMPRESS_BLOCKSIZE: AtomicI32 = AtomicI32::new(0);
2741    static C_ABI_COMPRESS_SPLITMODE: AtomicI32 = AtomicI32::new(0);
2742    static C_ABI_COMPRESS_FILTER_LAST: AtomicU8 = AtomicU8::new(0);
2743    static C_ABI_COMPRESS_INPUT_PTR: AtomicUsize = AtomicUsize::new(0);
2744    static C_ABI_COMPRESS_SCHUNK: AtomicUsize = AtomicUsize::new(0);
2745    static C_ABI_COMPRESS_CHUNK: AtomicUsize = AtomicUsize::new(0);
2746    static C_ABI_COMPRESS_PREFILTER_SET: AtomicBool = AtomicBool::new(false);
2747    static C_ABI_COMPRESS_PREPARAMS: AtomicUsize = AtomicUsize::new(0);
2748    static C_ABI_COMPRESS_TUNER_ID: AtomicI32 = AtomicI32::new(0);
2749    static C_ABI_COMPRESS_INSTR_CODEC: AtomicBool = AtomicBool::new(false);
2750    static C_ABI_COMPRESS_CODEC_PARAMS: AtomicUsize = AtomicUsize::new(0);
2751    static C_ABI_DECOMPRESS_INPUT_LEN: AtomicI32 = AtomicI32::new(0);
2752    static C_ABI_DECOMPRESS_OUTPUT_LEN: AtomicI32 = AtomicI32::new(0);
2753    static C_ABI_DECOMPRESS_META: AtomicU8 = AtomicU8::new(0);
2754    static C_ABI_DECOMPRESS_TYPESIZE: AtomicI32 = AtomicI32::new(0);
2755    static C_ABI_DECOMPRESS_INPUT_PTR: AtomicUsize = AtomicUsize::new(0);
2756    static C_ABI_DECOMPRESS_SCHUNK: AtomicUsize = AtomicUsize::new(0);
2757    static C_ABI_DECOMPRESS_CHUNK: AtomicUsize = AtomicUsize::new(0);
2758    static C_ABI_DECOMPRESS_POSTFILTER_SET: AtomicBool = AtomicBool::new(false);
2759    static C_ABI_DECOMPRESS_POSTPARAMS: AtomicUsize = AtomicUsize::new(0);
2760
2761    unsafe extern "C" fn c_abi_codec_encoder(
2762        input: *const u8,
2763        input_len: i32,
2764        output: *mut u8,
2765        output_len: i32,
2766        meta: u8,
2767        cparams: *mut Blosc2CParams,
2768        chunk: *const c_void,
2769    ) -> i32 {
2770        if input.is_null() || output.is_null() || cparams.is_null() {
2771            return -1;
2772        }
2773        let cparams = unsafe { &*cparams };
2774        C_ABI_COMPRESS_INPUT_LEN.store(input_len, Ordering::SeqCst);
2775        C_ABI_COMPRESS_OUTPUT_LEN.store(output_len, Ordering::SeqCst);
2776        C_ABI_COMPRESS_META.store(meta, Ordering::SeqCst);
2777        C_ABI_COMPRESS_INPUT_PTR.store(input as usize, Ordering::SeqCst);
2778        C_ABI_COMPRESS_CLEVEL.store(cparams.clevel, Ordering::SeqCst);
2779        C_ABI_COMPRESS_COMP_META.store(cparams.compcode_meta, Ordering::SeqCst);
2780        C_ABI_COMPRESS_USE_DICT.store(cparams.use_dict, Ordering::SeqCst);
2781        C_ABI_COMPRESS_TYPESIZE.store(cparams.typesize, Ordering::SeqCst);
2782        C_ABI_COMPRESS_BLOCKSIZE.store(cparams.blocksize, Ordering::SeqCst);
2783        C_ABI_COMPRESS_SPLITMODE.store(cparams.splitmode, Ordering::SeqCst);
2784        C_ABI_COMPRESS_FILTER_LAST.store(cparams.filters[BLOSC2_MAX_FILTERS - 1], Ordering::SeqCst);
2785        C_ABI_COMPRESS_SCHUNK.store(cparams.schunk as usize, Ordering::SeqCst);
2786        C_ABI_COMPRESS_CHUNK.store(chunk as usize, Ordering::SeqCst);
2787        C_ABI_COMPRESS_PREFILTER_SET.store(cparams.prefilter.is_some(), Ordering::SeqCst);
2788        C_ABI_COMPRESS_PREPARAMS.store(cparams.preparams as usize, Ordering::SeqCst);
2789        C_ABI_COMPRESS_TUNER_ID.store(cparams.tuner_id, Ordering::SeqCst);
2790        C_ABI_COMPRESS_INSTR_CODEC.store(cparams.instr_codec, Ordering::SeqCst);
2791        C_ABI_COMPRESS_CODEC_PARAMS.store(cparams.codec_params as usize, Ordering::SeqCst);
2792        if input_len < 0 || output_len < input_len {
2793            return 0;
2794        }
2795        unsafe {
2796            std::ptr::copy_nonoverlapping(input, output, input_len as usize);
2797        }
2798        input_len
2799    }
2800
2801    unsafe extern "C" fn c_abi_codec_decoder(
2802        input: *const u8,
2803        input_len: i32,
2804        output: *mut u8,
2805        output_len: i32,
2806        meta: u8,
2807        dparams: *mut Blosc2DParams,
2808        chunk: *const c_void,
2809    ) -> i32 {
2810        if input.is_null() || output.is_null() || dparams.is_null() {
2811            return -1;
2812        }
2813        let dparams = unsafe { &*dparams };
2814        C_ABI_DECOMPRESS_INPUT_LEN.store(input_len, Ordering::SeqCst);
2815        C_ABI_DECOMPRESS_OUTPUT_LEN.store(output_len, Ordering::SeqCst);
2816        C_ABI_DECOMPRESS_META.store(meta, Ordering::SeqCst);
2817        C_ABI_DECOMPRESS_INPUT_PTR.store(input as usize, Ordering::SeqCst);
2818        C_ABI_DECOMPRESS_TYPESIZE.store(dparams.typesize, Ordering::SeqCst);
2819        C_ABI_DECOMPRESS_SCHUNK.store(dparams.schunk as usize, Ordering::SeqCst);
2820        C_ABI_DECOMPRESS_CHUNK.store(chunk as usize, Ordering::SeqCst);
2821        C_ABI_DECOMPRESS_POSTFILTER_SET.store(dparams.postfilter.is_some(), Ordering::SeqCst);
2822        C_ABI_DECOMPRESS_POSTPARAMS.store(dparams.postparams as usize, Ordering::SeqCst);
2823        if input_len < 0 || output_len < input_len {
2824            return -1;
2825        }
2826        unsafe {
2827            std::ptr::copy_nonoverlapping(input, output, input_len as usize);
2828        }
2829        output_len
2830    }
2831
2832    #[test]
2833    fn blosc_clevel_to_zstd_matches_c_library_mapping() {
2834        // Table from c-blosc2/blosc/blosc2.c zstd_wrap_compress.
2835        // Blosc level → zstd level.
2836        let expected = [
2837            (0, -1),
2838            (1, 1),
2839            (2, 3),
2840            (3, 5),
2841            (4, 7),
2842            (5, 9),
2843            (6, 11),
2844            (7, 13),
2845            (8, 15),
2846            (9, 22),
2847        ];
2848        for (blosc, zstd) in expected {
2849            assert_eq!(
2850                blosc_clevel_to_zstd(blosc),
2851                zstd,
2852                "blosc {blosc} must map to zstd {zstd}"
2853            );
2854        }
2855    }
2856
2857    #[test]
2858    fn lz4_acceleration_matches_current_c_blosc2_fast_mode() {
2859        // c-blosc2/blosc/blosc2.c lz4_wrap_compress overrides get_accel's
2860        // 10 - clevel value to 1 in the non-IPP implementation.
2861        let expected = [
2862            (0, 1),
2863            (1, 1),
2864            (2, 1),
2865            (3, 1),
2866            (4, 1),
2867            (5, 1),
2868            (6, 1),
2869            (7, 1),
2870            (8, 1),
2871            (9, 1),
2872        ];
2873        for (clevel, accel) in expected {
2874            assert_eq!(lz4_acceleration(clevel), accel);
2875        }
2876    }
2877
2878    #[test]
2879    fn lz4_fast_paths_roundtrip_with_clevel_acceleration() {
2880        let data = b"abcdefghijklmnopabcdefghZZZZabcdefghijklmnopabcdefghijklmnop";
2881        let dict = b"abcdefghijklmnop0123456789abcdefghijklmnop0123456789";
2882
2883        let mut low = vec![0; 256];
2884        let mut high = vec![0; 256];
2885        let low_size = lz4_compress(1, data, &mut low);
2886        let high_size = lz4_compress(9, data, &mut high);
2887        assert!(low_size > 0);
2888        assert!(high_size > 0);
2889        let mut restored = vec![0; data.len()];
2890        assert_eq!(
2891            lz4_decompress(&low[..low_size as usize], &mut restored),
2892            data.len() as i32
2893        );
2894        assert_eq!(restored, data);
2895        restored.fill(0);
2896        assert_eq!(
2897            lz4_decompress(&high[..high_size as usize], &mut restored),
2898            data.len() as i32
2899        );
2900        assert_eq!(restored, data);
2901
2902        low.fill(0);
2903        high.fill(0);
2904        let low_size = lz4_compress_with_dict(1, data, &mut low, dict);
2905        let high_size = lz4_compress_with_dict(9, data, &mut high, dict);
2906        assert!(low_size > 0);
2907        assert!(high_size > 0);
2908        restored.fill(0);
2909        assert_eq!(
2910            lz4_decompress_with_dict(&low[..low_size as usize], &mut restored, dict),
2911            data.len() as i32
2912        );
2913        assert_eq!(restored, data);
2914        restored.fill(0);
2915        assert_eq!(
2916            lz4_decompress_with_dict(&high[..high_size as usize], &mut restored, dict),
2917            data.len() as i32
2918        );
2919        assert_eq!(restored, data);
2920    }
2921
2922    #[test]
2923    fn lz4hc_size_guard_returns_c_2gb_limit_sentinel() {
2924        let c_lz4hc_limit = 2usize << 30;
2925        assert_eq!(lz4hc_2gb_limit_result(c_lz4hc_limit), None);
2926        assert_eq!(
2927            lz4hc_2gb_limit_result(c_lz4hc_limit + 1),
2928            Some(BLOSC2_ERROR_2GB_LIMIT)
2929        );
2930    }
2931
2932    #[test]
2933    fn zlib_decompress_returns_actual_size_for_oversized_output_like_c() {
2934        let data = b"zlib payload";
2935        let mut compressed = vec![0; 128];
2936        let cbytes = zlib_compress(data, &mut compressed, 5);
2937        assert!(cbytes > 0);
2938
2939        let mut decoded = vec![0xaa; data.len() + 8];
2940        assert_eq!(
2941            zlib_decompress(&compressed[..cbytes as usize], &mut decoded),
2942            data.len() as i32
2943        );
2944        assert_eq!(&decoded[..data.len()], data);
2945        assert_eq!(&decoded[data.len()..], &[0xaa; 8]);
2946    }
2947
2948    #[test]
2949    fn zstd_decompress_returns_actual_size_for_oversized_output_like_c() {
2950        let data = b"zstd payload";
2951        let mut compressed = vec![0; 128];
2952        let cbytes = zstd_compress(data, &mut compressed, 5);
2953        assert!(cbytes > 0);
2954
2955        let mut decoded = vec![0xaa; data.len() + 8];
2956        assert_eq!(
2957            zstd_decompress(&compressed[..cbytes as usize], &mut decoded),
2958            data.len() as i32
2959        );
2960        assert_eq!(&decoded[..data.len()], data);
2961        assert_eq!(&decoded[data.len()..], &[0xaa; 8]);
2962    }
2963
2964    #[test]
2965    fn zstd_dict_decompress_returns_actual_size_for_oversized_output_like_c() {
2966        let dict = b"payload dictionary payload dictionary";
2967        let data = b"dictionary payload";
2968        let mut compressed = vec![0; 128];
2969        let cbytes = zstd_compress_with_dict(data, &mut compressed, 5, dict);
2970        assert!(cbytes > 0);
2971
2972        let mut decoded = vec![0xaa; data.len() + 8];
2973        assert_eq!(
2974            zstd_decompress_with_dict(&compressed[..cbytes as usize], &mut decoded, dict),
2975            data.len() as i32
2976        );
2977        assert_eq!(&decoded[..data.len()], data);
2978        assert_eq!(&decoded[data.len()..], &[0xaa; 8]);
2979    }
2980
2981    #[test]
2982    fn known_global_codec_metadata_matches_c_registry() {
2983        for (code, _name) in [
2984            (BLOSC_CODEC_NDLZ, "ndlz"),
2985            (BLOSC_CODEC_ZFP_FIXED_ACCURACY, "zfp_acc"),
2986            (BLOSC_CODEC_ZFP_FIXED_PRECISION, "zfp_prec"),
2987            (BLOSC_CODEC_ZFP_FIXED_RATE, "zfp_rate"),
2988            (BLOSC_CODEC_OPENHTJ2K, "openhtj2k"),
2989            (BLOSC_CODEC_GROK, "grok"),
2990            (BLOSC_CODEC_OPENZL, "openzl"),
2991        ] {
2992            assert!(is_known_global_codec(code));
2993        }
2994        for (code, name) in [
2995            (BLOSC_CODEC_NDLZ, "ndlz"),
2996            (BLOSC_CODEC_ZFP_FIXED_ACCURACY, "zfp_acc"),
2997            (BLOSC_CODEC_ZFP_FIXED_PRECISION, "zfp_prec"),
2998            (BLOSC_CODEC_ZFP_FIXED_RATE, "zfp_rate"),
2999            (BLOSC_CODEC_OPENHTJ2K, "openhtj2k"),
3000            (BLOSC_CODEC_GROK, "grok"),
3001            (BLOSC_CODEC_OPENZL, "openzl"),
3002        ] {
3003            assert!(is_registered_codec(code));
3004            assert_eq!(registered_codec_name(code), Some(name));
3005            assert_eq!(registered_codec_code(name), Some(code));
3006            assert_eq!(registered_codec_version(code), Some(1));
3007            assert_eq!(registered_codec_name_by_complib(code), Some(name));
3008            assert_eq!(
3009                registered_codec_complib_info(name),
3010                Some((code, name, "unknown"))
3011            );
3012        }
3013    }
3014
3015    #[test]
3016    fn unimplemented_or_missing_plugin_codecs_return_c_support_error() {
3017        let mut compressed = vec![0u8; 128];
3018        let mut decompressed = vec![0u8; 128];
3019
3020        assert_eq!(
3021            compress_block(BLOSC_CODEC_OPENHTJ2K, 5, b"payload", &mut compressed),
3022            BLOSC2_ERROR_CODEC_SUPPORT
3023        );
3024        assert_eq!(
3025            decompress_block(BLOSC_CODEC_OPENHTJ2K, b"payload", &mut decompressed),
3026            BLOSC2_ERROR_CODEC_SUPPORT
3027        );
3028        assert_eq!(
3029            compress_block(250, 5, b"payload", &mut compressed),
3030            BLOSC2_ERROR_CODEC_SUPPORT
3031        );
3032        assert_eq!(
3033            decompress_block(250, b"payload", &mut decompressed),
3034            BLOSC2_ERROR_CODEC_SUPPORT
3035        );
3036    }
3037
3038    fn passthrough_codec_compress(_clevel: u8, _meta: u8, src: &[u8], dest: &mut [u8]) -> i32 {
3039        if dest.len() < src.len() {
3040            return 0;
3041        }
3042        dest[..src.len()].copy_from_slice(src);
3043        src.len() as i32
3044    }
3045
3046    fn passthrough_codec_decompress(_meta: u8, src: &[u8], dest: &mut [u8]) -> i32 {
3047        if dest.len() < src.len() {
3048            return 0;
3049        }
3050        dest[..src.len()].copy_from_slice(src);
3051        src.len() as i32
3052    }
3053
3054    fn short_codec_compress(_clevel: u8, _meta: u8, src: &[u8], dest: &mut [u8]) -> i32 {
3055        if src.is_empty() || dest.is_empty() {
3056            return 0;
3057        }
3058        dest[0] = src[0];
3059        1
3060    }
3061
3062    fn short_codec_decompress(_meta: u8, src: &[u8], dest: &mut [u8]) -> i32 {
3063        if src.is_empty() || dest.is_empty() {
3064            return 0;
3065        }
3066        dest[0] = src[0];
3067        1
3068    }
3069
3070    fn negative_codec_compress(_clevel: u8, _meta: u8, _src: &[u8], _dest: &mut [u8]) -> i32 {
3071        -1
3072    }
3073
3074    fn oversized_codec_compress(_clevel: u8, _meta: u8, _src: &[u8], dest: &mut [u8]) -> i32 {
3075        dest.len() as i32 + 1
3076    }
3077
3078    fn exact_codec_decompress(_meta: u8, src: &[u8], dest: &mut [u8]) -> i32 {
3079        for (index, byte) in dest.iter_mut().enumerate() {
3080            *byte = src.get(index).copied().unwrap_or(0);
3081        }
3082        dest.len() as i32
3083    }
3084
3085    fn negative_codec_decompress(_meta: u8, _src: &[u8], _dest: &mut [u8]) -> i32 {
3086        -1
3087    }
3088
3089    fn oversized_codec_decompress(_meta: u8, _src: &[u8], dest: &mut [u8]) -> i32 {
3090        dest.len() as i32 + 1
3091    }
3092
3093    fn context_passthrough_codec_compress(
3094        _ctx: &mut CodecCallbackContext<'_>,
3095        src: &[u8],
3096        dest: &mut [u8],
3097    ) -> i32 {
3098        if dest.len() < src.len() {
3099            return 0;
3100        }
3101        dest[..src.len()].copy_from_slice(src);
3102        src.len() as i32
3103    }
3104
3105    fn context_passthrough_codec_decompress(
3106        _ctx: &mut CodecCallbackContext<'_>,
3107        src: &[u8],
3108        dest: &mut [u8],
3109    ) -> i32 {
3110        if dest.len() < src.len() {
3111            return 0;
3112        }
3113        dest[..src.len()].copy_from_slice(src);
3114        src.len() as i32
3115    }
3116
3117    fn codec_test_context<'a>(b2nd_metalayer: Option<&'a [u8]>) -> CodecCallbackContext<'a> {
3118        CodecCallbackContext {
3119            compcode: BLOSC_CODEC_NDLZ,
3120            complib: None,
3121            meta: 4,
3122            clevel: 5,
3123            cparams: None,
3124            dparams: None,
3125            chunk: CodecChunkContext {
3126                schunk: 0,
3127                nchunk: -1,
3128                nblock: -1,
3129                chunk_source: 0,
3130                block_offset: 0,
3131                blocksize: 0,
3132                bsize: 0,
3133            },
3134            b2nd_metalayer,
3135            user_data: 0,
3136        }
3137    }
3138
3139    #[cfg(feature = "plugin-zfp")]
3140    fn zfp_test_chunk_context() -> CodecChunkContext {
3141        CodecChunkContext {
3142            schunk: 1,
3143            nchunk: 0,
3144            nblock: 0,
3145            chunk_source: 0,
3146            block_offset: 0,
3147            blocksize: 64,
3148            bsize: 64,
3149        }
3150    }
3151
3152    #[test]
3153    fn user_codec_callbacks_receive_existing_destination_bytes_like_c() {
3154        const COMPRESS_CODE: u8 = 240;
3155        const DECOMPRESS_CODE: u8 = 241;
3156
3157        register_codec(
3158            COMPRESS_CODE,
3159            short_codec_compress,
3160            passthrough_codec_decompress,
3161        )
3162        .unwrap();
3163        let mut compressed = vec![0xaa; 4];
3164        assert_eq!(
3165            compress_block(COMPRESS_CODE, 5, &[0x11, 0x22], &mut compressed),
3166            1
3167        );
3168        assert_eq!(compressed, vec![0x11, 0xaa, 0xaa, 0xaa]);
3169
3170        register_codec(
3171            DECOMPRESS_CODE,
3172            passthrough_codec_compress,
3173            short_codec_decompress,
3174        )
3175        .unwrap();
3176        let mut decompressed = vec![0xbb; 4];
3177        assert_eq!(
3178            decompress_block(DECOMPRESS_CODE, &[0x33, 0x44], &mut decompressed),
3179            BLOSC2_ERROR_DATA
3180        );
3181        assert_eq!(decompressed, vec![0x33, 0xbb, 0xbb, 0xbb]);
3182    }
3183
3184    #[test]
3185    fn user_codec_compress_callback_results_match_c() {
3186        const NEGATIVE_CODE: u8 = 242;
3187        const OVERSIZED_CODE: u8 = 243;
3188
3189        register_codec(
3190            NEGATIVE_CODE,
3191            negative_codec_compress,
3192            passthrough_codec_decompress,
3193        )
3194        .unwrap();
3195        let mut compressed = vec![0; 4];
3196        assert_eq!(
3197            compress_block(NEGATIVE_CODE, 5, b"payload", &mut compressed),
3198            BLOSC2_ERROR_DATA
3199        );
3200
3201        register_codec(
3202            OVERSIZED_CODE,
3203            oversized_codec_compress,
3204            passthrough_codec_decompress,
3205        )
3206        .unwrap();
3207        assert_eq!(
3208            compress_block(OVERSIZED_CODE, 5, b"payload", &mut compressed),
3209            BLOSC2_ERROR_WRITE_BUFFER
3210        );
3211    }
3212
3213    #[test]
3214    fn user_codec_decompress_callback_results_match_c() {
3215        const EXACT_CODE: u8 = 244;
3216        const NEGATIVE_CODE: u8 = 245;
3217        const OVERSIZED_CODE: u8 = 246;
3218
3219        register_codec(
3220            EXACT_CODE,
3221            passthrough_codec_compress,
3222            exact_codec_decompress,
3223        )
3224        .unwrap();
3225        let mut decompressed = vec![0; 4];
3226        assert_eq!(
3227            decompress_block(EXACT_CODE, b"ab", &mut decompressed),
3228            decompressed.len() as i32
3229        );
3230        assert_eq!(&decompressed, b"ab\0\0");
3231
3232        register_codec(
3233            NEGATIVE_CODE,
3234            passthrough_codec_compress,
3235            negative_codec_decompress,
3236        )
3237        .unwrap();
3238        assert_eq!(
3239            decompress_block(NEGATIVE_CODE, b"ab", &mut decompressed),
3240            BLOSC2_ERROR_DATA
3241        );
3242
3243        register_codec(
3244            OVERSIZED_CODE,
3245            passthrough_codec_compress,
3246            oversized_codec_decompress,
3247        )
3248        .unwrap();
3249        assert_eq!(
3250            decompress_block(OVERSIZED_CODE, b"ab", &mut decompressed),
3251            BLOSC2_ERROR_DATA
3252        );
3253    }
3254
3255    #[test]
3256    fn user_codec_decompress_rejects_i32_oversized_dest_len() {
3257        // A public decompress_block test would require constructing a valid
3258        // >2 GiB mutable slice. Cover the same C-contract boundary directly;
3259        // real Blosc block sizes are int32-bounded before codec callbacks.
3260        assert_eq!(
3261            normalize_user_decompress_result(0, i32::MAX as usize + 1),
3262            BLOSC2_ERROR_DATA
3263        );
3264    }
3265
3266    #[test]
3267    fn blosc2_codec_registration_rejects_builtin_and_global_ids() {
3268        let global_codec = Blosc2Codec {
3269            compcode: 39,
3270            compname: "public-global-id-rejected",
3271            complib: 39,
3272            version: 1,
3273            encoder: passthrough_codec_compress,
3274            decoder: passthrough_codec_decompress,
3275        };
3276        assert_eq!(
3277            blosc2_register_codec(&global_codec),
3278            BLOSC2_ERROR_CODEC_PARAM
3279        );
3280        assert_eq!(
3281            blosc2_register_codec_c(Some(&global_codec)),
3282            BLOSC2_ERROR_CODEC_PARAM
3283        );
3284
3285        let user_codec = Blosc2Codec {
3286            compcode: 247,
3287            compname: "public-user-id-accepted",
3288            complib: 247,
3289            version: 1,
3290            encoder: passthrough_codec_compress,
3291            decoder: passthrough_codec_decompress,
3292        };
3293        assert_eq!(blosc2_register_codec(&user_codec), BLOSC2_ERROR_SUCCESS);
3294
3295        assert_eq!(
3296            blosc2_register_codec_c(Some(&user_codec)),
3297            BLOSC2_ERROR_SUCCESS
3298        );
3299
3300        let builtin_codec = Blosc2Codec {
3301            compcode: BLOSC_LZ4,
3302            compname: "builtin-id-rejected",
3303            complib: BLOSC_LZ4,
3304            version: 1,
3305            encoder: passthrough_codec_compress,
3306            decoder: passthrough_codec_decompress,
3307        };
3308        assert_eq!(
3309            blosc2_register_codec(&builtin_codec),
3310            BLOSC2_ERROR_CODEC_PARAM
3311        );
3312    }
3313
3314    #[test]
3315    fn known_global_codec_registration_is_descriptor_idempotent_without_dispatch() {
3316        let mut compressed = vec![0; 32];
3317        #[cfg(feature = "plugin-zfp")]
3318        let unsupported_without_static_plugin = 0;
3319        #[cfg(not(feature = "plugin-zfp"))]
3320        let unsupported_without_static_plugin = BLOSC2_ERROR_CODEC_SUPPORT;
3321
3322        assert_eq!(
3323            compress_block(
3324                BLOSC_CODEC_ZFP_FIXED_ACCURACY,
3325                5,
3326                b"payload",
3327                &mut compressed
3328            ),
3329            unsupported_without_static_plugin
3330        );
3331        assert_eq!(
3332            registered_codec_name(BLOSC_CODEC_ZFP_FIXED_ACCURACY),
3333            Some("zfp_acc")
3334        );
3335
3336        assert_eq!(
3337            register_global_codec_with_metadata(
3338                BLOSC_CODEC_ZFP_FIXED_ACCURACY,
3339                "zfp_acc",
3340                BLOSC_CODEC_ZFP_FIXED_ACCURACY,
3341                1,
3342                passthrough_codec_compress,
3343                passthrough_codec_decompress,
3344            ),
3345            Ok(())
3346        );
3347        assert_eq!(
3348            register_private_codec_with_metadata(
3349                BLOSC_CODEC_ZFP_FIXED_ACCURACY,
3350                "zfp_acc",
3351                BLOSC_CODEC_ZFP_FIXED_ACCURACY,
3352                1,
3353                passthrough_codec_compress,
3354                passthrough_codec_decompress,
3355            ),
3356            Ok(())
3357        );
3358        assert_eq!(
3359            register_global_context_codec_with_metadata(
3360                BLOSC_CODEC_ZFP_FIXED_ACCURACY,
3361                "zfp_acc",
3362                BLOSC_CODEC_ZFP_FIXED_ACCURACY,
3363                1,
3364                context_passthrough_codec_compress,
3365                context_passthrough_codec_decompress,
3366            ),
3367            Ok(())
3368        );
3369
3370        assert_eq!(
3371            registered_codec_name(BLOSC_CODEC_ZFP_FIXED_ACCURACY),
3372            Some("zfp_acc")
3373        );
3374        assert_eq!(
3375            registered_codec_version(BLOSC_CODEC_ZFP_FIXED_ACCURACY),
3376            Some(1)
3377        );
3378        assert_eq!(
3379            registered_codec_complib_info("zfp_acc"),
3380            Some((BLOSC_CODEC_ZFP_FIXED_ACCURACY, "zfp_acc", "unknown"))
3381        );
3382        assert_eq!(
3383            compress_block(
3384                BLOSC_CODEC_ZFP_FIXED_ACCURACY,
3385                5,
3386                b"payload",
3387                &mut compressed
3388            ),
3389            unsupported_without_static_plugin
3390        );
3391        let mut decompressed = vec![0; 7];
3392        #[cfg(feature = "plugin-zfp")]
3393        let unsupported_decompress_without_static_plugin = 0;
3394        #[cfg(not(feature = "plugin-zfp"))]
3395        let unsupported_decompress_without_static_plugin = BLOSC2_ERROR_CODEC_SUPPORT;
3396        assert_eq!(
3397            decompress_block(
3398                BLOSC_CODEC_ZFP_FIXED_ACCURACY,
3399                b"payload",
3400                &mut decompressed
3401            ),
3402            unsupported_decompress_without_static_plugin
3403        );
3404    }
3405
3406    #[test]
3407    fn known_global_codec_registration_is_idempotent_for_builtin_dispatch_ids() {
3408        assert_eq!(
3409            register_global_codec_with_metadata(
3410                BLOSC_CODEC_NDLZ,
3411                "ndlz",
3412                BLOSC_CODEC_NDLZ,
3413                1,
3414                passthrough_codec_compress,
3415                passthrough_codec_decompress,
3416            ),
3417            Ok(())
3418        );
3419    }
3420
3421    #[test]
3422    fn blosc2_register_codec_abi_uses_raw_c_callback_shape() {
3423        let _lock = C_ABI_TEST_LOCK.lock().unwrap();
3424        const CODE: u8 = 170;
3425        let codec = Blosc2CodecAbi {
3426            compcode: CODE,
3427            compname: b"raw-c-abi-codec\0".as_ptr().cast(),
3428            complib: CODE,
3429            version: 3,
3430            encoder: Some(c_abi_codec_encoder),
3431            decoder: Some(c_abi_codec_decoder),
3432        };
3433        assert_eq!(
3434            blosc2_register_codec_abi(&codec as *const Blosc2CodecAbi),
3435            BLOSC2_ERROR_SUCCESS
3436        );
3437        assert_eq!(
3438            blosc2_register_codec_abi(std::ptr::null()),
3439            BLOSC2_ERROR_INVALID_PARAM
3440        );
3441        assert_eq!(registered_codec_name(CODE), Some("raw-c-abi-codec"));
3442        assert_eq!(registered_codec_version(CODE), Some(3));
3443
3444        C_ABI_COMPRESS_INPUT_LEN.store(0, Ordering::SeqCst);
3445        C_ABI_DECOMPRESS_INPUT_LEN.store(0, Ordering::SeqCst);
3446        let mut source_chunk = vec![0u8; 128];
3447        source_chunk.extend_from_slice(b"c-abi payload");
3448        let src = &source_chunk[128..];
3449        let filtered_block = src.to_vec();
3450        let mut encoded = vec![0; 64];
3451        let cparams = CodecCParamsContext {
3452            compcode: CODE,
3453            compcode_meta: 0x5a,
3454            clevel: 7,
3455            use_dict: 1,
3456            typesize: 4,
3457            blocksize: 0,
3458            splitmode: BLOSC_NEVER_SPLIT,
3459            filters: [BLOSC_NOFILTER; BLOSC2_MAX_FILTERS],
3460            filters_meta: [0; BLOSC2_MAX_FILTERS],
3461            nthreads: 2,
3462            nchunk: 33,
3463            user_data: 0xfeed,
3464            instr_codec: true,
3465            codec_params: 0xc0de,
3466        };
3467        let chunk = CodecChunkContext {
3468            schunk: 0x1234,
3469            nchunk: 33,
3470            nblock: 4,
3471            chunk_source: source_chunk.as_ptr() as usize,
3472            block_offset: 128,
3473            blocksize: 64,
3474            bsize: src.len(),
3475        };
3476        let cbytes = compress_block_with_context(
3477            CODE,
3478            7,
3479            0x5a,
3480            &filtered_block,
3481            &mut encoded,
3482            Some(CodecCallbackContext {
3483                compcode: CODE,
3484                complib: Some(CODE),
3485                meta: 0x5a,
3486                clevel: 7,
3487                cparams: Some(&cparams),
3488                dparams: None,
3489                chunk,
3490                b2nd_metalayer: None,
3491                user_data: 0xfeed,
3492            }),
3493        );
3494        assert_eq!(cbytes, src.len() as i32);
3495        assert_eq!(&encoded[..src.len()], src);
3496        assert_eq!(
3497            C_ABI_COMPRESS_INPUT_LEN.load(Ordering::SeqCst),
3498            src.len() as i32
3499        );
3500        assert_eq!(
3501            C_ABI_COMPRESS_INPUT_PTR.load(Ordering::SeqCst),
3502            filtered_block.as_ptr() as usize
3503        );
3504        assert_eq!(C_ABI_COMPRESS_OUTPUT_LEN.load(Ordering::SeqCst), 64);
3505        assert_eq!(C_ABI_COMPRESS_META.load(Ordering::SeqCst), 0x5a);
3506        assert_eq!(C_ABI_COMPRESS_CLEVEL.load(Ordering::SeqCst), 7);
3507        assert_eq!(C_ABI_COMPRESS_COMP_META.load(Ordering::SeqCst), 0x5a);
3508        assert_eq!(C_ABI_COMPRESS_USE_DICT.load(Ordering::SeqCst), 1);
3509        assert_eq!(C_ABI_COMPRESS_TYPESIZE.load(Ordering::SeqCst), 4);
3510        assert_eq!(C_ABI_COMPRESS_BLOCKSIZE.load(Ordering::SeqCst), 0);
3511        assert_eq!(
3512            C_ABI_COMPRESS_SPLITMODE.load(Ordering::SeqCst),
3513            BLOSC_NEVER_SPLIT
3514        );
3515        assert_eq!(
3516            C_ABI_COMPRESS_FILTER_LAST.load(Ordering::SeqCst),
3517            BLOSC_NOFILTER
3518        );
3519        assert_eq!(C_ABI_COMPRESS_SCHUNK.load(Ordering::SeqCst), 0x1234);
3520        assert_eq!(
3521            C_ABI_COMPRESS_CHUNK.load(Ordering::SeqCst),
3522            source_chunk.as_ptr() as usize
3523        );
3524        assert!(!C_ABI_COMPRESS_PREFILTER_SET.load(Ordering::SeqCst));
3525        assert_eq!(C_ABI_COMPRESS_PREPARAMS.load(Ordering::SeqCst), 0);
3526        assert_eq!(C_ABI_COMPRESS_TUNER_ID.load(Ordering::SeqCst), 0);
3527        assert!(C_ABI_COMPRESS_INSTR_CODEC.load(Ordering::SeqCst));
3528        assert_eq!(C_ABI_COMPRESS_CODEC_PARAMS.load(Ordering::SeqCst), 0xc0de);
3529
3530        let dparams = CodecDParamsContext {
3531            nthreads: 3,
3532            typesize: 4,
3533            nchunk: 34,
3534            user_data: 0xbeef,
3535        };
3536        let mut decoded = vec![0; src.len()];
3537        let mut compressed_chunk_bytes = vec![0u8; 128];
3538        compressed_chunk_bytes.extend_from_slice(&encoded[..cbytes as usize]);
3539        let compressed_block = encoded[..cbytes as usize].to_vec();
3540        assert_eq!(
3541            decompress_block_with_context(
3542                CODE,
3543                0x5a,
3544                &compressed_block,
3545                &mut decoded,
3546                Some(CodecCallbackContext {
3547                    compcode: CODE,
3548                    complib: Some(CODE),
3549                    meta: 0x5a,
3550                    clevel: 0,
3551                    cparams: None,
3552                    dparams: Some(&dparams),
3553                    chunk: CodecChunkContext {
3554                        nchunk: 34,
3555                        chunk_source: compressed_chunk_bytes.as_ptr() as usize,
3556                        ..chunk
3557                    },
3558                    b2nd_metalayer: None,
3559                    user_data: 0xbeef,
3560                }),
3561            ),
3562            src.len() as i32
3563        );
3564        assert_eq!(decoded, src);
3565        assert_eq!(
3566            C_ABI_DECOMPRESS_INPUT_LEN.load(Ordering::SeqCst),
3567            src.len() as i32
3568        );
3569        assert_eq!(
3570            C_ABI_DECOMPRESS_INPUT_PTR.load(Ordering::SeqCst),
3571            compressed_block.as_ptr() as usize
3572        );
3573        assert_eq!(
3574            C_ABI_DECOMPRESS_OUTPUT_LEN.load(Ordering::SeqCst),
3575            src.len() as i32
3576        );
3577        assert_eq!(C_ABI_DECOMPRESS_META.load(Ordering::SeqCst), 0x5a);
3578        assert_eq!(C_ABI_DECOMPRESS_TYPESIZE.load(Ordering::SeqCst), 4);
3579        assert_eq!(C_ABI_DECOMPRESS_SCHUNK.load(Ordering::SeqCst), 0x1234);
3580        assert_eq!(
3581            C_ABI_DECOMPRESS_CHUNK.load(Ordering::SeqCst),
3582            compressed_chunk_bytes.as_ptr() as usize
3583        );
3584        assert!(!C_ABI_DECOMPRESS_POSTFILTER_SET.load(Ordering::SeqCst));
3585        assert_eq!(C_ABI_DECOMPRESS_POSTPARAMS.load(Ordering::SeqCst), 0);
3586    }
3587
3588    #[test]
3589    fn blosc2_register_codec_abi_fallback_params_match_c_defaults() {
3590        let _lock = C_ABI_TEST_LOCK.lock().unwrap();
3591        const CODE: u8 = 171;
3592        let codec = Blosc2CodecAbi {
3593            compcode: CODE,
3594            compname: b"raw-c-abi-defaults\0".as_ptr().cast(),
3595            complib: CODE,
3596            version: 1,
3597            encoder: Some(c_abi_codec_encoder),
3598            decoder: Some(c_abi_codec_decoder),
3599        };
3600        assert_eq!(
3601            blosc2_register_codec_abi(&codec as *const Blosc2CodecAbi),
3602            BLOSC2_ERROR_SUCCESS
3603        );
3604
3605        let src = b"default params";
3606        let mut encoded = vec![0; 64];
3607        C_ABI_COMPRESS_CHUNK.store(usize::MAX, Ordering::SeqCst);
3608        assert_eq!(
3609            compress_block_with_meta(CODE, 6, 0x23, src, &mut encoded),
3610            src.len() as i32
3611        );
3612        assert_eq!(
3613            C_ABI_COMPRESS_CHUNK.load(Ordering::SeqCst),
3614            src.as_ptr() as usize
3615        );
3616        assert_eq!(C_ABI_COMPRESS_META.load(Ordering::SeqCst), 0x23);
3617        assert_eq!(C_ABI_COMPRESS_CLEVEL.load(Ordering::SeqCst), 6);
3618        assert_eq!(C_ABI_COMPRESS_USE_DICT.load(Ordering::SeqCst), 0);
3619        assert_eq!(C_ABI_COMPRESS_TYPESIZE.load(Ordering::SeqCst), 8);
3620        assert_eq!(C_ABI_COMPRESS_BLOCKSIZE.load(Ordering::SeqCst), 0);
3621        assert_eq!(
3622            C_ABI_COMPRESS_SPLITMODE.load(Ordering::SeqCst),
3623            BLOSC_FORWARD_COMPAT_SPLIT
3624        );
3625        assert_eq!(
3626            C_ABI_COMPRESS_FILTER_LAST.load(Ordering::SeqCst),
3627            BLOSC_SHUFFLE
3628        );
3629        assert!(!C_ABI_COMPRESS_PREFILTER_SET.load(Ordering::SeqCst));
3630        assert_eq!(C_ABI_COMPRESS_PREPARAMS.load(Ordering::SeqCst), 0);
3631        assert_eq!(C_ABI_COMPRESS_TUNER_ID.load(Ordering::SeqCst), 0);
3632        assert!(!C_ABI_COMPRESS_INSTR_CODEC.load(Ordering::SeqCst));
3633        assert_eq!(C_ABI_COMPRESS_CODEC_PARAMS.load(Ordering::SeqCst), 0);
3634
3635        let mut decoded = vec![0; src.len()];
3636        C_ABI_DECOMPRESS_CHUNK.store(usize::MAX, Ordering::SeqCst);
3637        assert_eq!(
3638            decompress_block_with_meta(CODE, 0x23, &encoded[..src.len()], &mut decoded),
3639            src.len() as i32
3640        );
3641        assert_eq!(decoded, src);
3642        assert_eq!(
3643            C_ABI_DECOMPRESS_CHUNK.load(Ordering::SeqCst),
3644            encoded.as_ptr() as usize
3645        );
3646        assert_eq!(C_ABI_DECOMPRESS_TYPESIZE.load(Ordering::SeqCst), 8);
3647        assert!(!C_ABI_DECOMPRESS_POSTFILTER_SET.load(Ordering::SeqCst));
3648        assert_eq!(C_ABI_DECOMPRESS_POSTPARAMS.load(Ordering::SeqCst), 0);
3649    }
3650
3651    #[test]
3652    fn blosc2_register_codec_abi_null_chunk_source_fallback_is_null() {
3653        let _lock = C_ABI_TEST_LOCK.lock().unwrap();
3654        const CODE: u8 = 172;
3655        let codec = Blosc2CodecAbi {
3656            compcode: CODE,
3657            compname: b"raw-c-abi-null-chunk-fallback\0".as_ptr().cast(),
3658            complib: CODE,
3659            version: 1,
3660            encoder: Some(c_abi_codec_encoder),
3661            decoder: Some(c_abi_codec_decoder),
3662        };
3663        assert_eq!(
3664            blosc2_register_codec_abi(&codec as *const Blosc2CodecAbi),
3665            BLOSC2_ERROR_SUCCESS
3666        );
3667
3668        let src = b"null chunk";
3669        let mut encoded = vec![0; 32];
3670        C_ABI_COMPRESS_CHUNK.store(usize::MAX, Ordering::SeqCst);
3671        assert_eq!(
3672            compress_block_with_context(
3673                CODE,
3674                5,
3675                0,
3676                src,
3677                &mut encoded,
3678                Some(CodecCallbackContext {
3679                    compcode: CODE,
3680                    complib: Some(CODE),
3681                    meta: 0,
3682                    clevel: 5,
3683                    cparams: None,
3684                    dparams: None,
3685                    chunk: CodecChunkContext {
3686                        schunk: 0,
3687                        nchunk: -1,
3688                        nblock: 0,
3689                        chunk_source: 0,
3690                        block_offset: src.as_ptr() as usize,
3691                        blocksize: src.len(),
3692                        bsize: src.len(),
3693                    },
3694                    b2nd_metalayer: None,
3695                    user_data: 0,
3696                }),
3697            ),
3698            src.len() as i32
3699        );
3700        assert_eq!(C_ABI_COMPRESS_CHUNK.load(Ordering::SeqCst), 0);
3701
3702        let mut decoded = vec![0; src.len()];
3703        C_ABI_DECOMPRESS_CHUNK.store(usize::MAX, Ordering::SeqCst);
3704        assert_eq!(
3705            decompress_block_with_context(
3706                CODE,
3707                0,
3708                &encoded[..src.len()],
3709                &mut decoded,
3710                Some(CodecCallbackContext {
3711                    compcode: CODE,
3712                    complib: Some(CODE),
3713                    meta: 0,
3714                    clevel: 0,
3715                    cparams: None,
3716                    dparams: None,
3717                    chunk: CodecChunkContext {
3718                        schunk: 0,
3719                        nchunk: -1,
3720                        nblock: 0,
3721                        chunk_source: 0,
3722                        block_offset: encoded.as_ptr() as usize,
3723                        blocksize: src.len(),
3724                        bsize: src.len(),
3725                    },
3726                    b2nd_metalayer: None,
3727                    user_data: 0,
3728                }),
3729            ),
3730            src.len() as i32
3731        );
3732        assert_eq!(decoded, src);
3733        assert_eq!(C_ABI_DECOMPRESS_CHUNK.load(Ordering::SeqCst), 0);
3734    }
3735
3736    #[test]
3737    fn blosc2_register_codec_abi_null_callbacks_return_codec_support() {
3738        let _lock = C_ABI_TEST_LOCK.lock().unwrap();
3739        const NULL_ENCODER_CODE: u8 = 173;
3740        let null_encoder = Blosc2CodecAbi {
3741            compcode: NULL_ENCODER_CODE,
3742            compname: b"raw-c-abi-null-encoder\0".as_ptr().cast(),
3743            complib: NULL_ENCODER_CODE,
3744            version: 1,
3745            encoder: None,
3746            decoder: Some(c_abi_codec_decoder),
3747        };
3748        assert_eq!(
3749            blosc2_register_codec_abi(&null_encoder as *const Blosc2CodecAbi),
3750            BLOSC2_ERROR_SUCCESS
3751        );
3752        let mut encoded = vec![0; 32];
3753        assert_eq!(
3754            compress_block(NULL_ENCODER_CODE, 5, b"payload", &mut encoded),
3755            BLOSC2_ERROR_CODEC_SUPPORT
3756        );
3757
3758        const NULL_DECODER_CODE: u8 = 174;
3759        let null_decoder = Blosc2CodecAbi {
3760            compcode: NULL_DECODER_CODE,
3761            compname: b"raw-c-abi-null-decoder\0".as_ptr().cast(),
3762            complib: NULL_DECODER_CODE,
3763            version: 1,
3764            encoder: Some(c_abi_codec_encoder),
3765            decoder: None,
3766        };
3767        assert_eq!(
3768            blosc2_register_codec_abi(&null_decoder as *const Blosc2CodecAbi),
3769            BLOSC2_ERROR_SUCCESS
3770        );
3771        let mut decoded = vec![0; 7];
3772        assert_eq!(
3773            decompress_block(NULL_DECODER_CODE, b"payload", &mut decoded),
3774            BLOSC2_ERROR_CODEC_SUPPORT
3775        );
3776    }
3777
3778    #[test]
3779    fn blosc2_register_codec_abi_rejects_null_name_and_parses_empty_name() {
3780        let _lock = C_ABI_TEST_LOCK.lock().unwrap();
3781        const CODE: u8 = 175;
3782        let null_name = Blosc2CodecAbi {
3783            compcode: CODE,
3784            compname: std::ptr::null(),
3785            complib: CODE,
3786            version: 1,
3787            encoder: Some(c_abi_codec_encoder),
3788            decoder: Some(c_abi_codec_decoder),
3789        };
3790        assert_eq!(
3791            blosc2_register_codec_abi(&null_name as *const Blosc2CodecAbi),
3792            BLOSC2_ERROR_INVALID_PARAM
3793        );
3794        assert_eq!(blosc2_codec_abi_name(b"\0".as_ptr().cast()), Ok(""));
3795    }
3796
3797    #[test]
3798    fn blosc2_register_codec_abi_duplicate_id_matches_c_name_idempotence() {
3799        let _lock = C_ABI_TEST_LOCK.lock().unwrap();
3800        const CODE: u8 = 176;
3801        let codec = Blosc2CodecAbi {
3802            compcode: CODE,
3803            compname: b"raw-c-abi-duplicate-id\0".as_ptr().cast(),
3804            complib: CODE,
3805            version: 1,
3806            encoder: Some(c_abi_codec_encoder),
3807            decoder: Some(c_abi_codec_decoder),
3808        };
3809        assert_eq!(
3810            blosc2_register_codec_abi(&codec as *const Blosc2CodecAbi),
3811            BLOSC2_ERROR_SUCCESS
3812        );
3813        assert_eq!(
3814            blosc2_register_codec_abi(&codec as *const Blosc2CodecAbi),
3815            BLOSC2_ERROR_SUCCESS
3816        );
3817
3818        let same_callbacks_different_name = Blosc2CodecAbi {
3819            compname: b"raw-c-abi-other-name\0".as_ptr().cast(),
3820            ..codec
3821        };
3822        assert_eq!(
3823            blosc2_register_codec_abi(&same_callbacks_different_name as *const Blosc2CodecAbi),
3824            BLOSC2_ERROR_CODEC_PARAM
3825        );
3826        assert_eq!(registered_codec_name(CODE), Some("raw-c-abi-duplicate-id"));
3827    }
3828
3829    #[test]
3830    fn known_global_codec_registration_rejects_different_or_missing_names() {
3831        assert_eq!(
3832            register_global_codec(
3833                BLOSC_CODEC_ZFP_FIXED_PRECISION,
3834                passthrough_codec_compress,
3835                passthrough_codec_decompress,
3836            ),
3837            Err("Global plugin codec ID already registered")
3838        );
3839        assert_eq!(
3840            register_global_codec_with_metadata(
3841                BLOSC_CODEC_ZFP_FIXED_PRECISION,
3842                "not-zfp-prec",
3843                BLOSC_CODEC_ZFP_FIXED_PRECISION,
3844                1,
3845                passthrough_codec_compress,
3846                passthrough_codec_decompress,
3847            ),
3848            Err("Global plugin codec ID already registered")
3849        );
3850        assert_eq!(
3851            register_private_codec_with_metadata(
3852                BLOSC_CODEC_ZFP_FIXED_PRECISION,
3853                "not-zfp-prec",
3854                BLOSC_CODEC_ZFP_FIXED_PRECISION,
3855                1,
3856                passthrough_codec_compress,
3857                passthrough_codec_decompress,
3858            ),
3859            Err("Private codec ID already registered")
3860        );
3861        assert_eq!(
3862            register_global_context_codec(
3863                BLOSC_CODEC_ZFP_FIXED_PRECISION,
3864                context_passthrough_codec_compress,
3865                context_passthrough_codec_decompress,
3866            ),
3867            Err("Global plugin codec ID already registered")
3868        );
3869    }
3870
3871    #[test]
3872    fn internal_global_codec_registration_accepts_global_ids_like_c_private_path() {
3873        const CODE: u8 = 40;
3874        assert_eq!(
3875            register_global_codec_with_metadata(
3876                CODE,
3877                "private-global-codec",
3878                CODE,
3879                1,
3880                passthrough_codec_compress,
3881                passthrough_codec_decompress,
3882            ),
3883            Ok(())
3884        );
3885        assert_eq!(registered_codec_name(CODE), Some("private-global-codec"));
3886        assert_eq!(
3887            registered_codec_complib_info("private-global-codec"),
3888            Some((CODE, "private-global-codec", "unknown"))
3889        );
3890        assert_eq!(
3891            register_global_codec_with_metadata(
3892                CODE,
3893                "private-global-codec",
3894                CODE,
3895                1,
3896                passthrough_codec_compress,
3897                passthrough_codec_decompress,
3898            ),
3899            Ok(())
3900        );
3901    }
3902
3903    #[test]
3904    fn unnamed_global_and_private_duplicate_registration_is_rejected() {
3905        const GLOBAL_CODE: u8 = 42;
3906        assert_eq!(
3907            register_global_codec(
3908                GLOBAL_CODE,
3909                passthrough_codec_compress,
3910                passthrough_codec_decompress,
3911            ),
3912            Ok(())
3913        );
3914        assert_eq!(
3915            register_global_codec(
3916                GLOBAL_CODE,
3917                passthrough_codec_compress,
3918                passthrough_codec_decompress,
3919            ),
3920            Err("Global plugin codec ID already registered")
3921        );
3922
3923        const PRIVATE_CODE: u8 = 43;
3924        assert_eq!(
3925            register_private_codec(
3926                PRIVATE_CODE,
3927                passthrough_codec_compress,
3928                passthrough_codec_decompress,
3929            ),
3930            Ok(())
3931        );
3932        assert_eq!(
3933            register_private_codec(
3934                PRIVATE_CODE,
3935                passthrough_codec_compress,
3936                passthrough_codec_decompress,
3937            ),
3938            Err("Private codec ID already registered")
3939        );
3940    }
3941
3942    #[test]
3943    fn private_codec_registration_accepts_user_ids_like_c_private_path() {
3944        const CODE: u8 = 251;
3945        assert_eq!(
3946            register_global_codec_with_metadata(
3947                CODE,
3948                "global-helper-user-id-rejected",
3949                CODE,
3950                1,
3951                passthrough_codec_compress,
3952                passthrough_codec_decompress,
3953            ),
3954            Err("Global plugin codec IDs must be in 32..=159")
3955        );
3956        assert_eq!(
3957            register_private_codec_with_metadata(
3958                CODE,
3959                "private-user-range-codec",
3960                CODE,
3961                2,
3962                passthrough_codec_compress,
3963                passthrough_codec_decompress,
3964            ),
3965            Ok(())
3966        );
3967        assert!(is_registered_codec(CODE));
3968        assert_eq!(
3969            registered_codec_name(CODE),
3970            Some("private-user-range-codec")
3971        );
3972        assert_eq!(
3973            registered_codec_code("private-user-range-codec"),
3974            Some(CODE)
3975        );
3976        assert_eq!(registered_codec_version(CODE), Some(2));
3977        assert_eq!(
3978            registered_codec_complib_info("private-user-range-codec"),
3979            Some((CODE, "private-user-range-codec", "unknown"))
3980        );
3981        assert_eq!(
3982            register_private_codec(
3983                BLOSC_LZ4,
3984                passthrough_codec_compress,
3985                passthrough_codec_decompress,
3986            ),
3987            Err("Private codec IDs must be >= 32")
3988        );
3989    }
3990
3991    #[test]
3992    fn registered_codec_complib_lookup_prefers_builtin_libraries_like_c() {
3993        const CODE: u8 = 41;
3994        register_global_codec_with_metadata(
3995            CODE,
3996            "lz4-backed-plugin",
3997            BLOSC_LZ4_LIB,
3998            1,
3999            passthrough_codec_compress,
4000            passthrough_codec_decompress,
4001        )
4002        .unwrap();
4003
4004        assert_eq!(
4005            registered_codec_name_by_complib(BLOSC_LZ4_LIB),
4006            Some(BLOSC_LZ4_LIBNAME)
4007        );
4008        assert_eq!(
4009            registered_codec_complib_info("lz4-backed-plugin"),
4010            Some((BLOSC_LZ4_LIB, BLOSC_LZ4_LIBNAME, "1.10.0"))
4011        );
4012    }
4013
4014    #[test]
4015    fn ndlz_compress_null_context_returns_c_fallback_sentinel() {
4016        let input: Vec<u8> = (1..=16).collect();
4017        let mut encoded = vec![0; 64];
4018        assert_eq!(
4019            compress_block_with_meta(BLOSC_CODEC_NDLZ, 5, 4, &input, &mut encoded),
4020            0
4021        );
4022    }
4023
4024    #[test]
4025    fn ndlz_compress_present_context_requires_valid_b2nd_metadata() {
4026        let input: Vec<u8> = (1..=16).collect();
4027        let mut encoded = vec![0; 64];
4028
4029        assert_eq!(
4030            compress_block_with_context(
4031                BLOSC_CODEC_NDLZ,
4032                5,
4033                4,
4034                &input,
4035                &mut encoded,
4036                Some(codec_test_context(None)),
4037            ),
4038            -1
4039        );
4040        assert_eq!(
4041            compress_block_with_context(
4042                BLOSC_CODEC_NDLZ,
4043                5,
4044                4,
4045                &input,
4046                &mut encoded,
4047                Some(codec_test_context(Some(b"invalid-b2nd"))),
4048            ),
4049            -1
4050        );
4051    }
4052
4053    #[cfg(feature = "plugin-zfp")]
4054    #[test]
4055    fn zfp_fixed_precision_meta_clamps_to_c_max_precision() {
4056        let config = zfp_config_for_mode(
4057            BLOSC_CODEC_ZFP_FIXED_PRECISION,
4058            u8::MAX,
4059            ZfpScalarType::Float,
4060            ZfpDimensionality::D4,
4061        );
4062
4063        assert_eq!(config.max_prec(), ZFP_MAX_PREC);
4064    }
4065
4066    #[cfg(feature = "plugin-zfp")]
4067    #[test]
4068    fn zfp_fixed_rate_roundtrip_uses_b2nd_blockshape_context() {
4069        let b2nd_meta = B2ndMeta::new(vec![4, 4], vec![4, 4], vec![4, 4], "<f4", 0)
4070            .unwrap()
4071            .serialize()
4072            .unwrap();
4073        let cparams = CodecCParamsContext {
4074            compcode: BLOSC_CODEC_ZFP_FIXED_RATE,
4075            compcode_meta: 25,
4076            clevel: 5,
4077            use_dict: 0,
4078            typesize: 4,
4079            blocksize: 64,
4080            splitmode: BLOSC_NEVER_SPLIT,
4081            filters: [BLOSC_NOFILTER; BLOSC2_MAX_FILTERS],
4082            filters_meta: [0; BLOSC2_MAX_FILTERS],
4083            nthreads: 1,
4084            nchunk: 0,
4085            user_data: 0,
4086            instr_codec: false,
4087            codec_params: 0,
4088        };
4089        let dparams = CodecDParamsContext {
4090            nthreads: 1,
4091            typesize: 4,
4092            nchunk: 0,
4093            user_data: 0,
4094        };
4095        let chunk = zfp_test_chunk_context();
4096        let input: Vec<u8> = (0..16)
4097            .flat_map(|i| ((i as f32) * 0.25 + 1.0).to_ne_bytes())
4098            .collect();
4099        let mut encoded = vec![0; 128];
4100        let cbytes = compress_block_with_context(
4101            BLOSC_CODEC_ZFP_FIXED_RATE,
4102            5,
4103            25,
4104            &input,
4105            &mut encoded,
4106            Some(CodecCallbackContext {
4107                compcode: BLOSC_CODEC_ZFP_FIXED_RATE,
4108                complib: None,
4109                meta: 25,
4110                clevel: 5,
4111                cparams: Some(&cparams),
4112                dparams: None,
4113                chunk,
4114                b2nd_metalayer: Some(&b2nd_meta),
4115                user_data: 0,
4116            }),
4117        );
4118        assert!(cbytes > 0 && cbytes < input.len() as i32);
4119
4120        let mut decoded = vec![0; input.len()];
4121        let dbytes = decompress_block_with_context(
4122            BLOSC_CODEC_ZFP_FIXED_RATE,
4123            25,
4124            &encoded[..cbytes as usize],
4125            &mut decoded,
4126            Some(CodecCallbackContext {
4127                compcode: BLOSC_CODEC_ZFP_FIXED_RATE,
4128                complib: None,
4129                meta: 25,
4130                clevel: 5,
4131                cparams: None,
4132                dparams: Some(&dparams),
4133                chunk,
4134                b2nd_metalayer: Some(&b2nd_meta),
4135                user_data: 0,
4136            }),
4137        );
4138        assert_eq!(dbytes, input.len() as i32);
4139        assert!(decoded.iter().any(|&byte| byte != 0));
4140    }
4141
4142    #[test]
4143    fn ndlz_literal_blocks_decompress_for_meta_4_and_8() {
4144        let mut src4 = vec![2];
4145        src4.extend_from_slice(&3i32.to_le_bytes());
4146        src4.extend_from_slice(&4i32.to_le_bytes());
4147        src4.push(0);
4148        src4.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
4149        let mut out4 = vec![0; 12];
4150        assert_eq!(
4151            decompress_block_with_meta(BLOSC_CODEC_NDLZ, 4, &src4, &mut out4),
4152            12
4153        );
4154        assert_eq!(out4, (1..=12).collect::<Vec<_>>());
4155
4156        let mut src8 = vec![2];
4157        src8.extend_from_slice(&2i32.to_le_bytes());
4158        src8.extend_from_slice(&3i32.to_le_bytes());
4159        src8.push(0);
4160        src8.extend_from_slice(&[20, 21, 22, 23, 24, 25]);
4161        let mut out8 = vec![0; 6];
4162        assert_eq!(
4163            decompress_block_with_meta(BLOSC_CODEC_NDLZ, 8, &src8, &mut out8),
4164            6
4165        );
4166        assert_eq!(out8, vec![20, 21, 22, 23, 24, 25]);
4167    }
4168
4169    #[test]
4170    fn ndlz_repeat_cells_and_invalid_meta_are_handled() {
4171        let mut src = vec![2];
4172        src.extend_from_slice(&4i32.to_le_bytes());
4173        src.extend_from_slice(&4i32.to_le_bytes());
4174        src.push(0x40);
4175        src.push(7);
4176        let mut out = vec![0; 16];
4177        assert_eq!(
4178            decompress_block_with_meta(BLOSC_CODEC_NDLZ, 4, &src, &mut out),
4179            16
4180        );
4181        assert_eq!(out, vec![7; 16]);
4182
4183        assert_eq!(
4184            decompress_block_with_meta(BLOSC_CODEC_NDLZ, 5, &src, &mut out),
4185            -1
4186        );
4187    }
4188
4189    #[test]
4190    fn ndlz_short_input_returns_c_zero_sentinel() {
4191        let mut out = vec![0; 16];
4192        assert_eq!(
4193            decompress_block_with_meta(BLOSC_CODEC_NDLZ, 4, &[], &mut out),
4194            0
4195        );
4196        assert_eq!(
4197            decompress_block_with_meta(BLOSC_CODEC_NDLZ, 8, &[2, 0, 0, 0, 0, 0, 0], &mut out),
4198            0
4199        );
4200        assert_eq!(
4201            decompress_block_with_meta(BLOSC_CODEC_NDLZ, 4, &[2, 0, 0, 0, 0, 0, 0, 0], &mut out),
4202            -1
4203        );
4204    }
4205
4206    #[test]
4207    fn ndlz_literal_encoder_returns_c_fallback_for_oversized_blocks() {
4208        for (meta, blockshape, input) in [
4209            (4, [4, 4], (1..=16).collect::<Vec<_>>()),
4210            (8, [8, 8], (20..84).collect::<Vec<_>>()),
4211        ] {
4212            let mut encoded = vec![0; input.len() + 32];
4213            let cbytes = compress_ndlz_2d_block(meta, blockshape, &input, &mut encoded);
4214            assert_eq!(cbytes, 0);
4215        }
4216    }
4217
4218    #[test]
4219    fn ndlz_encoder_returns_zero_for_valid_fallback_cases() {
4220        let small_input = vec![1; 12];
4221        let mut encoded = vec![0; 64];
4222        assert_eq!(
4223            compress_ndlz_2d_block(4, [3, 4], &small_input, &mut encoded),
4224            0
4225        );
4226        let mut no_header_space = vec![0; 8];
4227        assert_eq!(
4228            compress_ndlz_2d_block(4, [3, 4], &small_input, &mut no_header_space),
4229            -1
4230        );
4231
4232        let input: Vec<u8> = (1..=16).collect();
4233        assert_eq!(
4234            compress_ndlz_2d_block(4, [4, 4], &input, &mut no_header_space),
4235            -1
4236        );
4237        let mut too_small_output = vec![0; 16];
4238        assert_eq!(
4239            compress_ndlz_2d_block(4, [4, 4], &input, &mut too_small_output),
4240            0
4241        );
4242        assert_eq!(compress_ndlz_2d_block(4, [4, 4], &input, &mut encoded), 0);
4243        let padded_input: Vec<u8> = (1..=20).collect();
4244        assert_eq!(
4245            compress_ndlz_2d_block(4, [4, 5], &padded_input, &mut encoded),
4246            0
4247        );
4248
4249        let mut encoded = vec![0; 64];
4250        assert_eq!(compress_ndlz_2d_block(5, [4, 4], &input, &mut encoded), -1);
4251        assert_eq!(
4252            compress_ndlz_2d_block(4, [4, 4], &input[..15], &mut encoded),
4253            -1
4254        );
4255        assert_eq!(compress_ndlz_2d_block(4, [-1, 4], &input, &mut encoded), -1);
4256    }
4257
4258    #[test]
4259    fn ndlz_encoder_applies_c_worst_case_cell_guard_before_each_cell() {
4260        let mut input = vec![7; 20];
4261        input[16..].copy_from_slice(&[1, 2, 3, 4]);
4262        let mut encoded = vec![0; 27];
4263
4264        assert_eq!(compress_ndlz_2d_block(4, [4, 5], &input, &mut encoded), 0);
4265    }
4266
4267    #[test]
4268    fn ndlz_encoder_uses_repeat_cell_token_for_full_constant_cells() {
4269        let input = vec![9; 16];
4270        let mut encoded = vec![0; 64];
4271        let cbytes = compress_ndlz_2d_block(4, [4, 4], &input, &mut encoded);
4272        assert_eq!(cbytes, 11);
4273        assert_eq!(&encoded[..11], &[2, 4, 0, 0, 0, 4, 0, 0, 0, 0x40, 9]);
4274
4275        let mut decoded = vec![0; input.len()];
4276        assert_eq!(
4277            decompress_block_with_meta(
4278                BLOSC_CODEC_NDLZ,
4279                4,
4280                &encoded[..cbytes as usize],
4281                &mut decoded,
4282            ),
4283            input.len() as i32
4284        );
4285        assert_eq!(decoded, input);
4286    }
4287
4288    #[test]
4289    fn ndlz_encoder_uses_full_cell_match_tokens_for_repeated_cells() {
4290        let first_cell: Vec<u8> = (1..=16).collect();
4291        let mut input = Vec::with_capacity(32);
4292        for row in 0..4 {
4293            input.extend_from_slice(&first_cell[row * 4..row * 4 + 4]);
4294            input.extend_from_slice(&first_cell[row * 4..row * 4 + 4]);
4295        }
4296
4297        let mut encoded = vec![0; 96];
4298        let cbytes = compress_ndlz_2d_block(4, [4, 8], &input, &mut encoded);
4299        assert_eq!(cbytes, 29);
4300        assert_eq!(encoded[9], 0);
4301        assert_eq!(encoded[26], 0xc0);
4302        assert_eq!(u16::from_le_bytes([encoded[27], encoded[28]]), 16);
4303
4304        let mut decoded = vec![0; input.len()];
4305        assert_eq!(
4306            decompress_block_with_meta(
4307                BLOSC_CODEC_NDLZ,
4308                4,
4309                &encoded[..cbytes as usize],
4310                &mut decoded,
4311            ),
4312            input.len() as i32
4313        );
4314        assert_eq!(decoded, input);
4315    }
4316
4317    #[test]
4318    fn ndlz_encoder_uses_full_cell_match_tokens_for_repeated_8x8_cells() {
4319        let first_cell: Vec<u8> = (0..64).map(|i| ((i * 3 + 1) % 251) as u8).collect();
4320        let mut input = Vec::with_capacity(128);
4321        for row in 0..8 {
4322            input.extend_from_slice(&first_cell[row * 8..row * 8 + 8]);
4323            input.extend_from_slice(&first_cell[row * 8..row * 8 + 8]);
4324        }
4325
4326        let mut encoded = vec![0; 192];
4327        let cbytes = compress_ndlz_2d_block(8, [8, 16], &input, &mut encoded);
4328        assert_eq!(cbytes, 77);
4329        assert_eq!(encoded[9], 0);
4330        assert_eq!(encoded[74], 0xc0);
4331        assert_eq!(u16::from_le_bytes([encoded[75], encoded[76]]), 64);
4332
4333        let mut decoded = vec![0; input.len()];
4334        assert_eq!(
4335            decompress_block_with_meta(
4336                BLOSC_CODEC_NDLZ,
4337                8,
4338                &encoded[..cbytes as usize],
4339                &mut decoded,
4340            ),
4341            input.len() as i32
4342        );
4343        assert_eq!(decoded, input);
4344    }
4345
4346    #[test]
4347    fn ndlz_encoder_uses_row_match_tokens_for_4x4_cells() {
4348        let first_cell: Vec<u8> = (0..16).map(|i| (i + 1) as u8).collect();
4349
4350        let mut triple_input = Vec::with_capacity(64);
4351        for row in 0..4 {
4352            triple_input.extend_from_slice(&first_cell[row * 4..row * 4 + 4]);
4353            let second_row = match row {
4354                0 => &first_cell[4..8],
4355                1 => &first_cell[8..12],
4356                2 => &[90, 91, 92, 93],
4357                _ => &first_cell[12..16],
4358            };
4359            triple_input.extend_from_slice(second_row);
4360            triple_input.extend_from_slice(&[7, 7, 7, 7]);
4361            triple_input.extend_from_slice(&[8, 8, 8, 8]);
4362        }
4363
4364        let mut encoded = vec![0; 96];
4365        let cbytes = compress_ndlz_2d_block(4, [4, 16], &triple_input, &mut encoded);
4366        assert_eq!(cbytes, 37);
4367        assert_eq!(encoded[26], (7 << 5) | (2 << 3));
4368        assert_eq!(u16::from_le_bytes([encoded[27], encoded[28]]), 12);
4369
4370        let mut decoded = vec![0; triple_input.len()];
4371        assert_eq!(
4372            decompress_block_with_meta(
4373                BLOSC_CODEC_NDLZ,
4374                4,
4375                &encoded[..cbytes as usize],
4376                &mut decoded,
4377            ),
4378            triple_input.len() as i32
4379        );
4380        assert_eq!(decoded, triple_input);
4381
4382        let mut pair_input = Vec::with_capacity(64);
4383        for row in 0..4 {
4384            pair_input.extend_from_slice(&first_cell[row * 4..row * 4 + 4]);
4385            let second_row = match row {
4386                0 => &first_cell[0..4],
4387                1 => &[50, 51, 52, 53],
4388                2 => &first_cell[4..8],
4389                _ => &[60, 61, 62, 63],
4390            };
4391            pair_input.extend_from_slice(second_row);
4392            pair_input.extend_from_slice(&[7, 7, 7, 7]);
4393            pair_input.extend_from_slice(&[8, 8, 8, 8]);
4394        }
4395
4396        let cbytes = compress_ndlz_2d_block(4, [4, 16], &pair_input, &mut encoded);
4397        assert_eq!(cbytes, 41);
4398        assert_eq!(encoded[26], (1 << 7) | (2 << 3));
4399        assert_eq!(u16::from_le_bytes([encoded[27], encoded[28]]), 16);
4400
4401        let mut decoded = vec![0; pair_input.len()];
4402        assert_eq!(
4403            decompress_block_with_meta(
4404                BLOSC_CODEC_NDLZ,
4405                4,
4406                &encoded[..cbytes as usize],
4407                &mut decoded,
4408            ),
4409            pair_input.len() as i32
4410        );
4411        assert_eq!(decoded, pair_input);
4412    }
4413
4414    #[test]
4415    fn ndlz_encoder_uses_two_row_pair_match_token_for_4x4_cells() {
4416        let a = [1, 2, 3, 4];
4417        let b = [5, 6, 7, 8];
4418        let c = [9, 10, 11, 12];
4419        let d = [13, 14, 15, 16];
4420        let x = [21, 22, 23, 24];
4421        let y = [25, 26, 27, 28];
4422        let u = [31, 32, 33, 34];
4423        let v = [35, 36, 37, 38];
4424
4425        let rows = [[a, u, a], [b, c, b], [x, d, c], [y, v, d]];
4426        let mut input = Vec::with_capacity(48);
4427        for row in rows {
4428            for cell_row in row {
4429                input.extend_from_slice(&cell_row);
4430            }
4431        }
4432
4433        let mut encoded = vec![0; 96];
4434        let cbytes = compress_ndlz_2d_block(4, [4, 12], &input, &mut encoded);
4435        assert_eq!(cbytes, 48);
4436        assert_eq!(encoded[43], 40);
4437        assert_eq!(u16::from_le_bytes([encoded[44], encoded[45]]), 33);
4438        assert_eq!(u16::from_le_bytes([encoded[46], encoded[47]]), 12);
4439
4440        let mut decoded = vec![0; input.len()];
4441        assert_eq!(
4442            decompress_block_with_meta(BLOSC_CODEC_NDLZ, 4, &encoded[..48], &mut decoded),
4443            input.len() as i32
4444        );
4445        assert_eq!(decoded, input);
4446    }
4447
4448    #[test]
4449    fn ndlz_encoder_uses_row_match_tokens_for_8x8_cells() {
4450        let first_cell: Vec<u8> = (0..64).map(|i| ((i * 7 + 11) % 251) as u8).collect();
4451
4452        let mut triple_input = Vec::with_capacity(128);
4453        for row in 0..8 {
4454            triple_input.extend_from_slice(&first_cell[row * 8..row * 8 + 8]);
4455            let second_row: Vec<u8> = match row {
4456                2 => first_cell[0..8].to_vec(),
4457                3 => first_cell[8..16].to_vec(),
4458                4 => first_cell[16..24].to_vec(),
4459                _ => (0..8).map(|col| (180 + row * 8 + col) as u8).collect(),
4460            };
4461            triple_input.extend_from_slice(&second_row);
4462        }
4463
4464        let mut encoded = vec![0; 192];
4465        let cbytes = compress_ndlz_2d_block(8, [8, 16], &triple_input, &mut encoded);
4466        assert_eq!(cbytes, 117);
4467        assert_eq!(encoded[74], (21 << 3) | 2);
4468        assert_eq!(u16::from_le_bytes([encoded[75], encoded[76]]), 64);
4469
4470        let mut decoded = vec![0; triple_input.len()];
4471        assert_eq!(
4472            decompress_block_with_meta(
4473                BLOSC_CODEC_NDLZ,
4474                8,
4475                &encoded[..cbytes as usize],
4476                &mut decoded,
4477            ),
4478            triple_input.len() as i32
4479        );
4480        assert_eq!(decoded, triple_input);
4481
4482        let mut pair_input = Vec::with_capacity(128);
4483        for row in 0..8 {
4484            pair_input.extend_from_slice(&first_cell[row * 8..row * 8 + 8]);
4485            let second_row: Vec<u8> = match row {
4486                3 => first_cell[0..8].to_vec(),
4487                4 => first_cell[8..16].to_vec(),
4488                _ => (0..8).map(|col| (90 + row * 8 + col) as u8).collect(),
4489            };
4490            pair_input.extend_from_slice(&second_row);
4491        }
4492
4493        let cbytes = compress_ndlz_2d_block(8, [8, 16], &pair_input, &mut encoded);
4494        assert_eq!(cbytes, 125);
4495        assert_eq!(encoded[74], (17 << 3) | 3);
4496        assert_eq!(u16::from_le_bytes([encoded[75], encoded[76]]), 64);
4497
4498        let mut decoded = vec![0; pair_input.len()];
4499        assert_eq!(
4500            decompress_block_with_meta(
4501                BLOSC_CODEC_NDLZ,
4502                8,
4503                &encoded[..cbytes as usize],
4504                &mut decoded,
4505            ),
4506            pair_input.len() as i32
4507        );
4508        assert_eq!(decoded, pair_input);
4509    }
4510
4511    #[test]
4512    fn zstd_at_higher_blosc_level_compresses_better() {
4513        // A quick sanity check: after the mapping fix, blosc level 9 should
4514        // produce a significantly smaller or equal output than level 1 on
4515        // repetitive data. With the old identity mapping, level 9 used zstd
4516        // level 9; with the fix, level 9 uses zstd level 22 (maxCLevel).
4517        let data: Vec<u8> = (0..16384u32).flat_map(|i| (i % 17).to_le_bytes()).collect();
4518        let mut buf1 = vec![0u8; data.len() + 256];
4519        let mut buf9 = vec![0u8; data.len() + 256];
4520
4521        let csize1 = zstd_compress(&data, &mut buf1, 1);
4522        let csize9 = zstd_compress(&data, &mut buf9, 9);
4523
4524        assert!(csize1 > 0 && csize9 > 0, "compression must not fail");
4525        assert!(
4526            csize9 <= csize1,
4527            "level 9 must compress at least as well as level 1 (got {csize9} vs {csize1})"
4528        );
4529    }
4530
4531    #[test]
4532    fn zlib_zstd_direct_decompress_failures_return_c_zero_sentinel() {
4533        let mut dest = vec![0u8; 128];
4534        assert_eq!(
4535            decompress_block(BLOSC_ZLIB, b"not-a-zlib-block", &mut dest),
4536            0
4537        );
4538        let src = b"zlib payload";
4539        let mut compressed = vec![0u8; 128];
4540        let cbytes = zlib_compress(src, &mut compressed, 5);
4541        assert!(cbytes > 0);
4542        compressed.truncate(cbytes as usize);
4543        let mut zlib_out = vec![0u8; src.len()];
4544        assert_eq!(
4545            zlib_decompress(&compressed, &mut zlib_out),
4546            src.len() as i32
4547        );
4548        compressed.extend_from_slice(b"trailing");
4549        assert_eq!(
4550            zlib_decompress(&compressed, &mut zlib_out),
4551            src.len() as i32
4552        );
4553        assert_eq!(
4554            decompress_block(BLOSC_ZSTD, b"not-a-zstd-block", &mut dest),
4555            0
4556        );
4557        assert_eq!(
4558            decompress_block_with_dict(BLOSC_ZSTD, b"not-a-zstd-block", &mut dest, b"dict"),
4559            0
4560        );
4561    }
4562
4563    #[test]
4564    fn zstd_dictionary_matches_c_fixed_cdict_level() {
4565        let dict: Vec<u8> = (0..8192u32)
4566            .flat_map(|i| format!("record-{i:04}-COMMON-PREFIX-COMMON-SUFFIX;").into_bytes())
4567            .collect();
4568        let data: Vec<u8> = (0..16384u32)
4569            .flat_map(|i| {
4570                format!("record-{:04}-COMMON-PREFIX-COMMON-SUFFIX;", i % 8192).into_bytes()
4571            })
4572            .collect();
4573        let mut buf1 = vec![0u8; data.len() + 1024];
4574        let mut buf9 = vec![0u8; data.len() + 1024];
4575
4576        let csize1 = zstd_compress_with_dict(&data, &mut buf1, 1, &dict);
4577        let csize9 = zstd_compress_with_dict(&data, &mut buf9, 9, &dict);
4578
4579        assert!(
4580            csize1 > 0 && csize9 > 0,
4581            "dictionary compression must not fail"
4582        );
4583        assert_eq!(
4584            csize9, csize1,
4585            "C-Blosc2 creates Zstd CDicts at level 1 regardless of Blosc clevel"
4586        );
4587        assert_eq!(&buf9[..csize9 as usize], &buf1[..csize1 as usize]);
4588
4589        let mut restored = vec![0; data.len()];
4590        let dsize = zstd_decompress_with_dict(&buf9[..csize9 as usize], &mut restored, &dict);
4591        assert_eq!(dsize as usize, data.len());
4592        assert_eq!(restored, data);
4593    }
4594
4595    #[test]
4596    fn lz4hc_roundtrips_via_lz4_decoder() {
4597        let data: Vec<u8> = (0..8192u32).flat_map(|i| (i % 64).to_le_bytes()).collect();
4598        let mut compressed = vec![0; data.len() + 1024];
4599
4600        let csize = compress_block(BLOSC_LZ4HC, 9, &data, &mut compressed);
4601        assert!(csize > 0);
4602
4603        let mut decompressed = vec![0; data.len()];
4604        let dsize = decompress_block(
4605            BLOSC_LZ4HC,
4606            &compressed[..csize as usize],
4607            &mut decompressed,
4608        );
4609
4610        assert_eq!(dsize as usize, data.len());
4611        assert_eq!(decompressed, data);
4612
4613        let mut oversized = vec![0; data.len() + 1];
4614        assert_eq!(
4615            decompress_block(BLOSC_LZ4HC, &compressed[..csize as usize], &mut oversized),
4616            0
4617        );
4618        assert_eq!(
4619            decompress_block(BLOSC_LZ4HC, b"bad-lz4-block", &mut decompressed),
4620            0
4621        );
4622    }
4623
4624    #[test]
4625    fn lz4_dictionary_paths_roundtrip() {
4626        let dict = b"abcdefghijklmnop0123456789abcdefghijklmnop0123456789";
4627        let data = b"abcdefghijklmnopabcdefghZZZZabcdefghijklmnop";
4628        let mut compressed = vec![0; 256];
4629        let mut decompressed = vec![0; data.len()];
4630
4631        let csize = lz4_compress_with_dict(5, data, &mut compressed, dict);
4632        assert!(csize > 0);
4633
4634        let dsize =
4635            lz4_decompress_with_dict(&compressed[..csize as usize], &mut decompressed, dict);
4636        assert_eq!(dsize as usize, data.len());
4637        assert_eq!(decompressed, data);
4638
4639        let mut oversized = vec![0; data.len() + 1];
4640        assert_eq!(
4641            lz4_decompress_with_dict(&compressed[..csize as usize], &mut oversized, dict),
4642            0
4643        );
4644        assert_eq!(
4645            lz4_decompress_with_dict(b"bad-lz4-block", &mut decompressed, dict),
4646            0
4647        );
4648    }
4649
4650    #[test]
4651    fn lz4hc_dictionary_paths_roundtrip() {
4652        let dict = b"abcdefghijklmnop0123456789abcdefghijklmnop0123456789";
4653        let data = b"abcdefghijklmnopabcdefghZZZZabcdefghijklmnop";
4654        let mut compressed = vec![0; 256];
4655        let mut decompressed = vec![0; data.len()];
4656
4657        let csize = lz4hc_compress_with_dict(9, data, &mut compressed, dict);
4658        assert!(csize > 0);
4659
4660        let dsize =
4661            lz4_decompress_with_dict(&compressed[..csize as usize], &mut decompressed, dict);
4662        assert_eq!(dsize as usize, data.len());
4663        assert_eq!(decompressed, data);
4664    }
4665}