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