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/// Decompress one atomic byte, mirroring `H5Z__nbit_decompress_one_byte`.
55#[allow(clippy::too_many_arguments)]
56fn nbit_decompress_one_byte(
57    data: &mut [u8],
58    data_offset: usize,
59    k: u32,
60    begin_i: u32,
61    end_i: u32,
62    buffer: &[u8],
63    cur: &mut NbitCursor,
64    p: &NbitAtomic,
65    datatype_len: u32,
66) -> FormatResult<()> {
67    if cur.j >= buffer.len() {
68        return Err(FormatError::InvalidData("nbit: buffer too short".into()));
69    }
70    let mut val = buffer[cur.j];
71    let mut dat_offset: usize = 0;
72    let mut dat_len: usize;
73
74    if begin_i != end_i {
75        if k == begin_i {
76            dat_len = 8 - ((datatype_len - p.precision - p.offset) % 8) as usize;
77        } else if k == end_i {
78            dat_len = 8 - (p.offset % 8) as usize;
79            dat_offset = 8 - dat_len;
80        } else {
81            dat_len = 8;
82        }
83    } else {
84        dat_offset = (p.offset % 8) as usize;
85        dat_len = p.precision as usize;
86    }
87
88    let idx = data_offset + k as usize;
89    if cur.buf_len > dat_len {
90        data[idx] =
91            (((val >> (cur.buf_len - dat_len)) as u32 & mask_u32(dat_len)) << dat_offset) as u8;
92        cur.buf_len -= dat_len;
93    } else {
94        data[idx] =
95            (((val as u32 & mask_u32(cur.buf_len)) << (dat_len - cur.buf_len)) << dat_offset) as u8;
96        dat_len -= cur.buf_len;
97        cur.next_byte();
98        if dat_len == 0 {
99            return Ok(());
100        }
101        if cur.j >= buffer.len() {
102            return Err(FormatError::InvalidData("nbit: buffer too short".into()));
103        }
104        val = buffer[cur.j];
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    }
109    Ok(())
110}
111
112/// Compress one atomic byte, mirroring `H5Z__nbit_compress_one_byte`.
113#[allow(clippy::too_many_arguments)]
114fn nbit_compress_one_byte(
115    data: &[u8],
116    data_offset: usize,
117    k: u32,
118    begin_i: u32,
119    end_i: u32,
120    buffer: &mut [u8],
121    cur: &mut NbitCursor,
122    p: &NbitAtomic,
123    datatype_len: u32,
124) {
125    let mut val = data[data_offset + k as usize];
126    let mut dat_len: usize;
127
128    if begin_i != end_i {
129        if k == begin_i {
130            dat_len = 8 - ((datatype_len - p.precision - p.offset) % 8) as usize;
131        } else if k == end_i {
132            dat_len = 8 - (p.offset % 8) as usize;
133            val >>= 8 - dat_len;
134        } else {
135            dat_len = 8;
136        }
137    } else {
138        val >>= p.offset % 8;
139        dat_len = p.precision as usize;
140    }
141
142    if cur.buf_len > dat_len {
143        buffer[cur.j] |= ((val as u32 & mask_u32(dat_len)) << (cur.buf_len - dat_len)) as u8;
144        cur.buf_len -= dat_len;
145    } else {
146        buffer[cur.j] |= ((val as u32 >> (dat_len - cur.buf_len)) & mask_u32(cur.buf_len)) as u8;
147        dat_len -= cur.buf_len;
148        cur.next_byte();
149        if dat_len == 0 {
150            return;
151        }
152        buffer[cur.j] = ((val as u32 & mask_u32(dat_len)) << (cur.buf_len - dat_len)) as u8;
153        cur.buf_len -= dat_len;
154    }
155}
156
157/// Decompress one nooptype element, mirroring `H5Z__nbit_decompress_one_nooptype`.
158fn nbit_decompress_one_nooptype(
159    data: &mut [u8],
160    data_offset: usize,
161    buffer: &[u8],
162    cur: &mut NbitCursor,
163    size: u32,
164) -> FormatResult<()> {
165    for i in 0..size as usize {
166        if cur.j >= buffer.len() {
167            return Err(FormatError::InvalidData("nbit: buffer too short".into()));
168        }
169        let mut val = buffer[cur.j];
170        let mut dat_len: usize = 8;
171        data[data_offset + i] =
172            ((val as u32 & mask_u32(cur.buf_len)) << (dat_len - cur.buf_len)) as u8;
173        dat_len -= cur.buf_len;
174        cur.next_byte();
175        if dat_len == 0 {
176            continue;
177        }
178        if cur.j >= buffer.len() {
179            return Err(FormatError::InvalidData("nbit: buffer too short".into()));
180        }
181        val = buffer[cur.j];
182        data[data_offset + i] |=
183            ((val >> (cur.buf_len - dat_len)) as u32 & mask_u32(dat_len)) as u8;
184        cur.buf_len -= dat_len;
185    }
186    Ok(())
187}
188
189/// Compress one nooptype element, mirroring `H5Z__nbit_compress_one_nooptype`.
190fn nbit_compress_one_nooptype(
191    data: &[u8],
192    data_offset: usize,
193    buffer: &mut [u8],
194    cur: &mut NbitCursor,
195    size: u32,
196) {
197    for i in 0..size as usize {
198        let val = data[data_offset + i];
199        let mut dat_len: usize = 8;
200        buffer[cur.j] |= ((val as u32 >> (dat_len - cur.buf_len)) & mask_u32(cur.buf_len)) as u8;
201        dat_len -= cur.buf_len;
202        cur.next_byte();
203        if dat_len == 0 {
204            continue;
205        }
206        buffer[cur.j] = ((val as u32 & mask_u32(dat_len)) << (cur.buf_len - dat_len)) as u8;
207        cur.buf_len -= dat_len;
208    }
209}
210
211/// Decompress one atomic element, mirroring `H5Z__nbit_decompress_one_atomic`.
212fn nbit_decompress_one_atomic(
213    data: &mut [u8],
214    data_offset: usize,
215    buffer: &[u8],
216    cur: &mut NbitCursor,
217    p: &NbitAtomic,
218) -> FormatResult<()> {
219    let datatype_len = p.size * 8;
220    if p.order == NBIT_ORDER_LE {
221        let begin_i = if !(p.precision + p.offset).is_multiple_of(8) {
222            (p.precision + p.offset) / 8
223        } else {
224            (p.precision + p.offset) / 8 - 1
225        };
226        let end_i = p.offset / 8;
227        let mut k = begin_i as i64;
228        while k >= end_i as i64 {
229            nbit_decompress_one_byte(
230                data,
231                data_offset,
232                k as u32,
233                begin_i,
234                end_i,
235                buffer,
236                cur,
237                p,
238                datatype_len,
239            )?;
240            k -= 1;
241        }
242    } else {
243        let begin_i = (datatype_len - p.precision - p.offset) / 8;
244        let end_i = if !p.offset.is_multiple_of(8) {
245            (datatype_len - p.offset) / 8
246        } else {
247            (datatype_len - p.offset) / 8 - 1
248        };
249        for k in begin_i..=end_i {
250            nbit_decompress_one_byte(
251                data,
252                data_offset,
253                k,
254                begin_i,
255                end_i,
256                buffer,
257                cur,
258                p,
259                datatype_len,
260            )?;
261        }
262    }
263    Ok(())
264}
265
266/// Compress one atomic element, mirroring `H5Z__nbit_compress_one_atomic`.
267fn nbit_compress_one_atomic(
268    data: &[u8],
269    data_offset: usize,
270    buffer: &mut [u8],
271    cur: &mut NbitCursor,
272    p: &NbitAtomic,
273) {
274    let datatype_len = p.size * 8;
275    if p.order == NBIT_ORDER_LE {
276        let begin_i = if !(p.precision + p.offset).is_multiple_of(8) {
277            (p.precision + p.offset) / 8
278        } else {
279            (p.precision + p.offset) / 8 - 1
280        };
281        let end_i = p.offset / 8;
282        let mut k = begin_i as i64;
283        while k >= end_i as i64 {
284            nbit_compress_one_byte(
285                data,
286                data_offset,
287                k as u32,
288                begin_i,
289                end_i,
290                buffer,
291                cur,
292                p,
293                datatype_len,
294            );
295            k -= 1;
296        }
297    } else {
298        let begin_i = (datatype_len - p.precision - p.offset) / 8;
299        let end_i = if !p.offset.is_multiple_of(8) {
300            (datatype_len - p.offset) / 8
301        } else {
302            (datatype_len - p.offset) / 8 - 1
303        };
304        for k in begin_i..=end_i {
305            nbit_compress_one_byte(
306                data,
307                data_offset,
308                k,
309                begin_i,
310                end_i,
311                buffer,
312                cur,
313                p,
314                datatype_len,
315            );
316        }
317    }
318}
319
320/// Read an atomic parameter group starting at `parms[idx]` (after the class
321/// code has already been consumed): `size, order, precision, offset`.
322fn read_atomic(parms: &[u32], idx: &mut usize) -> FormatResult<NbitAtomic> {
323    if *idx + 4 > parms.len() {
324        return Err(FormatError::InvalidData(
325            "nbit: parameter list truncated".into(),
326        ));
327    }
328    let p = NbitAtomic {
329        size: parms[*idx],
330        order: parms[*idx + 1],
331        precision: parms[*idx + 2],
332        offset: parms[*idx + 3],
333    };
334    *idx += 4;
335    // Validate every atomic (top-level, array member, compound member) so
336    // the bit math below cannot overflow or panic on a crafted file.
337    let bits = p.size.checked_mul(8);
338    let span = p.precision.checked_add(p.offset);
339    match (bits, span) {
340        (Some(bits), Some(span))
341            if p.size > 0 && p.precision > 0 && p.precision <= bits && span <= bits => {}
342        _ => {
343            return Err(FormatError::InvalidData(format!(
344                "nbit: invalid atomic datatype (size={}, precision={}, offset={})",
345                p.size, p.precision, p.offset
346            )));
347        }
348    }
349    Ok(p)
350}
351
352/// Decompress one array element, mirroring `H5Z__nbit_decompress_one_array`.
353fn nbit_decompress_one_array(
354    data: &mut [u8],
355    data_offset: usize,
356    buffer: &[u8],
357    cur: &mut NbitCursor,
358    parms: &[u32],
359    parms_index: &mut usize,
360) -> FormatResult<()> {
361    if *parms_index + 2 > parms.len() {
362        return Err(FormatError::InvalidData(
363            "nbit: parameter list truncated".into(),
364        ));
365    }
366    let total_size = parms[*parms_index];
367    let base_class = parms[*parms_index + 1];
368    *parms_index += 2;
369
370    match base_class {
371        NBIT_ATOMIC => {
372            let p = read_atomic(parms, parms_index)?;
373            let n = total_size / p.size;
374            for i in 0..n as usize {
375                nbit_decompress_one_atomic(
376                    data,
377                    data_offset + i * p.size as usize,
378                    buffer,
379                    cur,
380                    &p,
381                )?;
382            }
383        }
384        NBIT_ARRAY => {
385            let base_size = parms[*parms_index];
386            let n = total_size / base_size;
387            let begin_index = *parms_index;
388            for i in 0..n as usize {
389                *parms_index = begin_index;
390                nbit_decompress_one_array(
391                    data,
392                    data_offset + i * base_size as usize,
393                    buffer,
394                    cur,
395                    parms,
396                    parms_index,
397                )?;
398            }
399        }
400        NBIT_COMPOUND => {
401            let base_size = parms[*parms_index];
402            let n = total_size / base_size;
403            let begin_index = *parms_index;
404            for i in 0..n as usize {
405                *parms_index = begin_index;
406                nbit_decompress_one_compound(
407                    data,
408                    data_offset + i * base_size as usize,
409                    buffer,
410                    cur,
411                    parms,
412                    parms_index,
413                )?;
414            }
415        }
416        NBIT_NOOPTYPE => {
417            *parms_index += 1; // skip size of no-op type
418            nbit_decompress_one_nooptype(data, data_offset, buffer, cur, total_size)?;
419        }
420        _ => {
421            return Err(FormatError::InvalidData(format!(
422                "nbit: bad base class {}",
423                base_class
424            )))
425        }
426    }
427    Ok(())
428}
429
430/// Decompress one compound element, mirroring `H5Z__nbit_decompress_one_compound`.
431fn nbit_decompress_one_compound(
432    data: &mut [u8],
433    data_offset: usize,
434    buffer: &[u8],
435    cur: &mut NbitCursor,
436    parms: &[u32],
437    parms_index: &mut usize,
438) -> FormatResult<()> {
439    if *parms_index + 2 > parms.len() {
440        return Err(FormatError::InvalidData(
441            "nbit: parameter list truncated".into(),
442        ));
443    }
444    *parms_index += 1; // skip compound size
445    let nmembers = parms[*parms_index];
446    *parms_index += 1;
447
448    for _ in 0..nmembers {
449        if *parms_index + 2 > parms.len() {
450            return Err(FormatError::InvalidData(
451                "nbit: parameter list truncated".into(),
452            ));
453        }
454        let member_offset = parms[*parms_index] as usize;
455        let member_class = parms[*parms_index + 1];
456        *parms_index += 2;
457
458        match member_class {
459            NBIT_ATOMIC => {
460                let p = read_atomic(parms, parms_index)?;
461                nbit_decompress_one_atomic(data, data_offset + member_offset, buffer, cur, &p)?;
462            }
463            NBIT_ARRAY => {
464                nbit_decompress_one_array(
465                    data,
466                    data_offset + member_offset,
467                    buffer,
468                    cur,
469                    parms,
470                    parms_index,
471                )?;
472            }
473            NBIT_COMPOUND => {
474                nbit_decompress_one_compound(
475                    data,
476                    data_offset + member_offset,
477                    buffer,
478                    cur,
479                    parms,
480                    parms_index,
481                )?;
482            }
483            NBIT_NOOPTYPE => {
484                let size = parms[*parms_index];
485                *parms_index += 1;
486                nbit_decompress_one_nooptype(data, data_offset + member_offset, buffer, cur, size)?;
487            }
488            _ => {
489                return Err(FormatError::InvalidData(format!(
490                    "nbit: bad member class {}",
491                    member_class
492                )))
493            }
494        }
495    }
496    Ok(())
497}
498
499/// Compress one array element, mirroring `H5Z__nbit_compress_one_array`.
500fn nbit_compress_one_array(
501    data: &[u8],
502    data_offset: usize,
503    buffer: &mut [u8],
504    cur: &mut NbitCursor,
505    parms: &[u32],
506    parms_index: &mut usize,
507) -> FormatResult<()> {
508    if *parms_index + 2 > parms.len() {
509        return Err(FormatError::InvalidData(
510            "nbit: parameter list truncated".into(),
511        ));
512    }
513    let total_size = parms[*parms_index];
514    let base_class = parms[*parms_index + 1];
515    *parms_index += 2;
516
517    match base_class {
518        NBIT_ATOMIC => {
519            let p = read_atomic(parms, parms_index)?;
520            let n = total_size / p.size;
521            for i in 0..n as usize {
522                nbit_compress_one_atomic(data, data_offset + i * p.size as usize, buffer, cur, &p);
523            }
524        }
525        NBIT_ARRAY => {
526            let base_size = parms[*parms_index];
527            let n = total_size / base_size;
528            let begin_index = *parms_index;
529            for i in 0..n as usize {
530                *parms_index = begin_index;
531                nbit_compress_one_array(
532                    data,
533                    data_offset + i * base_size as usize,
534                    buffer,
535                    cur,
536                    parms,
537                    parms_index,
538                )?;
539            }
540        }
541        NBIT_COMPOUND => {
542            let base_size = parms[*parms_index];
543            let n = total_size / base_size;
544            let begin_index = *parms_index;
545            for i in 0..n as usize {
546                *parms_index = begin_index;
547                nbit_compress_one_compound(
548                    data,
549                    data_offset + i * base_size as usize,
550                    buffer,
551                    cur,
552                    parms,
553                    parms_index,
554                )?;
555            }
556        }
557        NBIT_NOOPTYPE => {
558            *parms_index += 1;
559            nbit_compress_one_nooptype(data, data_offset, buffer, cur, total_size);
560        }
561        _ => {
562            return Err(FormatError::InvalidData(format!(
563                "nbit: bad base class {}",
564                base_class
565            )))
566        }
567    }
568    Ok(())
569}
570
571/// Compress one compound element, mirroring `H5Z__nbit_compress_one_compound`.
572fn nbit_compress_one_compound(
573    data: &[u8],
574    data_offset: usize,
575    buffer: &mut [u8],
576    cur: &mut NbitCursor,
577    parms: &[u32],
578    parms_index: &mut usize,
579) -> FormatResult<()> {
580    if *parms_index + 2 > parms.len() {
581        return Err(FormatError::InvalidData(
582            "nbit: parameter list truncated".into(),
583        ));
584    }
585    *parms_index += 1;
586    let nmembers = parms[*parms_index];
587    *parms_index += 1;
588
589    for _ in 0..nmembers {
590        if *parms_index + 2 > parms.len() {
591            return Err(FormatError::InvalidData(
592                "nbit: parameter list truncated".into(),
593            ));
594        }
595        let member_offset = parms[*parms_index] as usize;
596        let member_class = parms[*parms_index + 1];
597        *parms_index += 2;
598
599        match member_class {
600            NBIT_ATOMIC => {
601                let p = read_atomic(parms, parms_index)?;
602                nbit_compress_one_atomic(data, data_offset + member_offset, buffer, cur, &p);
603            }
604            NBIT_ARRAY => {
605                nbit_compress_one_array(
606                    data,
607                    data_offset + member_offset,
608                    buffer,
609                    cur,
610                    parms,
611                    parms_index,
612                )?;
613            }
614            NBIT_COMPOUND => {
615                nbit_compress_one_compound(
616                    data,
617                    data_offset + member_offset,
618                    buffer,
619                    cur,
620                    parms,
621                    parms_index,
622                )?;
623            }
624            NBIT_NOOPTYPE => {
625                let size = parms[*parms_index];
626                *parms_index += 1;
627                nbit_compress_one_nooptype(data, data_offset + member_offset, buffer, cur, size);
628            }
629            _ => {
630                return Err(FormatError::InvalidData(format!(
631                    "nbit: bad member class {}",
632                    member_class
633                )))
634            }
635        }
636    }
637    Ok(())
638}
639
640/// Apply the HDF5 N-bit filter.
641///
642/// `cd_values` follows `H5Znbit.c`'s schema:
643/// `[0]` = number of parameters, `[1]` = need-not-compress flag,
644/// `[2]` = element count, `[3..]` = the datatype parameter tree.
645///
646/// On compress, `data` is the raw element buffer; on decompress, `data`
647/// is the packed buffer and the result is the unpacked element buffer.
648pub fn apply_nbit(data: &[u8], cd_values: &[u32], compress: bool) -> FormatResult<Vec<u8>> {
649    if cd_values.len() < 4 {
650        return Err(FormatError::InvalidData("nbit: cd_values too short".into()));
651    }
652    // cd_values[1] != 0 -> data is full-precision, filter is a pass-through.
653    if cd_values[1] != 0 {
654        return Ok(data.to_vec());
655    }
656
657    let d_nelmts = cd_values[2] as usize;
658    let dtype_size = cd_values[4] as usize;
659    if dtype_size == 0 {
660        return Err(FormatError::InvalidData("nbit: zero datatype size".into()));
661    }
662    let unpacked_size = d_nelmts * dtype_size;
663
664    if compress {
665        if data.len() != unpacked_size {
666            return Err(FormatError::InvalidData(format!(
667                "nbit: input size {} != expected {}",
668                data.len(),
669                unpacked_size
670            )));
671        }
672        // Worst case the packed buffer is the same size as the unpacked one.
673        let mut buffer = vec![0u8; unpacked_size + 1];
674        let mut cur = NbitCursor { j: 0, buf_len: 8 };
675        match cd_values[3] {
676            NBIT_ATOMIC => {
677                let mut idx = 4;
678                let p = read_atomic(cd_values, &mut idx)?;
679                for i in 0..d_nelmts {
680                    nbit_compress_one_atomic(data, i * p.size as usize, &mut buffer, &mut cur, &p);
681                }
682            }
683            NBIT_ARRAY => {
684                let size = cd_values[4] as usize;
685                for i in 0..d_nelmts {
686                    let mut idx = 4;
687                    nbit_compress_one_array(
688                        data,
689                        i * size,
690                        &mut buffer,
691                        &mut cur,
692                        cd_values,
693                        &mut idx,
694                    )?;
695                }
696            }
697            NBIT_COMPOUND => {
698                let size = cd_values[4] as usize;
699                for i in 0..d_nelmts {
700                    let mut idx = 4;
701                    nbit_compress_one_compound(
702                        data,
703                        i * size,
704                        &mut buffer,
705                        &mut cur,
706                        cd_values,
707                        &mut idx,
708                    )?;
709                }
710            }
711            other => {
712                return Err(FormatError::InvalidData(format!(
713                    "nbit: unsupported top class {}",
714                    other
715                )))
716            }
717        }
718        // libhdf5 reports new_size + 1 (any hanging bits round up).
719        buffer.truncate(cur.j + 1);
720        Ok(buffer)
721    } else {
722        let mut out = vec![0u8; unpacked_size];
723        let mut cur = NbitCursor { j: 0, buf_len: 8 };
724        match cd_values[3] {
725            NBIT_ATOMIC => {
726                let mut idx = 4;
727                let p = read_atomic(cd_values, &mut idx)?;
728                if p.precision > p.size * 8 || p.precision + p.offset > p.size * 8 {
729                    return Err(FormatError::InvalidData(
730                        "nbit: invalid precision/offset".into(),
731                    ));
732                }
733                for i in 0..d_nelmts {
734                    nbit_decompress_one_atomic(&mut out, i * p.size as usize, data, &mut cur, &p)?;
735                }
736            }
737            NBIT_ARRAY => {
738                let size = cd_values[4] as usize;
739                for i in 0..d_nelmts {
740                    let mut idx = 4;
741                    nbit_decompress_one_array(
742                        &mut out,
743                        i * size,
744                        data,
745                        &mut cur,
746                        cd_values,
747                        &mut idx,
748                    )?;
749                }
750            }
751            NBIT_COMPOUND => {
752                let size = cd_values[4] as usize;
753                for i in 0..d_nelmts {
754                    let mut idx = 4;
755                    nbit_decompress_one_compound(
756                        &mut out,
757                        i * size,
758                        data,
759                        &mut cur,
760                        cd_values,
761                        &mut idx,
762                    )?;
763                }
764            }
765            other => {
766                return Err(FormatError::InvalidData(format!(
767                    "nbit: unsupported top class {}",
768                    other
769                )))
770            }
771        }
772        Ok(out)
773    }
774}
775
776// ===========================================================================
777//  Scale-offset filter (H5Z_FILTER_SCALEOFFSET, id 6)
778// ===========================================================================
779
780// cd_values index layout (H5Zscaleoffset.c).
781const SO_PARM_SCALETYPE: usize = 0;
782const SO_PARM_SCALEFACTOR: usize = 1;
783const SO_PARM_NELMTS: usize = 2;
784const SO_PARM_CLASS: usize = 3;
785const SO_PARM_SIZE: usize = 4;
786const SO_PARM_SIGN: usize = 5;
787const SO_PARM_ORDER: usize = 6;
788const SO_PARM_FILAVAIL: usize = 7;
789/// First cd_values index holding the (optional) packed fill value.
790const SO_PARM_FILVAL: usize = 8;
791
792const SO_CLS_INTEGER: u32 = 0;
793const SO_CLS_FLOAT: u32 = 1;
794const SO_ORDER_LE: u32 = 0;
795const SO_FILL_DEFINED: u32 = 1;
796// Float scale type: 0 = variable-minimum-bits (D-scale); 1 = E-scale (unsupported).
797const SO_FLOAT_DSCALE: u32 = 0;
798
799/// 21-byte parameter header stored in front of every scale-offset chunk.
800const SO_BUF_OFFSET: usize = 21;
801
802/// Decompress one scale-offset byte, mirroring
803/// `H5Z__scaleoffset_decompress_one_byte`.
804#[allow(clippy::too_many_arguments)]
805fn so_decompress_one_byte(
806    data: &mut [u8],
807    data_offset: usize,
808    k: u32,
809    begin_i: u32,
810    buffer: &[u8],
811    cur: &mut NbitCursor,
812    minbits: u32,
813    dtype_len: u32,
814) -> FormatResult<()> {
815    if cur.j >= buffer.len() {
816        return Err(FormatError::InvalidData(
817            "scaleoffset: buffer too short".into(),
818        ));
819    }
820    let mut val = buffer[cur.j];
821    let mut bits_to_copy: usize = if k == begin_i {
822        8 - ((dtype_len - minbits) % 8) as usize
823    } else {
824        8
825    };
826
827    let idx = data_offset + k as usize;
828    if cur.buf_len > bits_to_copy {
829        data[idx] = ((val >> (cur.buf_len - bits_to_copy)) as u32 & mask_u32(bits_to_copy)) as u8;
830        cur.buf_len -= bits_to_copy;
831    } else {
832        data[idx] = ((val as u32 & mask_u32(cur.buf_len)) << (bits_to_copy - cur.buf_len)) as u8;
833        bits_to_copy -= cur.buf_len;
834        cur.next_byte();
835        if bits_to_copy == 0 {
836            return Ok(());
837        }
838        if cur.j >= buffer.len() {
839            return Err(FormatError::InvalidData(
840                "scaleoffset: buffer too short".into(),
841            ));
842        }
843        val = buffer[cur.j];
844        data[idx] |= ((val >> (cur.buf_len - bits_to_copy)) as u32 & mask_u32(bits_to_copy)) as u8;
845        cur.buf_len -= bits_to_copy;
846    }
847    Ok(())
848}
849
850/// Decompress one scale-offset atomic element, mirroring
851/// `H5Z__scaleoffset_decompress_one_atomic`.
852fn so_decompress_one_atomic(
853    data: &mut [u8],
854    data_offset: usize,
855    buffer: &[u8],
856    cur: &mut NbitCursor,
857    size: u32,
858    minbits: u32,
859    order: u32,
860) -> FormatResult<()> {
861    let dtype_len = size * 8;
862    if order == SO_ORDER_LE {
863        let begin_i = size - 1 - (dtype_len - minbits) / 8;
864        let mut k = begin_i as i64;
865        while k >= 0 {
866            so_decompress_one_byte(
867                data,
868                data_offset,
869                k as u32,
870                begin_i,
871                buffer,
872                cur,
873                minbits,
874                dtype_len,
875            )?;
876            k -= 1;
877        }
878    } else {
879        let begin_i = (dtype_len - minbits) / 8;
880        for k in begin_i..=(size - 1) {
881            so_decompress_one_byte(
882                data,
883                data_offset,
884                k,
885                begin_i,
886                buffer,
887                cur,
888                minbits,
889                dtype_len,
890            )?;
891        }
892    }
893    Ok(())
894}
895
896/// Read a little-/big-endian integer of `size` bytes from `data` at `offset`.
897fn read_uint(data: &[u8], offset: usize, size: usize, order: u32) -> u64 {
898    let mut v: u64 = 0;
899    if order == SO_ORDER_LE {
900        for i in 0..size {
901            v |= (data[offset + i] as u64) << (i * 8);
902        }
903    } else {
904        for i in 0..size {
905            v = (v << 8) | data[offset + i] as u64;
906        }
907    }
908    v
909}
910
911/// Write a little-/big-endian integer of `size` bytes into `data` at `offset`.
912fn write_uint(data: &mut [u8], offset: usize, size: usize, order: u32, v: u64) {
913    if order == SO_ORDER_LE {
914        for i in 0..size {
915            data[offset + i] = (v >> (i * 8)) as u8;
916        }
917    } else {
918        for i in 0..size {
919            data[offset + i] = (v >> ((size - 1 - i) * 8)) as u8;
920        }
921    }
922}
923
924/// Reverse the HDF5 scale-offset filter (decompress only).
925///
926/// `cd_values` follows `H5Zscaleoffset.c`'s 20-entry schema. The output is
927/// the raw element buffer in the dataset datatype's byte order.
928pub fn reverse_scaleoffset(data: &[u8], cd_values: &[u32]) -> FormatResult<Vec<u8>> {
929    if cd_values.len() < 8 {
930        return Err(FormatError::InvalidData(
931            "scaleoffset: cd_values too short".into(),
932        ));
933    }
934    let scale_type = cd_values[SO_PARM_SCALETYPE];
935    let scale_factor = cd_values[SO_PARM_SCALEFACTOR] as i32;
936    let d_nelmts = cd_values[SO_PARM_NELMTS] as usize;
937    let dtype_class = cd_values[SO_PARM_CLASS];
938    let size = cd_values[SO_PARM_SIZE] as usize;
939    let dtype_sign = cd_values[SO_PARM_SIGN];
940    let order = cd_values[SO_PARM_ORDER];
941    let filavail = cd_values[SO_PARM_FILAVAIL];
942
943    if size == 0 || size > 8 {
944        return Err(FormatError::InvalidData(format!(
945            "scaleoffset: unsupported datatype size {}",
946            size
947        )));
948    }
949    // Reconstruct the packed fill value from cd_values[8..]. libhdf5 stores
950    // it 4 bytes per cd_value, least-significant cd_value first; each cd_value
951    // holds the bytes in the dataset datatype's byte order. We read it as a
952    // raw `size`-byte little-endian-composed value (correct for the common
953    // little-endian-dataset case h5py emits on x86/ARM).
954    let filval: u64 = if filavail == SO_FILL_DEFINED {
955        let mut v: u64 = 0;
956        let n_cd = size.div_ceil(4);
957        if cd_values.len() < SO_PARM_FILVAL + n_cd {
958            return Err(FormatError::InvalidData(
959                "scaleoffset: cd_values missing fill value".into(),
960            ));
961        }
962        for (w, cd) in cd_values[SO_PARM_FILVAL..SO_PARM_FILVAL + n_cd]
963            .iter()
964            .enumerate()
965        {
966            v |= (*cd as u64) << (w * 32);
967        }
968        if size < 8 {
969            v &= (1u64 << (size * 8)) - 1;
970        }
971        v
972    } else {
973        0
974    };
975    if dtype_class == SO_CLS_FLOAT && scale_type != SO_FLOAT_DSCALE {
976        return Err(FormatError::UnsupportedFeature(
977            "scaleoffset E-scaling method is not supported".into(),
978        ));
979    }
980
981    let size_out = d_nelmts * size;
982
983    // For integer types, scale_factor < 0 is reset to 0 by the library.
984    let int_scalefactor = if scale_factor < 0 { 0 } else { scale_factor };
985    if dtype_class == SO_CLS_INTEGER && int_scalefactor as usize == size * 8 {
986        // No processing: payload after the header is the raw data.
987        if data.len() < SO_BUF_OFFSET + size_out {
988            return Err(FormatError::InvalidData(
989                "scaleoffset: buffer too short".into(),
990            ));
991        }
992        return Ok(data[SO_BUF_OFFSET..SO_BUF_OFFSET + size_out].to_vec());
993    }
994
995    // Read minbits + minval from the 21-byte header (always little-endian).
996    if data.len() < SO_BUF_OFFSET {
997        return Err(FormatError::InvalidData(
998            "scaleoffset: buffer too short for header".into(),
999        ));
1000    }
1001    let mut minbits: u32 = 0;
1002    for (i, &b) in data[..4].iter().enumerate() {
1003        minbits |= (b as u32) << (i * 8);
1004    }
1005    if minbits as usize > size * 8 {
1006        return Err(FormatError::InvalidData(
1007            "scaleoffset: minbits exceeds datatype size".into(),
1008        ));
1009    }
1010    let minval_size = std::cmp::min(8usize, data[4] as usize);
1011    let mut minval: u64 = 0;
1012    for i in 0..minval_size {
1013        minval |= (data[5 + i] as u64) << (i * 8);
1014    }
1015
1016    // Special case: full precision -> payload copied verbatim.
1017    if minbits as usize == size * 8 {
1018        if data.len() < SO_BUF_OFFSET + size_out {
1019            return Err(FormatError::InvalidData(
1020                "scaleoffset: buffer too short".into(),
1021            ));
1022        }
1023        return Ok(data[SO_BUF_OFFSET..SO_BUF_OFFSET + size_out].to_vec());
1024    }
1025
1026    let mut out = vec![0u8; size_out];
1027
1028    if minbits != 0 {
1029        if data.len() < SO_BUF_OFFSET {
1030            return Err(FormatError::InvalidData(
1031                "scaleoffset: buffer too short".into(),
1032            ));
1033        }
1034        let payload = &data[SO_BUF_OFFSET..];
1035        let mut cur = NbitCursor { j: 0, buf_len: 8 };
1036        for i in 0..d_nelmts {
1037            so_decompress_one_atomic(
1038                &mut out,
1039                i * size,
1040                payload,
1041                &mut cur,
1042                size as u32,
1043                minbits,
1044                order,
1045            )?;
1046        }
1047    }
1048    // minbits == 0: out stays all-zero (all elements identical, no fill value).
1049
1050    // Postprocess: add back minval (and apply float scaling).
1051    postdecompress(
1052        &mut out,
1053        d_nelmts,
1054        size,
1055        order,
1056        dtype_class,
1057        dtype_sign,
1058        minbits,
1059        minval,
1060        scale_factor,
1061        filavail == SO_FILL_DEFINED,
1062        filval,
1063    );
1064
1065    Ok(out)
1066}
1067
1068/// Sign-extend the low `size*8` bits of `v` to a full `i64`.
1069fn sign_extend(v: u64, size: usize) -> i64 {
1070    if size >= 8 {
1071        return v as i64;
1072    }
1073    let bits = size * 8;
1074    let shift = 64 - bits;
1075    ((v << shift) as i64) >> shift
1076}
1077
1078/// Postprocess decompressed scale-offset data.
1079#[allow(clippy::too_many_arguments)]
1080fn postdecompress(
1081    out: &mut [u8],
1082    d_nelmts: usize,
1083    size: usize,
1084    order: u32,
1085    dtype_class: u32,
1086    dtype_sign: u32,
1087    minbits: u32,
1088    minval: u64,
1089    scale_factor: i32,
1090    fill_defined: bool,
1091    filval: u64,
1092) {
1093    // Sentinel: a fully decompressed value equal to (1 << minbits) - 1 is
1094    // restored to the fill value rather than offset-added.
1095    let sentinel: u64 = if (minbits as usize) >= 64 {
1096        u64::MAX
1097    } else {
1098        (1u64 << minbits) - 1
1099    };
1100    let width_mask: u64 = if size >= 8 {
1101        u64::MAX
1102    } else {
1103        (1u64 << (size * 8)) - 1
1104    };
1105
1106    if dtype_class == SO_CLS_INTEGER {
1107        // buf[i] = (buf[i] == sentinel) ? filval : buf[i] + minval.
1108        for i in 0..d_nelmts {
1109            let off = i * size;
1110            let v = read_uint(out, off, size, order);
1111            let result = if fill_defined && v == sentinel {
1112                filval
1113            } else {
1114                v.wrapping_add(minval) & width_mask
1115            };
1116            write_uint(out, off, size, order, result);
1117        }
1118        let _ = dtype_sign;
1119    } else {
1120        // Float D-scale: value = (signed decompressed int) / 10^D + min,
1121        // where `min` reinterprets `minval`'s low bits as the float type.
1122        let d_val = scale_factor as f64;
1123        let divisor = 10f64.powf(d_val);
1124        if size == 4 {
1125            let min = f32::from_bits(minval as u32);
1126            let filval_f = f32::from_bits(filval as u32);
1127            for i in 0..d_nelmts {
1128                let off = i * size;
1129                let raw = read_uint(out, off, size, order);
1130                let val = if fill_defined && raw == sentinel {
1131                    filval_f
1132                } else {
1133                    (sign_extend(raw, size) as f32) / (divisor as f32) + min
1134                };
1135                write_uint(out, off, size, order, val.to_bits() as u64);
1136            }
1137        } else if size == 8 {
1138            let min = f64::from_bits(minval);
1139            let filval_f = f64::from_bits(filval);
1140            for i in 0..d_nelmts {
1141                let off = i * size;
1142                let raw = read_uint(out, off, size, order);
1143                if fill_defined && raw == sentinel {
1144                    write_uint(out, off, size, order, filval_f.to_bits());
1145                    continue;
1146                }
1147                let val = (sign_extend(raw, size) as f64) / divisor + min;
1148                write_uint(out, off, size, order, val.to_bits());
1149            }
1150        }
1151    }
1152}
1153
1154// ===========================================================================
1155//  Post-filter datatype conversion (H5T_convert equivalent)
1156// ===========================================================================
1157
1158use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
1159
1160/// True if `dt` is a standard IEEE-754 binary32/binary64 layout (the only
1161/// floating-point layouts the crate can faithfully reinterpret in place).
1162fn is_standard_ieee_float(dt: &DatatypeMessage) -> bool {
1163    match dt {
1164        DatatypeMessage::FloatingPoint {
1165            size,
1166            sign_location,
1167            bit_offset,
1168            bit_precision,
1169            exponent_location,
1170            exponent_size,
1171            mantissa_location,
1172            mantissa_size,
1173            exponent_bias,
1174            ..
1175        } => {
1176            let bits = *size * 8;
1177            let is_ieee32 = bits == 32
1178                && *bit_offset == 0
1179                && *bit_precision == 32
1180                && *sign_location == 31
1181                && *exponent_location == 23
1182                && *exponent_size == 8
1183                && *mantissa_location == 0
1184                && *mantissa_size == 23
1185                && *exponent_bias == 127;
1186            let is_ieee64 = bits == 64
1187                && *bit_offset == 0
1188                && *bit_precision == 64
1189                && *sign_location == 63
1190                && *exponent_location == 52
1191                && *exponent_size == 11
1192                && *mantissa_location == 0
1193                && *mantissa_size == 52
1194                && *exponent_bias == 1023;
1195            is_ieee32 || is_ieee64
1196        }
1197        _ => false,
1198    }
1199}
1200
1201/// True if the filter-pipeline / on-disk output for `dt` needs a post-filter
1202/// datatype conversion before the element values are usable.
1203///
1204/// For a `FixedPoint` datatype the filter pipeline output (or contiguous
1205/// on-disk bytes) carries the significant value in `bit_precision` bits
1206/// starting at `bit_offset`, with the rest zero-filled and the sign bit NOT
1207/// extended. libhdf5 fixes this up with a datatype conversion
1208/// (`H5T_convert`) after the filter pipeline; this returns true for any
1209/// such non-trivial layout.
1210///
1211/// It also returns true for a non-standard `FloatingPoint` layout, so the
1212/// caller routes it through [`apply_datatype_conversion`], which then
1213/// returns a clear error rather than silently yielding wrong data.
1214pub fn datatype_needs_bit_conversion(dt: &DatatypeMessage) -> bool {
1215    match dt {
1216        DatatypeMessage::FixedPoint {
1217            size,
1218            bit_offset,
1219            bit_precision,
1220            ..
1221        } => *bit_offset != 0 || (*bit_precision as u32) < *size * 8,
1222        DatatypeMessage::FloatingPoint { .. } => !is_standard_ieee_float(dt),
1223        _ => false,
1224    }
1225}
1226
1227/// Apply the post-filter datatype conversion in place to a fully-decoded
1228/// output buffer.
1229///
1230/// This mirrors libhdf5's `H5T_convert` step that runs AFTER the filter
1231/// pipeline. For a `FixedPoint` datatype with `bit_offset != 0` or
1232/// `bit_precision < size*8`, each `size`-byte element is rewritten so the
1233/// significant value occupies the whole element with bit offset 0:
1234///
1235///   1. interpret the element as an unsigned integer (respecting byte order),
1236///   2. shift right by `bit_offset`,
1237///   3. mask to `bit_precision` low bits,
1238///   4. sign-extend from bit `bit_precision-1` if the type is signed,
1239///   5. write the result back in the same byte order.
1240///
1241/// It is a strict no-op for ordinary full-width datatypes (and for any
1242/// non-`FixedPoint` class).
1243///
1244/// For `FloatingPoint` types with a non-standard bit layout that cannot be
1245/// faithfully reinterpreted, an error is returned rather than wrong data.
1246pub fn apply_datatype_conversion(buffer: &mut [u8], dt: &DatatypeMessage) -> FormatResult<()> {
1247    match dt {
1248        DatatypeMessage::FixedPoint {
1249            size,
1250            byte_order,
1251            signed,
1252            bit_offset,
1253            bit_precision,
1254        } => {
1255            let size = *size as usize;
1256            let precision = *bit_precision as usize;
1257            let offset = *bit_offset as usize;
1258
1259            // Full-width plain integer: nothing to do.
1260            if offset == 0 && precision == size * 8 {
1261                return Ok(());
1262            }
1263            if size == 0 || size > 8 {
1264                return Err(FormatError::InvalidData(format!(
1265                    "datatype conversion: unsupported FixedPoint size {size}"
1266                )));
1267            }
1268            if precision == 0 || offset + precision > size * 8 {
1269                return Err(FormatError::InvalidData(format!(
1270                    "datatype conversion: invalid bit layout (offset {offset}, \
1271                     precision {precision}, size {size})"
1272                )));
1273            }
1274            if !buffer.len().is_multiple_of(size) {
1275                return Err(FormatError::InvalidData(format!(
1276                    "datatype conversion: buffer length {} not a multiple of \
1277                     element size {size}",
1278                    buffer.len()
1279                )));
1280            }
1281
1282            let big_endian = matches!(byte_order, ByteOrder::BigEndian);
1283            let precision_mask: u64 = if precision == 64 {
1284                u64::MAX
1285            } else {
1286                (1u64 << precision) - 1
1287            };
1288            let sign_bit: u64 = 1u64 << (precision - 1);
1289
1290            for elem in buffer.chunks_exact_mut(size) {
1291                // Load element as a u64 in native value space.
1292                let mut raw: u64 = 0;
1293                if big_endian {
1294                    for &b in elem.iter() {
1295                        raw = (raw << 8) | b as u64;
1296                    }
1297                } else {
1298                    for (i, &b) in elem.iter().enumerate() {
1299                        raw |= (b as u64) << (8 * i);
1300                    }
1301                }
1302
1303                // Extract the significant bits.
1304                let mut value = (raw >> offset) & precision_mask;
1305
1306                // Sign-extend from bit `precision-1` when signed.
1307                if *signed && (value & sign_bit) != 0 {
1308                    value |= !precision_mask;
1309                }
1310
1311                // Store back in the same byte order, full element width.
1312                if big_endian {
1313                    for i in 0..size {
1314                        elem[size - 1 - i] = (value >> (8 * i)) as u8;
1315                    }
1316                } else {
1317                    for (i, b) in elem.iter_mut().enumerate() {
1318                        *b = (value >> (8 * i)) as u8;
1319                    }
1320                }
1321            }
1322            Ok(())
1323        }
1324        DatatypeMessage::FloatingPoint { .. } => {
1325            // Standard IEEE-754 layouts need no conversion. Anything else
1326            // cannot be faithfully reinterpreted here.
1327            if is_standard_ieee_float(dt) {
1328                Ok(())
1329            } else {
1330                Err(FormatError::InvalidData(
1331                    "datatype conversion: non-standard floating-point bit \
1332                     layout cannot be converted"
1333                        .into(),
1334                ))
1335            }
1336        }
1337        _ => Ok(()),
1338    }
1339}
1340
1341// ===========================================================================
1342//  Tests
1343// ===========================================================================
1344#[cfg(test)]
1345mod tests {
1346    use super::*;
1347
1348    /// Build an nbit cd_values list for an unsigned little-endian atomic int.
1349    fn nbit_atomic_cd(d_nelmts: u32, size: u32, precision: u32, offset: u32) -> Vec<u32> {
1350        // [0]=nparms [1]=need_not_compress [2]=d_nelmts [3]=class [4]=size
1351        // [5]=order [6]=precision [7]=offset
1352        let need_not_compress = if offset == 0 && precision == size * 8 {
1353            1
1354        } else {
1355            0
1356        };
1357        vec![
1358            8,
1359            need_not_compress,
1360            d_nelmts,
1361            NBIT_ATOMIC,
1362            size,
1363            NBIT_ORDER_LE,
1364            precision,
1365            offset,
1366        ]
1367    }
1368
1369    #[test]
1370    fn nbit_roundtrip_u16_precision12() {
1371        // 16-bit storage, 12-bit precision, offset 0.
1372        let values: Vec<u16> = (0..40u16).map(|i| (i * 71) & 0x0FFF).collect();
1373        let mut raw = Vec::new();
1374        for &v in &values {
1375            raw.extend_from_slice(&v.to_le_bytes());
1376        }
1377        let cd = nbit_atomic_cd(values.len() as u32, 2, 12, 0);
1378        let packed = apply_nbit(&raw, &cd, true).unwrap();
1379        assert!(packed.len() <= raw.len());
1380        let unpacked = apply_nbit(&packed, &cd, false).unwrap();
1381        assert_eq!(unpacked, raw);
1382    }
1383
1384    #[test]
1385    fn nbit_roundtrip_u32_precision20_offset4() {
1386        let values: Vec<u32> = (0..32u32).map(|i| ((i * 9999) & 0xFFFFF) << 4).collect();
1387        let mut raw = Vec::new();
1388        for &v in &values {
1389            raw.extend_from_slice(&v.to_le_bytes());
1390        }
1391        let cd = nbit_atomic_cd(values.len() as u32, 4, 20, 4);
1392        let packed = apply_nbit(&raw, &cd, true).unwrap();
1393        let unpacked = apply_nbit(&packed, &cd, false).unwrap();
1394        assert_eq!(unpacked, raw);
1395    }
1396
1397    #[test]
1398    fn nbit_passthrough_full_precision() {
1399        let raw: Vec<u8> = (0..64).collect();
1400        let cd = nbit_atomic_cd(16, 4, 32, 0); // full precision -> need_not_compress
1401        let packed = apply_nbit(&raw, &cd, true).unwrap();
1402        assert_eq!(packed, raw);
1403        let unpacked = apply_nbit(&packed, &cd, false).unwrap();
1404        assert_eq!(unpacked, raw);
1405    }
1406
1407    #[test]
1408    fn nbit_roundtrip_big_endian() {
1409        let values: Vec<u16> = (0..24u16).map(|i| (i * 53) & 0x03FF).collect();
1410        let mut raw = Vec::new();
1411        for &v in &values {
1412            raw.extend_from_slice(&v.to_be_bytes());
1413        }
1414        let mut cd = nbit_atomic_cd(values.len() as u32, 2, 10, 0);
1415        cd[5] = NBIT_ORDER_BE;
1416        let packed = apply_nbit(&raw, &cd, true).unwrap();
1417        let unpacked = apply_nbit(&packed, &cd, false).unwrap();
1418        assert_eq!(unpacked, raw);
1419    }
1420
1421    // ---------------------------------------------------------------
1422    //  Post-filter datatype conversion
1423    // ---------------------------------------------------------------
1424
1425    fn fixed(size: u32, signed: bool, offset: u16, precision: u16) -> DatatypeMessage {
1426        DatatypeMessage::FixedPoint {
1427            size,
1428            byte_order: ByteOrder::LittleEndian,
1429            signed,
1430            bit_offset: offset,
1431            bit_precision: precision,
1432        }
1433    }
1434
1435    #[test]
1436    fn conversion_noop_for_full_width_types() {
1437        // 32-bit unsigned, offset 0, precision 32 -> plain integer, no-op.
1438        let dt = fixed(4, false, 0, 32);
1439        assert!(!datatype_needs_bit_conversion(&dt));
1440        let mut buf = vec![0x78, 0x56, 0x34, 0x12, 0xFF, 0xFF, 0xFF, 0xFF];
1441        let before = buf.clone();
1442        apply_datatype_conversion(&mut buf, &dt).unwrap();
1443        assert_eq!(buf, before);
1444    }
1445
1446    #[test]
1447    fn conversion_noop_for_non_numeric_types() {
1448        let dt = DatatypeMessage::fixed_string(8);
1449        assert!(!datatype_needs_bit_conversion(&dt));
1450        let mut buf = b"hello!!\0".to_vec();
1451        let before = buf.clone();
1452        apply_datatype_conversion(&mut buf, &dt).unwrap();
1453        assert_eq!(buf, before);
1454    }
1455
1456    #[test]
1457    fn conversion_unsigned_offset_shifts_right() {
1458        // u16, bit_offset 3, precision 10. The value lives in bits [3,13).
1459        // Raw element layout (LE u16): value 0x2A5 placed at offset 3 ->
1460        // 0x2A5 << 3 = 0x1528.
1461        let dt = fixed(2, false, 3, 10);
1462        assert!(datatype_needs_bit_conversion(&dt));
1463        let mut buf = (0x1528u16).to_le_bytes().to_vec();
1464        apply_datatype_conversion(&mut buf, &dt).unwrap();
1465        assert_eq!(u16::from_le_bytes([buf[0], buf[1]]), 0x2A5);
1466    }
1467
1468    #[test]
1469    fn conversion_signed_negative_sign_extends() {
1470        // i16, bit_offset 4, precision 8. Store -3 (8-bit two's complement
1471        // = 0xFD) at offset 4: 0xFD << 4 = 0xFD0.
1472        let dt = fixed(2, true, 4, 8);
1473        let mut buf = (0x0FD0u16).to_le_bytes().to_vec();
1474        apply_datatype_conversion(&mut buf, &dt).unwrap();
1475        assert_eq!(i16::from_le_bytes([buf[0], buf[1]]), -3);
1476    }
1477
1478    #[test]
1479    fn conversion_signed_positive_stays_positive() {
1480        // i16, bit_offset 4, precision 8. Store +5 at offset 4 -> 0x050.
1481        let dt = fixed(2, true, 4, 8);
1482        let mut buf = (0x0050u16).to_le_bytes().to_vec();
1483        apply_datatype_conversion(&mut buf, &dt).unwrap();
1484        assert_eq!(i16::from_le_bytes([buf[0], buf[1]]), 5);
1485    }
1486
1487    #[test]
1488    fn conversion_reduced_precision_offset_zero() {
1489        // i32, bit_offset 0, precision 20 -> still non-trivial (precision <
1490        // size*8). Store -1 in 20 bits = 0xFFFFF.
1491        let dt = fixed(4, true, 0, 20);
1492        assert!(datatype_needs_bit_conversion(&dt));
1493        let mut buf = (0x000FFFFFu32).to_le_bytes().to_vec();
1494        apply_datatype_conversion(&mut buf, &dt).unwrap();
1495        assert_eq!(i32::from_le_bytes(buf.clone().try_into().unwrap()), -1);
1496    }
1497
1498    #[test]
1499    fn conversion_big_endian_signed() {
1500        // i16, BE, bit_offset 4, precision 8, value -3.
1501        let dt = DatatypeMessage::FixedPoint {
1502            size: 2,
1503            byte_order: ByteOrder::BigEndian,
1504            signed: true,
1505            bit_offset: 4,
1506            bit_precision: 8,
1507        };
1508        let mut buf = (0x0FD0u16).to_be_bytes().to_vec();
1509        apply_datatype_conversion(&mut buf, &dt).unwrap();
1510        assert_eq!(i16::from_be_bytes([buf[0], buf[1]]), -3);
1511    }
1512
1513    #[test]
1514    fn conversion_multiple_elements() {
1515        // u32, bit_offset 5, precision 16. Three elements.
1516        let dt = fixed(4, false, 5, 16);
1517        let vals: [u32; 3] = [0x1234, 0xABCD, 0x0001];
1518        let mut buf = Vec::new();
1519        for v in vals {
1520            buf.extend_from_slice(&(v << 5).to_le_bytes());
1521        }
1522        apply_datatype_conversion(&mut buf, &dt).unwrap();
1523        for (i, v) in vals.iter().enumerate() {
1524            let e = u32::from_le_bytes(buf[i * 4..i * 4 + 4].try_into().unwrap());
1525            assert_eq!(e, *v);
1526        }
1527    }
1528
1529    #[test]
1530    fn conversion_rejects_non_standard_float() {
1531        // A float with a non-IEEE bit layout must error, not corrupt data.
1532        let dt = DatatypeMessage::FloatingPoint {
1533            size: 4,
1534            byte_order: ByteOrder::LittleEndian,
1535            sign_location: 30,
1536            bit_offset: 1,
1537            bit_precision: 31,
1538            exponent_location: 22,
1539            exponent_size: 8,
1540            mantissa_location: 0,
1541            mantissa_size: 22,
1542            exponent_bias: 127,
1543        };
1544        assert!(datatype_needs_bit_conversion(&dt));
1545        let mut buf = vec![0u8; 4];
1546        assert!(apply_datatype_conversion(&mut buf, &dt).is_err());
1547    }
1548
1549    #[test]
1550    fn conversion_standard_float_is_noop() {
1551        let dt = DatatypeMessage::f64_type();
1552        assert!(!datatype_needs_bit_conversion(&dt));
1553        let mut buf = 12.5f64.to_le_bytes().to_vec();
1554        let before = buf.clone();
1555        apply_datatype_conversion(&mut buf, &dt).unwrap();
1556        assert_eq!(buf, before);
1557    }
1558
1559    #[test]
1560    fn conversion_rejects_bad_buffer_length() {
1561        let dt = fixed(4, false, 3, 16);
1562        let mut buf = vec![0u8; 5]; // not a multiple of 4
1563        assert!(apply_datatype_conversion(&mut buf, &dt).is_err());
1564    }
1565}