Skip to main content

rust_hdf5/format/
nbit_scaleoffset.rs

1//! Pure-Rust ports of the HDF5 N-bit (filter id 5) and Scale-offset
2//! (filter id 6) filters.
3//!
4//! Both ports are byte-exact with libhdf5's `H5Znbit.c` and
5//! `H5Zscaleoffset.c`. The bit-packing helpers mirror the C routines
6//! line-for-line so that crate-decoded chunks match libhdf5 element-exact.
7
8use crate::format::{FormatError, FormatResult};
9
10// ===========================================================================
11//  N-bit filter (H5Z_FILTER_NBIT, id 5)
12// ===========================================================================
13
14// Datatype class codes used in the nbit parameter tree.
15pub(crate) const NBIT_ATOMIC: u32 = 1;
16const NBIT_ARRAY: u32 = 2;
17const NBIT_COMPOUND: u32 = 3;
18const NBIT_NOOPTYPE: u32 = 4;
19pub(crate) const NBIT_ORDER_LE: u32 = 0;
20pub(crate) const NBIT_ORDER_BE: u32 = 1;
21
22/// Parameters describing one atomic element for the nbit packer.
23#[derive(Clone, Copy)]
24struct NbitAtomic {
25    size: u32,
26    order: u32,
27    precision: u32,
28    offset: u32,
29}
30
31/// A packed bit stream being written, most significant bit first within
32/// each byte, the way `H5Z__nbit_compress_one_byte` and
33/// `H5Z__scaleoffset_compress_one_byte` fill it: `j` is the next byte to
34/// complete, `acc` holds the `nacc` bits (fewer than 8) that do not yet
35/// fill one. The buffer is zero-filled, so a completed byte is stored, not
36/// or-ed in.
37struct BitWriter<'a> {
38    buf: &'a mut [u8],
39    j: usize,
40    acc: u64,
41    nacc: u32,
42}
43
44impl<'a> BitWriter<'a> {
45    fn new(buf: &'a mut [u8]) -> Self {
46        Self {
47            buf,
48            j: 0,
49            acc: 0,
50            nacc: 0,
51        }
52    }
53
54    /// Append the low `n` bits of `v`, most significant first, `n <= 64`.
55    #[inline]
56    fn put(&mut self, v: u64, n: u32) {
57        if n > 32 {
58            self.put_half(v >> 32, n - 32);
59            self.put_half(v, 32);
60        } else {
61            self.put_half(v, n);
62        }
63    }
64
65    /// `put` for `n <= 32`, so `acc` never holds more than 39 bits.
66    #[inline]
67    fn put_half(&mut self, v: u64, n: u32) {
68        self.acc = (self.acc << n) | (v & mask_u64(n as usize));
69        self.nacc += n;
70        while self.nacc >= 8 {
71            self.nacc -= 8;
72            self.buf[self.j] = (self.acc >> self.nacc) as u8;
73            self.j += 1;
74        }
75    }
76
77    /// Store the hanging bits and return the C cursor's byte index: the
78    /// partial byte, or one past the last byte when the final bit filled it.
79    fn finish(self) -> usize {
80        if self.nacc > 0 {
81            self.buf[self.j] = (self.acc << (8 - self.nacc)) as u8;
82        }
83        self.j
84    }
85}
86
87/// A packed bit stream being read, the counterpart of [`BitWriter`]:
88/// `acc` holds the `nacc` bits already taken from the buffer and not yet
89/// consumed. A byte is loaded only once a value needs it, so the stream
90/// runs short exactly where the C's byte cursor did, with `short` as the
91/// message.
92struct BitReader<'a> {
93    buf: &'a [u8],
94    j: usize,
95    acc: u64,
96    nacc: u32,
97    short: &'static str,
98}
99
100impl<'a> BitReader<'a> {
101    fn new(buf: &'a [u8], short: &'static str) -> Self {
102        Self {
103            buf,
104            j: 0,
105            acc: 0,
106            nacc: 0,
107            short,
108        }
109    }
110
111    /// Take the next `n` bits, most significant first, `n <= 64`.
112    #[inline]
113    fn get(&mut self, n: u32) -> FormatResult<u64> {
114        if n > 32 {
115            let hi = self.get_half(n - 32)?;
116            let lo = self.get_half(32)?;
117            Ok((hi << 32) | lo)
118        } else {
119            self.get_half(n)
120        }
121    }
122
123    /// `get` for `n <= 32`, so `acc` never holds more than 39 bits.
124    #[inline]
125    fn get_half(&mut self, n: u32) -> FormatResult<u64> {
126        while self.nacc < n {
127            let Some(&b) = self.buf.get(self.j) else {
128                return Err(FormatError::InvalidData(self.short.into()));
129            };
130            self.acc = (self.acc << 8) | u64::from(b);
131            self.j += 1;
132            self.nacc += 8;
133        }
134        self.nacc -= n;
135        Ok((self.acc >> self.nacc) & mask_u64(n as usize))
136    }
137}
138
139const NBIT_SHORT: &str = "nbit: buffer too short";
140
141/// The bytes of one atomic element that carry packed bits, in stream
142/// order: `(index, bits, shift)` — the byte, how many of its bits the
143/// stream holds, and where in the byte they sit. This is the byte walk of
144/// `H5Z__nbit_compress_one_atomic`: from the byte holding the field's most
145/// significant bit to the one holding its least, each contributing the
146/// field bits it covers, so the stream carries the `precision`-bit field
147/// most significant bit first.
148fn nbit_bytes(p: &NbitAtomic) -> impl Iterator<Item = (usize, u32, u32)> {
149    let len = p.size * 8;
150    let top = p.precision + p.offset;
151    let (begin, end, step): (i64, i64, i64) = if p.order == NBIT_ORDER_LE {
152        let begin = if top.is_multiple_of(8) {
153            top / 8 - 1
154        } else {
155            top / 8
156        };
157        (i64::from(begin), i64::from(p.offset / 8), -1)
158    } else {
159        let end = if p.offset.is_multiple_of(8) {
160            (len - p.offset) / 8 - 1
161        } else {
162            (len - p.offset) / 8
163        };
164        (i64::from((len - top) / 8), i64::from(end), 1)
165    };
166    let p = *p;
167    std::iter::successors(Some(begin), move |&k| (k != end).then(|| k + step)).map(move |k| {
168        let (bits, shift) = if begin == end {
169            (p.precision, p.offset % 8)
170        } else if k == begin {
171            (8 - (len - top) % 8, 0)
172        } else if k == end {
173            let bits = 8 - p.offset % 8;
174            (bits, 8 - bits)
175        } else {
176            (8, 0)
177        };
178        (k as usize, bits, shift)
179    })
180}
181
182/// An `N`-byte element as one integer, `N <= 8`. The width is a constant
183/// so the copy compiles to a load rather than a `memcpy` call per element.
184#[inline]
185fn load_uint<const N: usize>(bytes: [u8; N], le: bool) -> u64 {
186    let mut padded = [0u8; 8];
187    if le {
188        padded[..N].copy_from_slice(&bytes);
189        u64::from_le_bytes(padded)
190    } else {
191        padded[8 - N..].copy_from_slice(&bytes);
192        u64::from_be_bytes(padded)
193    }
194}
195
196/// The `N`-byte element holding `v`, `N <= 8`.
197#[inline]
198fn store_uint<const N: usize>(v: u64, le: bool) -> [u8; N] {
199    let mut out = [0u8; N];
200    if le {
201        out.copy_from_slice(&v.to_le_bytes()[..N]);
202    } else {
203        out.copy_from_slice(&v.to_be_bytes()[8 - N..]);
204    }
205    out
206}
207
208/// Call `$f::<N>($args)` with `N` the element width, one of 1, 2, 4 and
209/// 8 — the widths `H5Z__scaleoffset_get_type` admits and the ones an nbit
210/// atomic takes the single-load path for.
211macro_rules! by_width {
212    ($size:expr, $f:ident($($arg:expr),* $(,)?)) => {
213        match $size {
214            1 => $f::<1>($($arg),*),
215            2 => $f::<2>($($arg),*),
216            4 => $f::<4>($($arg),*),
217            8 => $f::<8>($($arg),*),
218            n => unreachable!("element width {n} is not 1, 2, 4 or 8"),
219        }
220    };
221}
222
223/// The packed field of one element, `N` bytes wide.
224#[inline]
225fn nbit_field<const N: usize>(elem: &[u8], le: bool, offset: u32) -> u64 {
226    load_uint::<N>(elem.try_into().expect("elem is N bytes"), le) >> offset
227}
228
229/// Store a field read back from the stream into its `N`-byte element.
230#[inline]
231fn nbit_place<const N: usize>(elem: &mut [u8], le: bool, offset: u32, field: u64) {
232    elem.copy_from_slice(&store_uint::<N>(field << offset, le));
233}
234
235/// Pack a whole buffer of `N`-byte atomic elements: the top-level atomic
236/// case, run as one loop with the width fixed instead of a dispatch per
237/// element.
238fn nbit_compress_atomics<const N: usize>(data: &[u8], w: &mut BitWriter, p: &NbitAtomic) {
239    let le = p.order == NBIT_ORDER_LE;
240    let (elems, _) = data.as_chunks::<N>();
241    for &e in elems {
242        w.put(load_uint(e, le) >> p.offset, p.precision);
243    }
244}
245
246/// Unpack a whole buffer of `N`-byte atomic elements.
247fn nbit_decompress_atomics<const N: usize>(
248    out: &mut [u8],
249    r: &mut BitReader,
250    p: &NbitAtomic,
251) -> FormatResult<()> {
252    let le = p.order == NBIT_ORDER_LE;
253    let (elems, _) = out.as_chunks_mut::<N>();
254    for e in elems {
255        *e = store_uint(r.get(p.precision)? << p.offset, le);
256    }
257    Ok(())
258}
259
260/// Decompress one nooptype element, mirroring `H5Z__nbit_decompress_one_nooptype`.
261fn nbit_decompress_one_nooptype(
262    data: &mut [u8],
263    data_offset: usize,
264    r: &mut BitReader,
265    size: u32,
266) -> FormatResult<()> {
267    for b in &mut data[data_offset..data_offset + size as usize] {
268        *b = r.get(8)? as u8;
269    }
270    Ok(())
271}
272
273/// Compress one nooptype element, mirroring `H5Z__nbit_compress_one_nooptype`.
274fn nbit_compress_one_nooptype(data: &[u8], data_offset: usize, w: &mut BitWriter, size: u32) {
275    for &b in &data[data_offset..data_offset + size as usize] {
276        w.put(u64::from(b), 8);
277    }
278}
279
280/// Decompress one atomic element, mirroring `H5Z__nbit_decompress_one_atomic`.
281///
282/// The bytes outside the field stay zero, as the C leaves them in its
283/// zero-filled output.
284fn nbit_decompress_one_atomic(
285    data: &mut [u8],
286    data_offset: usize,
287    r: &mut BitReader,
288    p: &NbitAtomic,
289) -> FormatResult<()> {
290    let elem = &mut data[data_offset..data_offset + p.size as usize];
291    if matches!(p.size, 1 | 2 | 4 | 8) {
292        let field = r.get(p.precision)?;
293        let le = p.order == NBIT_ORDER_LE;
294        by_width!(p.size, nbit_place(elem, le, p.offset, field));
295        return Ok(());
296    }
297    for (k, bits, shift) in nbit_bytes(p) {
298        elem[k] = (r.get(bits)? << shift) as u8;
299    }
300    Ok(())
301}
302
303/// Compress one atomic element, mirroring `H5Z__nbit_compress_one_atomic`.
304fn nbit_compress_one_atomic(data: &[u8], data_offset: usize, w: &mut BitWriter, p: &NbitAtomic) {
305    let elem = &data[data_offset..data_offset + p.size as usize];
306    if matches!(p.size, 1 | 2 | 4 | 8) {
307        let le = p.order == NBIT_ORDER_LE;
308        w.put(
309            by_width!(p.size, nbit_field(elem, le, p.offset)),
310            p.precision,
311        );
312        return;
313    }
314    for (k, bits, shift) in nbit_bytes(p) {
315        w.put(u64::from(elem[k] >> shift), bits);
316    }
317}
318
319/// Read an atomic parameter group starting at `parms[idx]` (after the class
320/// code has already been consumed): `size, order, precision, offset`.
321fn read_atomic(parms: &[u32], idx: &mut usize) -> FormatResult<NbitAtomic> {
322    if *idx + 4 > parms.len() {
323        return Err(FormatError::InvalidData(
324            "nbit: parameter list truncated".into(),
325        ));
326    }
327    let p = NbitAtomic {
328        size: parms[*idx],
329        order: parms[*idx + 1],
330        precision: parms[*idx + 2],
331        offset: parms[*idx + 3],
332    };
333    *idx += 4;
334    // Validate every atomic (top-level, array member, compound member) so
335    // the bit math below cannot overflow or panic on a crafted file.
336    let bits = p.size.checked_mul(8);
337    let span = p.precision.checked_add(p.offset);
338    match (bits, span) {
339        (Some(bits), Some(span))
340            if p.size > 0 && p.precision > 0 && p.precision <= bits && span <= bits => {}
341        _ => {
342            return Err(FormatError::InvalidData(format!(
343                "nbit: invalid atomic datatype (size={}, precision={}, offset={})",
344                p.size, p.precision, p.offset
345            )));
346        }
347    }
348    Ok(p)
349}
350
351/// Decompress one array element, mirroring `H5Z__nbit_decompress_one_array`.
352fn nbit_decompress_one_array(
353    data: &mut [u8],
354    data_offset: usize,
355    r: &mut BitReader,
356    parms: &[u32],
357    parms_index: &mut usize,
358) -> FormatResult<()> {
359    if *parms_index + 2 > parms.len() {
360        return Err(FormatError::InvalidData(
361            "nbit: parameter list truncated".into(),
362        ));
363    }
364    let total_size = parms[*parms_index];
365    let base_class = parms[*parms_index + 1];
366    *parms_index += 2;
367
368    match base_class {
369        NBIT_ATOMIC => {
370            let p = read_atomic(parms, parms_index)?;
371            let n = total_size / p.size;
372            for i in 0..n as usize {
373                nbit_decompress_one_atomic(data, data_offset + i * p.size as usize, r, &p)?;
374            }
375        }
376        NBIT_ARRAY => {
377            let base_size = parms[*parms_index];
378            let n = total_size / base_size;
379            let begin_index = *parms_index;
380            for i in 0..n as usize {
381                *parms_index = begin_index;
382                nbit_decompress_one_array(
383                    data,
384                    data_offset + i * base_size as usize,
385                    r,
386                    parms,
387                    parms_index,
388                )?;
389            }
390        }
391        NBIT_COMPOUND => {
392            let base_size = parms[*parms_index];
393            let n = total_size / base_size;
394            let begin_index = *parms_index;
395            for i in 0..n as usize {
396                *parms_index = begin_index;
397                nbit_decompress_one_compound(
398                    data,
399                    data_offset + i * base_size as usize,
400                    r,
401                    parms,
402                    parms_index,
403                )?;
404            }
405        }
406        NBIT_NOOPTYPE => {
407            *parms_index += 1; // skip size of no-op type
408            nbit_decompress_one_nooptype(data, data_offset, r, total_size)?;
409        }
410        _ => {
411            return Err(FormatError::InvalidData(format!(
412                "nbit: bad base class {}",
413                base_class
414            )))
415        }
416    }
417    Ok(())
418}
419
420/// Decompress one compound element, mirroring `H5Z__nbit_decompress_one_compound`.
421fn nbit_decompress_one_compound(
422    data: &mut [u8],
423    data_offset: usize,
424    r: &mut BitReader,
425    parms: &[u32],
426    parms_index: &mut usize,
427) -> FormatResult<()> {
428    if *parms_index + 2 > parms.len() {
429        return Err(FormatError::InvalidData(
430            "nbit: parameter list truncated".into(),
431        ));
432    }
433    *parms_index += 1; // skip compound size
434    let nmembers = parms[*parms_index];
435    *parms_index += 1;
436
437    for _ in 0..nmembers {
438        if *parms_index + 2 > parms.len() {
439            return Err(FormatError::InvalidData(
440                "nbit: parameter list truncated".into(),
441            ));
442        }
443        let member_offset = parms[*parms_index] as usize;
444        let member_class = parms[*parms_index + 1];
445        *parms_index += 2;
446
447        match member_class {
448            NBIT_ATOMIC => {
449                let p = read_atomic(parms, parms_index)?;
450                nbit_decompress_one_atomic(data, data_offset + member_offset, r, &p)?;
451            }
452            NBIT_ARRAY => {
453                nbit_decompress_one_array(
454                    data,
455                    data_offset + member_offset,
456                    r,
457                    parms,
458                    parms_index,
459                )?;
460            }
461            NBIT_COMPOUND => {
462                nbit_decompress_one_compound(
463                    data,
464                    data_offset + member_offset,
465                    r,
466                    parms,
467                    parms_index,
468                )?;
469            }
470            NBIT_NOOPTYPE => {
471                let size = parms[*parms_index];
472                *parms_index += 1;
473                nbit_decompress_one_nooptype(data, data_offset + member_offset, r, size)?;
474            }
475            _ => {
476                return Err(FormatError::InvalidData(format!(
477                    "nbit: bad member class {}",
478                    member_class
479                )))
480            }
481        }
482    }
483    Ok(())
484}
485
486/// Compress one array element, mirroring `H5Z__nbit_compress_one_array`.
487fn nbit_compress_one_array(
488    data: &[u8],
489    data_offset: usize,
490    w: &mut BitWriter,
491    parms: &[u32],
492    parms_index: &mut usize,
493) -> FormatResult<()> {
494    if *parms_index + 2 > parms.len() {
495        return Err(FormatError::InvalidData(
496            "nbit: parameter list truncated".into(),
497        ));
498    }
499    let total_size = parms[*parms_index];
500    let base_class = parms[*parms_index + 1];
501    *parms_index += 2;
502
503    match base_class {
504        NBIT_ATOMIC => {
505            let p = read_atomic(parms, parms_index)?;
506            let n = total_size / p.size;
507            for i in 0..n as usize {
508                nbit_compress_one_atomic(data, data_offset + i * p.size as usize, w, &p);
509            }
510        }
511        NBIT_ARRAY => {
512            let base_size = parms[*parms_index];
513            let n = total_size / base_size;
514            let begin_index = *parms_index;
515            for i in 0..n as usize {
516                *parms_index = begin_index;
517                nbit_compress_one_array(
518                    data,
519                    data_offset + i * base_size as usize,
520                    w,
521                    parms,
522                    parms_index,
523                )?;
524            }
525        }
526        NBIT_COMPOUND => {
527            let base_size = parms[*parms_index];
528            let n = total_size / base_size;
529            let begin_index = *parms_index;
530            for i in 0..n as usize {
531                *parms_index = begin_index;
532                nbit_compress_one_compound(
533                    data,
534                    data_offset + i * base_size as usize,
535                    w,
536                    parms,
537                    parms_index,
538                )?;
539            }
540        }
541        NBIT_NOOPTYPE => {
542            *parms_index += 1;
543            nbit_compress_one_nooptype(data, data_offset, w, total_size);
544        }
545        _ => {
546            return Err(FormatError::InvalidData(format!(
547                "nbit: bad base class {}",
548                base_class
549            )))
550        }
551    }
552    Ok(())
553}
554
555/// Compress one compound element, mirroring `H5Z__nbit_compress_one_compound`.
556fn nbit_compress_one_compound(
557    data: &[u8],
558    data_offset: usize,
559    w: &mut BitWriter,
560    parms: &[u32],
561    parms_index: &mut usize,
562) -> FormatResult<()> {
563    if *parms_index + 2 > parms.len() {
564        return Err(FormatError::InvalidData(
565            "nbit: parameter list truncated".into(),
566        ));
567    }
568    *parms_index += 1;
569    let nmembers = parms[*parms_index];
570    *parms_index += 1;
571
572    for _ in 0..nmembers {
573        if *parms_index + 2 > parms.len() {
574            return Err(FormatError::InvalidData(
575                "nbit: parameter list truncated".into(),
576            ));
577        }
578        let member_offset = parms[*parms_index] as usize;
579        let member_class = parms[*parms_index + 1];
580        *parms_index += 2;
581
582        match member_class {
583            NBIT_ATOMIC => {
584                let p = read_atomic(parms, parms_index)?;
585                nbit_compress_one_atomic(data, data_offset + member_offset, w, &p);
586            }
587            NBIT_ARRAY => {
588                nbit_compress_one_array(data, data_offset + member_offset, w, parms, parms_index)?;
589            }
590            NBIT_COMPOUND => {
591                nbit_compress_one_compound(
592                    data,
593                    data_offset + member_offset,
594                    w,
595                    parms,
596                    parms_index,
597                )?;
598            }
599            NBIT_NOOPTYPE => {
600                let size = parms[*parms_index];
601                *parms_index += 1;
602                nbit_compress_one_nooptype(data, data_offset + member_offset, w, size);
603            }
604            _ => {
605                return Err(FormatError::InvalidData(format!(
606                    "nbit: bad member class {}",
607                    member_class
608                )))
609            }
610        }
611    }
612    Ok(())
613}
614
615/// Apply the HDF5 N-bit filter.
616///
617/// `cd_values` follows `H5Znbit.c`'s schema:
618/// `[0]` = number of parameters, `[1]` = need-not-compress flag,
619/// `[2]` = element count, `[3..]` = the datatype parameter tree.
620///
621/// On compress, `data` is the raw element buffer; on decompress, `data`
622/// is the packed buffer and the result is the unpacked element buffer.
623pub fn apply_nbit(data: &[u8], cd_values: &[u32], compress: bool) -> FormatResult<Vec<u8>> {
624    if cd_values.len() < 4 {
625        return Err(FormatError::InvalidData("nbit: cd_values too short".into()));
626    }
627    // cd_values[1] != 0 -> data is full-precision, filter is a pass-through.
628    if cd_values[1] != 0 {
629        return Ok(data.to_vec());
630    }
631
632    let d_nelmts = cd_values[2] as usize;
633    let dtype_size = cd_values[4] as usize;
634    if dtype_size == 0 {
635        return Err(FormatError::InvalidData("nbit: zero datatype size".into()));
636    }
637    let unpacked_size = d_nelmts * dtype_size;
638
639    if compress {
640        if data.len() != unpacked_size {
641            return Err(FormatError::InvalidData(format!(
642                "nbit: input size {} != expected {}",
643                data.len(),
644                unpacked_size
645            )));
646        }
647        // Worst case the packed buffer is the same size as the unpacked one.
648        let mut buffer = vec![0u8; unpacked_size + 1];
649        let mut w = BitWriter::new(&mut buffer);
650        match cd_values[3] {
651            NBIT_ATOMIC => {
652                let mut idx = 4;
653                let p = read_atomic(cd_values, &mut idx)?;
654                if matches!(p.size, 1 | 2 | 4 | 8) {
655                    by_width!(p.size, nbit_compress_atomics(data, &mut w, &p));
656                } else {
657                    for i in 0..d_nelmts {
658                        nbit_compress_one_atomic(data, i * p.size as usize, &mut w, &p);
659                    }
660                }
661            }
662            NBIT_ARRAY => {
663                let size = cd_values[4] as usize;
664                for i in 0..d_nelmts {
665                    let mut idx = 4;
666                    nbit_compress_one_array(data, i * size, &mut w, cd_values, &mut idx)?;
667                }
668            }
669            NBIT_COMPOUND => {
670                let size = cd_values[4] as usize;
671                for i in 0..d_nelmts {
672                    let mut idx = 4;
673                    nbit_compress_one_compound(data, i * size, &mut w, cd_values, &mut idx)?;
674                }
675            }
676            other => {
677                return Err(FormatError::InvalidData(format!(
678                    "nbit: unsupported top class {}",
679                    other
680                )))
681            }
682        }
683        // libhdf5 reports new_size + 1 (any hanging bits round up).
684        let j = w.finish();
685        buffer.truncate(j + 1);
686        Ok(buffer)
687    } else {
688        let mut out = vec![0u8; unpacked_size];
689        let mut r = BitReader::new(data, NBIT_SHORT);
690        match cd_values[3] {
691            NBIT_ATOMIC => {
692                let mut idx = 4;
693                let p = read_atomic(cd_values, &mut idx)?;
694                if p.precision > p.size * 8 || p.precision + p.offset > p.size * 8 {
695                    return Err(FormatError::InvalidData(
696                        "nbit: invalid precision/offset".into(),
697                    ));
698                }
699                if matches!(p.size, 1 | 2 | 4 | 8) {
700                    by_width!(p.size, nbit_decompress_atomics(&mut out, &mut r, &p))?;
701                } else {
702                    for i in 0..d_nelmts {
703                        nbit_decompress_one_atomic(&mut out, i * p.size as usize, &mut r, &p)?;
704                    }
705                }
706            }
707            NBIT_ARRAY => {
708                let size = cd_values[4] as usize;
709                for i in 0..d_nelmts {
710                    let mut idx = 4;
711                    nbit_decompress_one_array(&mut out, i * size, &mut r, cd_values, &mut idx)?;
712                }
713            }
714            NBIT_COMPOUND => {
715                let size = cd_values[4] as usize;
716                for i in 0..d_nelmts {
717                    let mut idx = 4;
718                    nbit_decompress_one_compound(&mut out, i * size, &mut r, cd_values, &mut idx)?;
719                }
720            }
721            other => {
722                return Err(FormatError::InvalidData(format!(
723                    "nbit: unsupported top class {}",
724                    other
725                )))
726            }
727        }
728        Ok(out)
729    }
730}
731
732// ===========================================================================
733//  Scale-offset filter (H5Z_FILTER_SCALEOFFSET, id 6)
734// ===========================================================================
735
736// cd_values index layout (H5Zscaleoffset.c).
737const SO_PARM_SCALETYPE: usize = 0;
738const SO_PARM_SCALEFACTOR: usize = 1;
739const SO_PARM_NELMTS: usize = 2;
740const SO_PARM_CLASS: usize = 3;
741const SO_PARM_SIZE: usize = 4;
742const SO_PARM_SIGN: usize = 5;
743const SO_PARM_ORDER: usize = 6;
744const SO_PARM_FILAVAIL: usize = 7;
745/// First cd_values index holding the (optional) packed fill value.
746const SO_PARM_FILVAL: usize = 8;
747
748pub(crate) const SO_CLS_INTEGER: u32 = 0;
749pub(crate) const SO_CLS_FLOAT: u32 = 1;
750pub(crate) const SO_ORDER_LE: u32 = 0;
751const SO_FILL_DEFINED: u32 = 1;
752// Float scale type: 0 = variable-minimum-bits (D-scale); 1 = E-scale (unsupported).
753pub(crate) const SO_FLOAT_DSCALE: u32 = 0;
754/// `H5Z_SO_INT`: the scale type an integer dataset must carry.
755pub(crate) const SO_INT: u32 = 2;
756/// `H5Z_SCALEOFFSET_SGN_NONE`: an unsigned integer.
757pub(crate) const SO_SGN_NONE: u32 = 0;
758/// `H5Z_SCALEOFFSET_SGN_2`: a two's-complement signed integer.
759pub(crate) const SO_SGN_2: u32 = 1;
760/// `H5Z_SCALEOFFSET_ORDER_BE`.
761pub(crate) const SO_ORDER_BE: u32 = 1;
762/// `H5Z_SCALEOFFSET_TOTAL_NPARMS`: the length of the stored `cd_values`.
763pub(crate) const SO_TOTAL_NPARMS: usize = 20;
764
765/// 21-byte parameter header stored in front of every scale-offset chunk.
766const SO_BUF_OFFSET: usize = 21;
767
768const SO_SHORT: &str = "scaleoffset: buffer too short";
769
770/// `H5Z__scaleoffset_log2`: the ceiling of log2, with `log2(0) == 1`.
771fn so_log2(num: u64) -> u32 {
772    let mut v = 0u32;
773    let mut lower_bound: u64 = 1;
774    let mut val = num;
775    while {
776        val >>= 1;
777        val != 0
778    } {
779        v += 1;
780        lower_bound <<= 1;
781    }
782    if num == lower_bound {
783        v
784    } else {
785        v + 1
786    }
787}
788
789/// The filter parameters `cd_values` carries, parsed once for both
790/// directions the way `H5Z__filter_scaleoffset` reads them before it splits
791/// on `H5Z_FLAG_REVERSE`.
792#[derive(Clone, Copy)]
793struct SoParams {
794    scale_factor: i32,
795    d_nelmts: usize,
796    dtype_class: u32,
797    /// Element size in bytes; 1, 2, 4 or 8 (`H5Z__scaleoffset_get_type`).
798    size: usize,
799    dtype_sign: u32,
800    order: u32,
801    fill_defined: bool,
802    /// The fill value's bit image, masked to `size` bytes.
803    filval: u64,
804}
805
806impl SoParams {
807    fn parse(cd_values: &[u32]) -> FormatResult<Self> {
808        if cd_values.len() < 8 {
809            return Err(FormatError::InvalidData(
810                "scaleoffset: cd_values too short".into(),
811            ));
812        }
813        let scale_type = cd_values[SO_PARM_SCALETYPE];
814        let dtype_class = cd_values[SO_PARM_CLASS];
815        let size = cd_values[SO_PARM_SIZE] as usize;
816        let fill_defined = cd_values[SO_PARM_FILAVAIL] == SO_FILL_DEFINED;
817
818        // `H5Z__scaleoffset_get_type` maps the size to a C integer type and
819        // has none for the other widths.
820        if !matches!(size, 1 | 2 | 4 | 8) {
821            return Err(FormatError::InvalidData(format!(
822                "scaleoffset: unsupported datatype size {}",
823                size
824            )));
825        }
826        if dtype_class == SO_CLS_FLOAT && scale_type != SO_FLOAT_DSCALE {
827            return Err(FormatError::UnsupportedFeature(
828                "scaleoffset E-scaling method is not supported".into(),
829            ));
830        }
831
832        // Reconstruct the packed fill value from cd_values[8..]. libhdf5
833        // stores it 4 bytes per cd_value, least-significant cd_value first;
834        // each cd_value holds the bytes in the dataset datatype's byte order.
835        // We read it as a raw `size`-byte little-endian-composed value
836        // (correct for the common little-endian-dataset case h5py emits on
837        // x86/ARM).
838        let filval: u64 = if fill_defined {
839            let mut v: u64 = 0;
840            let n_cd = size.div_ceil(4);
841            if cd_values.len() < SO_PARM_FILVAL + n_cd {
842                return Err(FormatError::InvalidData(
843                    "scaleoffset: cd_values missing fill value".into(),
844                ));
845            }
846            for (w, cd) in cd_values[SO_PARM_FILVAL..SO_PARM_FILVAL + n_cd]
847                .iter()
848                .enumerate()
849            {
850                v |= (*cd as u64) << (w * 32);
851            }
852            v & mask_u64(size * 8)
853        } else {
854            0
855        };
856
857        // For integer types, a negative scale factor is reset to 0 by the
858        // library, which makes it compute the minimum bit count itself.
859        let mut scale_factor = cd_values[SO_PARM_SCALEFACTOR] as i32;
860        if dtype_class == SO_CLS_INTEGER && scale_factor < 0 {
861            scale_factor = 0;
862        }
863
864        Ok(Self {
865            scale_factor,
866            d_nelmts: cd_values[SO_PARM_NELMTS] as usize,
867            dtype_class,
868            size,
869            dtype_sign: cd_values[SO_PARM_SIGN],
870            order: cd_values[SO_PARM_ORDER],
871            fill_defined,
872            filval,
873        })
874    }
875
876    /// True when the filter does nothing at all in either direction: an
877    /// integer dataset whose user-set minimum bit count already spans the
878    /// datatype (`HGOTO_DONE(nbytes)` in `H5Z__filter_scaleoffset`, reached
879    /// before the forward/reverse split, so no header is written either).
880    fn is_noop(&self) -> bool {
881        self.dtype_class == SO_CLS_INTEGER && self.scale_factor as usize == self.size * 8
882    }
883
884    /// The element's precision in bits.
885    fn dtype_len(&self) -> u32 {
886        (self.size * 8) as u32
887    }
888
889    /// The low `size * 8` bits set.
890    fn width_mask(&self) -> u64 {
891        mask_u64(self.size * 8)
892    }
893}
894
895/// `~((u64)0 << n)`: the low `n` bits set.
896fn mask_u64(n: usize) -> u64 {
897    if n >= 64 {
898        u64::MAX
899    } else {
900        !(u64::MAX << n)
901    }
902}
903
904/// Pack every `N`-byte element of `buf` as its low `minbits` bits.
905fn so_pack<const N: usize>(buf: &[u8], le: bool, minbits: u32, w: &mut BitWriter) {
906    let (elems, _) = buf.as_chunks::<N>();
907    for &elem in elems {
908        w.put(load_uint(elem, le), minbits);
909    }
910}
911
912/// Unpack `minbits` bits into every `N`-byte element of `out`.
913fn so_unpack<const N: usize>(
914    out: &mut [u8],
915    le: bool,
916    minbits: u32,
917    r: &mut BitReader,
918) -> FormatResult<()> {
919    let (elems, _) = out.as_chunks_mut::<N>();
920    for elem in elems {
921        *elem = store_uint(r.get(minbits)?, le);
922    }
923    Ok(())
924}
925
926/// Reverse the HDF5 scale-offset filter (decompress only).
927///
928/// `cd_values` follows `H5Zscaleoffset.c`'s 20-entry schema. The output is
929/// the raw element buffer in the dataset datatype's byte order.
930pub fn reverse_scaleoffset(data: &[u8], cd_values: &[u32]) -> FormatResult<Vec<u8>> {
931    let p = SoParams::parse(cd_values)?;
932    let (d_nelmts, size, order) = (p.d_nelmts, p.size, p.order);
933    let size_out = d_nelmts * size;
934
935    if p.is_noop() {
936        // A user-set minimum-bit count equal to full precision makes the
937        // filter a no-op in both directions (`HGOTO_DONE(nbytes)`, before the
938        // forward/reverse split in `H5Z__filter_scaleoffset`): the chunk is
939        // the raw element buffer, with no parameter header in front of it.
940        if data.len() < size_out {
941            return Err(FormatError::InvalidData(SO_SHORT.into()));
942        }
943        return Ok(data[..size_out].to_vec());
944    }
945
946    // Read minbits + minval from the 21-byte header (always little-endian).
947    if data.len() < SO_BUF_OFFSET {
948        return Err(FormatError::InvalidData(
949            "scaleoffset: buffer too short for header".into(),
950        ));
951    }
952    let mut minbits: u32 = 0;
953    for (i, &b) in data[..4].iter().enumerate() {
954        minbits |= (b as u32) << (i * 8);
955    }
956    if minbits as usize > size * 8 {
957        return Err(FormatError::InvalidData(
958            "scaleoffset: minbits exceeds datatype size".into(),
959        ));
960    }
961    let minval_size = std::cmp::min(8usize, data[4] as usize);
962    let mut minval: u64 = 0;
963    for i in 0..minval_size {
964        minval |= (data[5 + i] as u64) << (i * 8);
965    }
966
967    // Special case: full precision -> payload copied verbatim.
968    if minbits as usize == size * 8 {
969        if data.len() < SO_BUF_OFFSET + size_out {
970            return Err(FormatError::InvalidData(SO_SHORT.into()));
971        }
972        return Ok(data[SO_BUF_OFFSET..SO_BUF_OFFSET + size_out].to_vec());
973    }
974
975    let mut out = vec![0u8; size_out];
976
977    if minbits != 0 {
978        if data.len() < SO_BUF_OFFSET {
979            return Err(FormatError::InvalidData(SO_SHORT.into()));
980        }
981        let mut r = BitReader::new(&data[SO_BUF_OFFSET..], SO_SHORT);
982        let le = order == SO_ORDER_LE;
983        by_width!(size, so_unpack(&mut out, le, minbits, &mut r))?;
984    }
985    // minbits == 0: out stays all-zero (all elements identical, no fill value).
986
987    // Postprocess: add back minval (and apply float scaling).
988    postdecompress(&mut out, &p, minbits, minval);
989
990    Ok(out)
991}
992
993/// Apply the HDF5 scale-offset filter (compress).
994///
995/// `data` is the raw element buffer in the dataset datatype's byte order and
996/// the result is the stored chunk: a 21-byte parameter header holding the
997/// chosen minimum bit count and the chunk minimum, followed by the packed
998/// values. Both the header and the compressed length mirror
999/// `H5Z__filter_scaleoffset`'s forward branch, including its allocation of
1000/// one byte more than the packed bits need.
1001pub fn forward_scaleoffset(data: &[u8], cd_values: &[u32]) -> FormatResult<Vec<u8>> {
1002    let p = SoParams::parse(cd_values)?;
1003    let nbytes = p.d_nelmts * p.size;
1004    if data.len() != nbytes {
1005        return Err(FormatError::InvalidData(format!(
1006            "scaleoffset: chunk is {} bytes, but the filter parameters describe {} elements of \
1007             {} bytes",
1008            data.len(),
1009            p.d_nelmts,
1010            p.size
1011        )));
1012    }
1013    if p.is_noop() {
1014        return Ok(data.to_vec());
1015    }
1016    if p.dtype_class == SO_CLS_INTEGER && p.scale_factor as usize > p.size * 8 {
1017        return Err(FormatError::InvalidData(
1018            "scaleoffset: minimum number of bits exceeds the datatype".into(),
1019        ));
1020    }
1021
1022    // Preprocess: rewrite every element as its offset from the chunk minimum
1023    // (fill values become the all-ones sentinel), and settle the bit count
1024    // that offset needs.
1025    let mut buf = data.to_vec();
1026    let (minbits, minval) = if p.dtype_class == SO_CLS_INTEGER {
1027        by_width!(p.size, precompress_int(&mut buf, &p))
1028    } else {
1029        precompress_float(&mut buf, &p)?
1030    };
1031    debug_assert!(minbits <= p.dtype_len());
1032
1033    // `size_out` is what libhdf5 allocates and reports: one byte more than
1034    // the packed bits occupy, which is what a reader finds stored.
1035    let size_out = SO_BUF_OFFSET + nbytes * minbits as usize / (p.size * 8) + 1;
1036    let mut out = vec![0u8; size_out];
1037    out[..4].copy_from_slice(&minbits.to_le_bytes());
1038    // libhdf5 stores `sizeof(unsigned long long)` here and the reader takes
1039    // the smaller of that and its own, so the count of minval bytes that
1040    // follow is fixed at 8.
1041    out[4] = 8;
1042    out[5..13].copy_from_slice(&minval.to_le_bytes());
1043
1044    if minbits as usize == p.size * 8 {
1045        // Full precision: the offsets need every bit, so they are stored
1046        // unpacked, and the trailing allocated byte is not part of the chunk.
1047        out.truncate(SO_BUF_OFFSET + nbytes);
1048        out[SO_BUF_OFFSET..].copy_from_slice(&buf);
1049        return Ok(out);
1050    }
1051    if minbits != 0 {
1052        let mut w = BitWriter::new(&mut out[SO_BUF_OFFSET..]);
1053        let le = p.order == SO_ORDER_LE;
1054        by_width!(p.size, so_pack(&buf, le, minbits, &mut w));
1055        w.finish();
1056    }
1057    // minbits == 0: every element is the chunk minimum, so the payload is
1058    // the single zero byte the size formula leaves.
1059    Ok(out)
1060}
1061
1062/// Preprocess an integer chunk of `N`-byte elements, mirroring
1063/// `H5Z__scaleoffset_precompress_i`.
1064///
1065/// Returns `(minbits, minval)` and leaves `buf` holding each element's offset
1066/// from the chunk minimum — or the all-ones sentinel where the element was
1067/// the fill value.
1068fn precompress_int<const N: usize>(buf: &mut [u8], p: &SoParams) -> (u32, u64) {
1069    let signed = p.dtype_sign == SO_SGN_2;
1070    let le = p.order == SO_ORDER_LE;
1071    let width_mask = p.width_mask();
1072    // The comparison key: a signed element orders by its sign-extended
1073    // value, an unsigned one by its raw bits. `i128` holds both.
1074    let key = |raw: u64| -> i128 {
1075        if signed {
1076            i128::from(sign_extend::<N>(raw))
1077        } else {
1078            i128::from(raw)
1079        }
1080    };
1081    let (elems, _) = buf.as_chunks_mut::<N>();
1082
1083    let mut minbits = p.scale_factor as u32;
1084    let mut min: i128 = 0;
1085    let mut max: i128 = 0;
1086
1087    if p.fill_defined {
1088        // Fill elements take no part in the range.
1089        let first = elems.iter().position(|&e| load_uint(e, le) != p.filval);
1090        if let Some(f) = first {
1091            min = key(load_uint(elems[f], le));
1092            max = min;
1093            for &e in &elems[f..] {
1094                let raw = load_uint(e, le);
1095                if raw == p.filval {
1096                    continue;
1097                }
1098                let v = key(raw);
1099                max = max.max(v);
1100                min = min.min(v);
1101            }
1102        }
1103        if minbits == 0 {
1104            let span_minus_1 = (max - min) as u64;
1105            if span_minus_1 > width_mask - 2 {
1106                // No sentinel fits above the range; store at full precision.
1107                // libhdf5 leaves minval at 0 here, which the reader ignores.
1108                return (p.dtype_len(), 0);
1109            }
1110            minbits = so_log2(span_minus_1 + 2);
1111        }
1112        if minbits != p.dtype_len() {
1113            let sentinel = mask_u64(minbits as usize);
1114            for e in elems.iter_mut() {
1115                let raw = load_uint(*e, le);
1116                let v = if raw == p.filval {
1117                    sentinel
1118                } else {
1119                    (key(raw) - min) as u64 & width_mask
1120                };
1121                *e = store_uint(v, le);
1122            }
1123        }
1124    } else {
1125        if let Some(&e0) = elems.first() {
1126            min = key(load_uint(e0, le));
1127            max = min;
1128        }
1129        for &e in elems.iter() {
1130            let v = key(load_uint(e, le));
1131            max = max.max(v);
1132            min = min.min(v);
1133        }
1134        if minbits == 0 {
1135            let span_minus_1 = (max - min) as u64;
1136            if span_minus_1 > width_mask - 2 {
1137                return (p.dtype_len(), 0);
1138            }
1139            minbits = so_log2(span_minus_1 + 1);
1140        }
1141        if minbits != p.dtype_len() {
1142            for e in elems.iter_mut() {
1143                let v = (key(load_uint(*e, le)) - min) as u64 & width_mask;
1144                *e = store_uint(v, le);
1145            }
1146        }
1147    }
1148
1149    (
1150        minbits,
1151        min as i64 as u64 & if signed { u64::MAX } else { width_mask },
1152    )
1153}
1154
1155/// The float operations `H5Z_scaleoffset_precompress_3` performs, each in the
1156/// element's own precision: `powf`/`roundf`/`lroundf` for a 4-byte element,
1157/// `pow`/`round`/`lround` for an 8-byte one. Doing them all in `f64` would
1158/// pick a different `minbits` at the boundary for `float` data.
1159trait SoFloat:
1160    Copy
1161    + PartialOrd
1162    + std::ops::Mul<Output = Self>
1163    + std::ops::Sub<Output = Self>
1164    + std::ops::Div<Output = Self>
1165    + std::ops::Add<Output = Self>
1166{
1167    const ZERO: Self;
1168    fn from_stored(v: u64) -> Self;
1169    fn to_stored(self) -> u64;
1170    fn widen(self) -> f64;
1171    /// `(type)v` for a `double`.
1172    fn narrow(v: f64) -> Self;
1173    /// `(type)v` for a `long long`.
1174    fn from_int(v: i64) -> Self;
1175    /// `pow_fun((type)base, (type)exp)`.
1176    fn pow(base: f64, exp: f64) -> Self;
1177    fn abs(self) -> Self;
1178    /// `round_fun`: to nearest, halfway away from zero.
1179    fn round(self) -> Self;
1180    /// `lround_fun` / `llround_fun`.
1181    fn lround(self) -> i64;
1182}
1183
1184impl SoFloat for f32 {
1185    const ZERO: Self = 0.0;
1186    fn from_stored(v: u64) -> Self {
1187        f32::from_bits(v as u32)
1188    }
1189    fn to_stored(self) -> u64 {
1190        self.to_bits() as u64
1191    }
1192    fn widen(self) -> f64 {
1193        self as f64
1194    }
1195    fn narrow(v: f64) -> Self {
1196        v as f32
1197    }
1198    fn from_int(v: i64) -> Self {
1199        v as f32
1200    }
1201    fn pow(base: f64, exp: f64) -> Self {
1202        (base as f32).powf(exp as f32)
1203    }
1204    fn abs(self) -> Self {
1205        f32::abs(self)
1206    }
1207    fn round(self) -> Self {
1208        f32::round(self)
1209    }
1210    fn lround(self) -> i64 {
1211        f32::round(self) as i64
1212    }
1213}
1214
1215impl SoFloat for f64 {
1216    const ZERO: Self = 0.0;
1217    fn from_stored(v: u64) -> Self {
1218        f64::from_bits(v)
1219    }
1220    fn to_stored(self) -> u64 {
1221        self.to_bits()
1222    }
1223    fn widen(self) -> f64 {
1224        self
1225    }
1226    fn narrow(v: f64) -> Self {
1227        v
1228    }
1229    fn from_int(v: i64) -> Self {
1230        v as f64
1231    }
1232    fn pow(base: f64, exp: f64) -> Self {
1233        base.powf(exp)
1234    }
1235    fn abs(self) -> Self {
1236        f64::abs(self)
1237    }
1238    fn round(self) -> Self {
1239        f64::round(self)
1240    }
1241    fn lround(self) -> i64 {
1242        f64::round(self) as i64
1243    }
1244}
1245
1246/// Preprocess a floating-point chunk by the variable-minimum-bits (D-scale)
1247/// method, mirroring `H5Z__scaleoffset_precompress_fd`.
1248///
1249/// Each value becomes the integer `round(v * 10^D - min * 10^D)`, so the
1250/// scale factor is the number of decimal digits kept. Returns
1251/// `(minbits, minval)`, where `minval` is the bit image of the chunk minimum
1252/// as a float — the form [`postdecompress`] reads it back in.
1253fn precompress_float(buf: &mut [u8], p: &SoParams) -> FormatResult<(u32, u64)> {
1254    match p.size {
1255        4 => Ok(precompress_float_typed::<f32, 4>(buf, p)),
1256        8 => Ok(precompress_float_typed::<f64, 8>(buf, p)),
1257        n => Err(FormatError::InvalidData(format!(
1258            "scaleoffset: no floating-point type of {n} bytes"
1259        ))),
1260    }
1261}
1262
1263fn precompress_float_typed<T: SoFloat, const N: usize>(buf: &mut [u8], p: &SoParams) -> (u32, u64) {
1264    let d_val = p.scale_factor as f64;
1265    let pow10 = T::pow(10.0, d_val);
1266    let filval = T::from_stored(p.filval);
1267    let le = p.order == SO_ORDER_LE;
1268    let get = |e: [u8; N]| T::from_stored(load_uint(e, le));
1269    let (elems, _) = buf.as_chunks_mut::<N>();
1270    // `H5Z_scaleoffset_max_min_3` widens the difference to `double` and
1271    // compares against a `double` threshold whatever the element type is,
1272    // while `H5Z_scaleoffset_modify_1` stays in the element type. For a
1273    // 4-byte element the two thresholds are not the same number, so the scan
1274    // and the rewrite each use their own.
1275    let scan_epsilon = 10f64.powf(-d_val);
1276    let is_fill_scan = |v: T| (v - filval).widen().abs() < scan_epsilon;
1277    let modify_epsilon = T::pow(10.0, -d_val);
1278    let is_fill_modify = |v: T| (v - filval).abs() < modify_epsilon;
1279
1280    let mut min = T::ZERO;
1281    let mut max = T::ZERO;
1282    if p.fill_defined {
1283        if let Some(f) = elems.iter().position(|&e| !is_fill_scan(get(e))) {
1284            min = get(elems[f]);
1285            max = min;
1286            for &e in &elems[f..] {
1287                let v = get(e);
1288                if is_fill_scan(v) {
1289                    continue;
1290                }
1291                if v > max {
1292                    max = v;
1293                }
1294                if v < min {
1295                    min = v;
1296                }
1297            }
1298        }
1299    } else if let Some(&e0) = elems.first() {
1300        min = get(e0);
1301        max = min;
1302        for &e in elems.iter() {
1303            let v = get(e);
1304            if v > max {
1305                max = v;
1306            }
1307            if v < min {
1308                min = v;
1309            }
1310        }
1311    }
1312
1313    // `H5Z_scaleoffset_check_3`: the scaled span has to stay inside the signed
1314    // integer the values are stored as. When it does not, the library gives up
1315    // on scaling and stores at full precision, leaving `minval` at 0 (its
1316    // `goto done` skips `H5Z_scaleoffset_save_min`).
1317    let dtype_len = p.dtype_len();
1318    let scaled = max * pow10 - min * pow10;
1319    if scaled.round() > T::pow(2.0, (dtype_len - 1) as f64) {
1320        return (dtype_len, 0);
1321    }
1322    let span = scaled.lround() as u64 + 1;
1323    let minbits = if p.fill_defined {
1324        so_log2(span + 1)
1325    } else {
1326        so_log2(span)
1327    };
1328
1329    if minbits != dtype_len {
1330        let sentinel = mask_u64(minbits as usize);
1331        for e in elems.iter_mut() {
1332            let v = get(*e);
1333            let stored = if p.fill_defined && is_fill_modify(v) {
1334                sentinel
1335            } else {
1336                (v * pow10 - min * pow10).lround() as u64 & p.width_mask()
1337            };
1338            *e = store_uint(stored, le);
1339        }
1340    }
1341
1342    (minbits, min.to_stored())
1343}
1344
1345/// Sign-extend the low `N * 8` bits of `v` to a full `i64`.
1346#[inline]
1347fn sign_extend<const N: usize>(v: u64) -> i64 {
1348    let shift = 64 - 8 * N as u32;
1349    ((v << shift) as i64) >> shift
1350}
1351
1352/// Postprocess decompressed scale-offset data.
1353///
1354/// `minbits` and `minval` come from the per-chunk header rather than
1355/// `p`, so they stay explicit parameters; everything else `postdecompress`
1356/// needs is exactly what [`SoParams`] already parsed once for both filter
1357/// directions.
1358fn postdecompress(out: &mut [u8], p: &SoParams, minbits: u32, minval: u64) {
1359    // Sentinel: a fully decompressed value equal to (1 << minbits) - 1 is
1360    // restored to the fill value rather than offset-added.
1361    let sentinel = mask_u64(minbits as usize);
1362    if p.dtype_class == SO_CLS_INTEGER {
1363        by_width!(p.size, postdecompress_int(out, p, sentinel, minval));
1364    } else {
1365        // Float D-scale: value = (signed decompressed int) / 10^D + min,
1366        // where `min` reinterprets `minval`'s low bits as the float type.
1367        match p.size {
1368            4 => postdecompress_float::<f32, 4>(out, p, sentinel, minval),
1369            8 => postdecompress_float::<f64, 8>(out, p, sentinel, minval),
1370            _ => {}
1371        }
1372    }
1373}
1374
1375/// `buf[i] = (buf[i] == sentinel) ? filval : buf[i] + minval` over `N`-byte
1376/// elements.
1377fn postdecompress_int<const N: usize>(out: &mut [u8], p: &SoParams, sentinel: u64, minval: u64) {
1378    let le = p.order == SO_ORDER_LE;
1379    let width_mask = p.width_mask();
1380    let (elems, _) = out.as_chunks_mut::<N>();
1381    for e in elems {
1382        let v = load_uint(*e, le);
1383        let result = if p.fill_defined && v == sentinel {
1384            p.filval
1385        } else {
1386            v.wrapping_add(minval) & width_mask
1387        };
1388        *e = store_uint(result, le);
1389    }
1390}
1391
1392fn postdecompress_float<T: SoFloat, const N: usize>(
1393    out: &mut [u8],
1394    p: &SoParams,
1395    sentinel: u64,
1396    minval: u64,
1397) {
1398    let le = p.order == SO_ORDER_LE;
1399    let divisor = T::narrow(10f64.powf(p.scale_factor as f64));
1400    let min = T::from_stored(minval);
1401    let filval = T::from_stored(p.filval);
1402    let (elems, _) = out.as_chunks_mut::<N>();
1403    for e in elems {
1404        let raw = load_uint(*e, le);
1405        let val = if p.fill_defined && raw == sentinel {
1406            filval
1407        } else {
1408            T::from_int(sign_extend::<N>(raw)) / divisor + min
1409        };
1410        *e = store_uint(val.to_stored(), le);
1411    }
1412}
1413
1414// ===========================================================================
1415//  Post-filter datatype conversion (H5T_convert equivalent)
1416// ===========================================================================
1417
1418use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
1419
1420/// True if `dt` is a floating-point type whose bit layout is an IEEE 754
1421/// interchange format — the only float layouts that can be reinterpreted in
1422/// place. [`DatatypeMessage::ieee_format`] is the single owner of that rule.
1423fn is_standard_ieee_float(dt: &DatatypeMessage) -> bool {
1424    dt.ieee_format().is_some()
1425}
1426
1427/// True if the filter-pipeline / on-disk output for `dt` needs a post-filter
1428/// datatype conversion before the element values are usable.
1429///
1430/// For a `FixedPoint` datatype the filter pipeline output (or contiguous
1431/// on-disk bytes) carries the significant value in `bit_precision` bits
1432/// starting at `bit_offset`, with the rest zero-filled and the sign bit NOT
1433/// extended. libhdf5 fixes this up with a datatype conversion
1434/// (`H5T_convert`) after the filter pipeline; this returns true for any
1435/// such non-trivial layout.
1436///
1437/// It also returns true for a non-standard `FloatingPoint` layout, so the
1438/// caller routes it through [`apply_datatype_conversion`], which then
1439/// returns a clear error rather than silently yielding wrong data.
1440pub fn datatype_needs_bit_conversion(dt: &DatatypeMessage) -> bool {
1441    match dt {
1442        DatatypeMessage::FixedPoint {
1443            size,
1444            bit_offset,
1445            bit_precision,
1446            ..
1447        } => *bit_offset != 0 || (*bit_precision as u32) < *size * 8,
1448        DatatypeMessage::FloatingPoint { .. } => !is_standard_ieee_float(dt),
1449        _ => false,
1450    }
1451}
1452
1453/// Apply the post-filter datatype conversion in place to a fully-decoded
1454/// output buffer.
1455///
1456/// This mirrors libhdf5's `H5T_convert` step that runs AFTER the filter
1457/// pipeline. For a `FixedPoint` datatype with `bit_offset != 0` or
1458/// `bit_precision < size*8`, each `size`-byte element is rewritten so the
1459/// significant value occupies the whole element with bit offset 0:
1460///
1461///   1. interpret the element as an unsigned integer (respecting byte order),
1462///   2. shift right by `bit_offset`,
1463///   3. mask to `bit_precision` low bits,
1464///   4. sign-extend from bit `bit_precision-1` if the type is signed,
1465///   5. write the result back in the same byte order.
1466///
1467/// It is a strict no-op for ordinary full-width datatypes (and for any
1468/// non-`FixedPoint` class).
1469///
1470/// For `FloatingPoint` types with a non-standard bit layout that cannot be
1471/// faithfully reinterpreted, an error is returned rather than wrong data.
1472pub fn apply_datatype_conversion(buffer: &mut [u8], dt: &DatatypeMessage) -> FormatResult<()> {
1473    match dt {
1474        DatatypeMessage::FixedPoint {
1475            size,
1476            byte_order,
1477            signed,
1478            bit_offset,
1479            bit_precision,
1480        } => {
1481            let size = *size as usize;
1482            let precision = *bit_precision as usize;
1483            let offset = *bit_offset as usize;
1484
1485            // Full-width plain integer: nothing to do.
1486            if offset == 0 && precision == size * 8 {
1487                return Ok(());
1488            }
1489            if size == 0 || size > 8 {
1490                return Err(FormatError::InvalidData(format!(
1491                    "datatype conversion: unsupported FixedPoint size {size}"
1492                )));
1493            }
1494            if precision == 0 || offset + precision > size * 8 {
1495                return Err(FormatError::InvalidData(format!(
1496                    "datatype conversion: invalid bit layout (offset {offset}, \
1497                     precision {precision}, size {size})"
1498                )));
1499            }
1500            if !buffer.len().is_multiple_of(size) {
1501                return Err(FormatError::InvalidData(format!(
1502                    "datatype conversion: buffer length {} not a multiple of \
1503                     element size {size}",
1504                    buffer.len()
1505                )));
1506            }
1507
1508            let big_endian = matches!(byte_order, ByteOrder::BigEndian);
1509            let precision_mask: u64 = if precision == 64 {
1510                u64::MAX
1511            } else {
1512                (1u64 << precision) - 1
1513            };
1514            let sign_bit: u64 = 1u64 << (precision - 1);
1515
1516            for elem in buffer.chunks_exact_mut(size) {
1517                // Load element as a u64 in native value space.
1518                let mut raw: u64 = 0;
1519                if big_endian {
1520                    for &b in elem.iter() {
1521                        raw = (raw << 8) | b as u64;
1522                    }
1523                } else {
1524                    for (i, &b) in elem.iter().enumerate() {
1525                        raw |= (b as u64) << (8 * i);
1526                    }
1527                }
1528
1529                // Extract the significant bits.
1530                let mut value = (raw >> offset) & precision_mask;
1531
1532                // Sign-extend from bit `precision-1` when signed.
1533                if *signed && (value & sign_bit) != 0 {
1534                    value |= !precision_mask;
1535                }
1536
1537                // Store back in the same byte order, full element width.
1538                if big_endian {
1539                    for i in 0..size {
1540                        elem[size - 1 - i] = (value >> (8 * i)) as u8;
1541                    }
1542                } else {
1543                    for (i, b) in elem.iter_mut().enumerate() {
1544                        *b = (value >> (8 * i)) as u8;
1545                    }
1546                }
1547            }
1548            Ok(())
1549        }
1550        DatatypeMessage::FloatingPoint { .. } => {
1551            // Standard IEEE-754 layouts need no conversion. Anything else
1552            // cannot be faithfully reinterpreted here.
1553            if is_standard_ieee_float(dt) {
1554                Ok(())
1555            } else {
1556                Err(FormatError::InvalidData(
1557                    "datatype conversion: non-standard floating-point bit \
1558                     layout cannot be converted"
1559                        .into(),
1560                ))
1561            }
1562        }
1563        _ => Ok(()),
1564    }
1565}
1566
1567// ===========================================================================
1568//  Tests
1569// ===========================================================================
1570#[cfg(test)]
1571mod tests {
1572    use super::*;
1573
1574    /// Build an nbit cd_values list for an unsigned little-endian atomic int.
1575    fn nbit_atomic_cd(d_nelmts: u32, size: u32, precision: u32, offset: u32) -> Vec<u32> {
1576        // [0]=nparms [1]=need_not_compress [2]=d_nelmts [3]=class [4]=size
1577        // [5]=order [6]=precision [7]=offset
1578        let need_not_compress = if offset == 0 && precision == size * 8 {
1579            1
1580        } else {
1581            0
1582        };
1583        vec![
1584            8,
1585            need_not_compress,
1586            d_nelmts,
1587            NBIT_ATOMIC,
1588            size,
1589            NBIT_ORDER_LE,
1590            precision,
1591            offset,
1592        ]
1593    }
1594
1595    #[test]
1596    fn nbit_roundtrip_u16_precision12() {
1597        // 16-bit storage, 12-bit precision, offset 0.
1598        let values: Vec<u16> = (0..40u16).map(|i| (i * 71) & 0x0FFF).collect();
1599        let mut raw = Vec::new();
1600        for &v in &values {
1601            raw.extend_from_slice(&v.to_le_bytes());
1602        }
1603        let cd = nbit_atomic_cd(values.len() as u32, 2, 12, 0);
1604        let packed = apply_nbit(&raw, &cd, true).unwrap();
1605        assert!(packed.len() <= raw.len());
1606        let unpacked = apply_nbit(&packed, &cd, false).unwrap();
1607        assert_eq!(unpacked, raw);
1608    }
1609
1610    #[test]
1611    fn nbit_roundtrip_u32_precision20_offset4() {
1612        let values: Vec<u32> = (0..32u32).map(|i| ((i * 9999) & 0xFFFFF) << 4).collect();
1613        let mut raw = Vec::new();
1614        for &v in &values {
1615            raw.extend_from_slice(&v.to_le_bytes());
1616        }
1617        let cd = nbit_atomic_cd(values.len() as u32, 4, 20, 4);
1618        let packed = apply_nbit(&raw, &cd, true).unwrap();
1619        let unpacked = apply_nbit(&packed, &cd, false).unwrap();
1620        assert_eq!(unpacked, raw);
1621    }
1622
1623    #[test]
1624    fn nbit_passthrough_full_precision() {
1625        let raw: Vec<u8> = (0..64).collect();
1626        let cd = nbit_atomic_cd(16, 4, 32, 0); // full precision -> need_not_compress
1627        let packed = apply_nbit(&raw, &cd, true).unwrap();
1628        assert_eq!(packed, raw);
1629        let unpacked = apply_nbit(&packed, &cd, false).unwrap();
1630        assert_eq!(unpacked, raw);
1631    }
1632
1633    #[test]
1634    fn nbit_roundtrip_big_endian() {
1635        let values: Vec<u16> = (0..24u16).map(|i| (i * 53) & 0x03FF).collect();
1636        let mut raw = Vec::new();
1637        for &v in &values {
1638            raw.extend_from_slice(&v.to_be_bytes());
1639        }
1640        let mut cd = nbit_atomic_cd(values.len() as u32, 2, 10, 0);
1641        cd[5] = NBIT_ORDER_BE;
1642        let packed = apply_nbit(&raw, &cd, true).unwrap();
1643        let unpacked = apply_nbit(&packed, &cd, false).unwrap();
1644        assert_eq!(unpacked, raw);
1645    }
1646
1647    // ---------------------------------------------------------------
1648    //  Post-filter datatype conversion
1649    // ---------------------------------------------------------------
1650
1651    fn fixed(size: u32, signed: bool, offset: u16, precision: u16) -> DatatypeMessage {
1652        DatatypeMessage::FixedPoint {
1653            size,
1654            byte_order: ByteOrder::LittleEndian,
1655            signed,
1656            bit_offset: offset,
1657            bit_precision: precision,
1658        }
1659    }
1660
1661    #[test]
1662    fn conversion_noop_for_full_width_types() {
1663        // 32-bit unsigned, offset 0, precision 32 -> plain integer, no-op.
1664        let dt = fixed(4, false, 0, 32);
1665        assert!(!datatype_needs_bit_conversion(&dt));
1666        let mut buf = vec![0x78, 0x56, 0x34, 0x12, 0xFF, 0xFF, 0xFF, 0xFF];
1667        let before = buf.clone();
1668        apply_datatype_conversion(&mut buf, &dt).unwrap();
1669        assert_eq!(buf, before);
1670    }
1671
1672    #[test]
1673    fn conversion_noop_for_non_numeric_types() {
1674        let dt = DatatypeMessage::fixed_string(8);
1675        assert!(!datatype_needs_bit_conversion(&dt));
1676        let mut buf = b"hello!!\0".to_vec();
1677        let before = buf.clone();
1678        apply_datatype_conversion(&mut buf, &dt).unwrap();
1679        assert_eq!(buf, before);
1680    }
1681
1682    #[test]
1683    fn conversion_unsigned_offset_shifts_right() {
1684        // u16, bit_offset 3, precision 10. The value lives in bits [3,13).
1685        // Raw element layout (LE u16): value 0x2A5 placed at offset 3 ->
1686        // 0x2A5 << 3 = 0x1528.
1687        let dt = fixed(2, false, 3, 10);
1688        assert!(datatype_needs_bit_conversion(&dt));
1689        let mut buf = (0x1528u16).to_le_bytes().to_vec();
1690        apply_datatype_conversion(&mut buf, &dt).unwrap();
1691        assert_eq!(u16::from_le_bytes([buf[0], buf[1]]), 0x2A5);
1692    }
1693
1694    #[test]
1695    fn conversion_signed_negative_sign_extends() {
1696        // i16, bit_offset 4, precision 8. Store -3 (8-bit two's complement
1697        // = 0xFD) at offset 4: 0xFD << 4 = 0xFD0.
1698        let dt = fixed(2, true, 4, 8);
1699        let mut buf = (0x0FD0u16).to_le_bytes().to_vec();
1700        apply_datatype_conversion(&mut buf, &dt).unwrap();
1701        assert_eq!(i16::from_le_bytes([buf[0], buf[1]]), -3);
1702    }
1703
1704    #[test]
1705    fn conversion_signed_positive_stays_positive() {
1706        // i16, bit_offset 4, precision 8. Store +5 at offset 4 -> 0x050.
1707        let dt = fixed(2, true, 4, 8);
1708        let mut buf = (0x0050u16).to_le_bytes().to_vec();
1709        apply_datatype_conversion(&mut buf, &dt).unwrap();
1710        assert_eq!(i16::from_le_bytes([buf[0], buf[1]]), 5);
1711    }
1712
1713    #[test]
1714    fn conversion_reduced_precision_offset_zero() {
1715        // i32, bit_offset 0, precision 20 -> still non-trivial (precision <
1716        // size*8). Store -1 in 20 bits = 0xFFFFF.
1717        let dt = fixed(4, true, 0, 20);
1718        assert!(datatype_needs_bit_conversion(&dt));
1719        let mut buf = (0x000FFFFFu32).to_le_bytes().to_vec();
1720        apply_datatype_conversion(&mut buf, &dt).unwrap();
1721        assert_eq!(i32::from_le_bytes(buf.clone().try_into().unwrap()), -1);
1722    }
1723
1724    #[test]
1725    fn conversion_big_endian_signed() {
1726        // i16, BE, bit_offset 4, precision 8, value -3.
1727        let dt = DatatypeMessage::FixedPoint {
1728            size: 2,
1729            byte_order: ByteOrder::BigEndian,
1730            signed: true,
1731            bit_offset: 4,
1732            bit_precision: 8,
1733        };
1734        let mut buf = (0x0FD0u16).to_be_bytes().to_vec();
1735        apply_datatype_conversion(&mut buf, &dt).unwrap();
1736        assert_eq!(i16::from_be_bytes([buf[0], buf[1]]), -3);
1737    }
1738
1739    #[test]
1740    fn conversion_multiple_elements() {
1741        // u32, bit_offset 5, precision 16. Three elements.
1742        let dt = fixed(4, false, 5, 16);
1743        let vals: [u32; 3] = [0x1234, 0xABCD, 0x0001];
1744        let mut buf = Vec::new();
1745        for v in vals {
1746            buf.extend_from_slice(&(v << 5).to_le_bytes());
1747        }
1748        apply_datatype_conversion(&mut buf, &dt).unwrap();
1749        for (i, v) in vals.iter().enumerate() {
1750            let e = u32::from_le_bytes(buf[i * 4..i * 4 + 4].try_into().unwrap());
1751            assert_eq!(e, *v);
1752        }
1753    }
1754
1755    #[test]
1756    fn conversion_rejects_non_standard_float() {
1757        // A float with a non-IEEE bit layout must error, not corrupt data.
1758        let dt = DatatypeMessage::FloatingPoint {
1759            size: 4,
1760            byte_order: ByteOrder::LittleEndian,
1761            sign_location: 30,
1762            bit_offset: 1,
1763            bit_precision: 31,
1764            exponent_location: 22,
1765            exponent_size: 8,
1766            mantissa_location: 0,
1767            mantissa_size: 22,
1768            exponent_bias: 127,
1769        };
1770        assert!(datatype_needs_bit_conversion(&dt));
1771        let mut buf = vec![0u8; 4];
1772        assert!(apply_datatype_conversion(&mut buf, &dt).is_err());
1773    }
1774
1775    #[test]
1776    fn conversion_standard_float_is_noop() {
1777        let dt = DatatypeMessage::f64_type();
1778        assert!(!datatype_needs_bit_conversion(&dt));
1779        let mut buf = 12.5f64.to_le_bytes().to_vec();
1780        let before = buf.clone();
1781        apply_datatype_conversion(&mut buf, &dt).unwrap();
1782        assert_eq!(buf, before);
1783    }
1784
1785    #[test]
1786    fn conversion_rejects_bad_buffer_length() {
1787        let dt = fixed(4, false, 3, 16);
1788        let mut buf = vec![0u8; 5]; // not a multiple of 4
1789        assert!(apply_datatype_conversion(&mut buf, &dt).is_err());
1790    }
1791}