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