1#[cfg(not(feature = "std"))]
28extern crate alloc;
29
30#[cfg(not(feature = "std"))]
31use alloc::{string::ToString, vec, vec::Vec};
32
33use crate::convert::TryToUsize;
34use crate::datatype::{Datatype, DatatypeByteOrder};
35use crate::error::FormatError;
36use crate::filter_pipeline::FilterDescription;
37
38const PARM_SCALETYPE: usize = 0;
40const PARM_SCALEFACTOR: usize = 1;
41const PARM_NELMTS: usize = 2;
42const PARM_CLASS: usize = 3;
43const PARM_SIZE: usize = 4;
44const PARM_SIGN: usize = 5;
45const PARM_ORDER: usize = 6;
46const PARM_FILAVAIL: usize = 7;
47const PARM_FILVAL: usize = 8;
48const TOTAL_NPARMS: usize = 20;
50const CORE_NPARMS: usize = PARM_FILVAL;
52
53const SO_FLOAT_DSCALE: u32 = 0;
55const SO_FLOAT_ESCALE: u32 = 1;
56const SO_INT: u32 = 2;
57
58const CLS_INTEGER: u32 = 0;
60const CLS_FLOAT: u32 = 1;
61
62const SGN_NONE: u32 = 0;
64const SGN_2: u32 = 1;
65
66const ORDER_LE: u32 = 0;
68const ORDER_BE: u32 = 1;
69
70const FILL_UNDEFINED: u32 = 0;
72const FILL_DEFINED: u32 = 1;
73
74pub(crate) const HEADER_LEN: usize = 21;
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum ScaleOffset {
85 Integer(u32),
89 FloatDScale(i32),
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct ScaleOffsetType {
103 class: u32,
105 sign: u32,
107 order: u32,
109}
110
111pub fn scale_offset_type_from_datatype(dt: &Datatype) -> Option<ScaleOffsetType> {
115 match dt {
116 Datatype::FixedPoint {
117 size,
118 byte_order,
119 signed,
120 ..
121 } if matches!(*size, 1 | 2 | 4 | 8) => Some(ScaleOffsetType {
122 class: CLS_INTEGER,
123 sign: if *signed { SGN_2 } else { SGN_NONE },
124 order: order_code(byte_order)?,
125 }),
126 Datatype::FloatingPoint {
127 size, byte_order, ..
128 } if matches!(*size, 4 | 8) => Some(ScaleOffsetType {
129 class: CLS_FLOAT,
130 sign: SGN_NONE,
131 order: order_code(byte_order)?,
132 }),
133 _ => None,
134 }
135}
136
137fn order_code(order: &DatatypeByteOrder) -> Option<u32> {
138 match order {
139 DatatypeByteOrder::LittleEndian => Some(ORDER_LE),
140 DatatypeByteOrder::BigEndian => Some(ORDER_BE),
141 DatatypeByteOrder::Vax => None,
144 }
145}
146
147pub fn build_cd_values(
153 mode: ScaleOffset,
154 ty: ScaleOffsetType,
155 size: u32,
156 nelmts: u32,
157) -> Result<Vec<u32>, FormatError> {
158 let (scale_type, scale_factor) = match (mode, ty.class) {
159 (ScaleOffset::Integer(minbits), CLS_INTEGER) => (SO_INT, minbits),
160 (ScaleOffset::FloatDScale(decimals), CLS_FLOAT) => (SO_FLOAT_DSCALE, decimals as u32),
161 (ScaleOffset::Integer(_), _) => {
162 return Err(FormatError::FilterError(
163 "scaleoffset: integer mode requires an integer dataset".to_string(),
164 ));
165 }
166 (ScaleOffset::FloatDScale(_), _) => {
167 return Err(FormatError::FilterError(
168 "scaleoffset: float D-scale mode requires a floating-point dataset".to_string(),
169 ));
170 }
171 };
172
173 let mut cd = vec![0u32; TOTAL_NPARMS];
174 cd[PARM_SCALETYPE] = scale_type;
175 cd[PARM_SCALEFACTOR] = scale_factor;
176 cd[PARM_NELMTS] = nelmts;
177 cd[PARM_CLASS] = ty.class;
178 cd[PARM_SIZE] = size;
179 cd[PARM_SIGN] = ty.sign;
180 cd[PARM_ORDER] = ty.order;
181 cd[PARM_FILAVAIL] = FILL_UNDEFINED;
182 Ok(cd)
183}
184
185pub(crate) fn scale_offset_mode(cd_values: &[u32]) -> Option<ScaleOffset> {
195 let scale_type = *cd_values.get(PARM_SCALETYPE)?;
196 let scale_factor = *cd_values.get(PARM_SCALEFACTOR)?;
197 if cd_values.get(PARM_FILAVAIL).copied() != Some(FILL_UNDEFINED) {
198 return None;
199 }
200 match scale_type {
201 SO_INT => Some(ScaleOffset::Integer(scale_factor)),
202 #[expect(
203 clippy::cast_possible_wrap,
204 reason = "scale_factor is a small decimal scale factor (D-scale parameter); the reference treats it as a signed int"
205 )]
206 SO_FLOAT_DSCALE => Some(ScaleOffset::FloatDScale(scale_factor as i32)),
207 _ => None,
210 }
211}
212
213struct Parms {
215 scale_type: u32,
216 scale_factor: i32,
217 nelmts: usize,
218 class: u32,
219 size: usize,
220 order: u32,
221 filavail: u32,
222}
223
224impl Parms {
225 fn parse(cd: &[u32]) -> Result<Parms, FormatError> {
226 if cd.len() < CORE_NPARMS {
227 return Err(FormatError::FilterError(
228 "scaleoffset: too few cd_values".to_string(),
229 ));
230 }
231 let class = cd[PARM_CLASS];
232 if class != CLS_INTEGER && class != CLS_FLOAT {
233 return Err(FormatError::FilterError(
234 "scaleoffset: unsupported datatype class".to_string(),
235 ));
236 }
237 let size = cd[PARM_SIZE] as usize;
238 if size == 0 || size > 8 {
239 return Err(FormatError::FilterError(
240 "scaleoffset: unsupported datatype size".to_string(),
241 ));
242 }
243 if class == CLS_FLOAT && size != 4 && size != 8 {
244 return Err(FormatError::FilterError(
245 "scaleoffset: float size must be 4 or 8".to_string(),
246 ));
247 }
248 let order = cd[PARM_ORDER];
249 if order != ORDER_LE && order != ORDER_BE {
250 return Err(FormatError::FilterError(
251 "scaleoffset: bad byte order".to_string(),
252 ));
253 }
254 #[expect(
255 clippy::cast_possible_wrap,
256 reason = "scale_factor is a small filter parameter (decimal scale factor or bit width); the reference stores it in a signed int"
257 )]
258 Ok(Parms {
259 scale_type: cd[PARM_SCALETYPE],
260 scale_factor: cd[PARM_SCALEFACTOR] as i32,
261 nelmts: cd[PARM_NELMTS].to_usize()?,
262 class,
263 size,
264 order,
265 filavail: cd[PARM_FILAVAIL],
266 })
267 }
268
269 fn width_mask(&self) -> u64 {
271 if self.size >= 8 {
272 u64::MAX
273 } else {
274 (1u64 << (self.size * 8)) - 1
275 }
276 }
277}
278
279pub fn decompress(input: &[u8], filter: &FilterDescription) -> Result<Vec<u8>, FormatError> {
282 let cd = &filter.client_data;
283 let p = Parms::parse(cd)?;
284
285 if p.scale_type == SO_FLOAT_ESCALE {
286 return Err(FormatError::FilterError(
287 "scaleoffset: float E-scale method is not supported".to_string(),
288 ));
289 }
290
291 #[expect(
292 clippy::cast_possible_truncation,
293 reason = "p.size is validated to 1..=8, so p.size * 8 is 8..=64 and fits in u32"
294 )]
295 let full_bits = (p.size * 8) as u32;
296 let size_out = (p.nelmts as u64 * p.size as u64).to_usize()?;
301
302 #[expect(
305 clippy::cast_possible_wrap,
306 reason = "full_bits is 8..=64 (p.size is 1..=8), so it fits in a non-negative i32"
307 )]
308 if p.scale_type != SO_FLOAT_DSCALE && p.scale_factor == full_bits as i32 {
309 return Ok(input.to_vec());
310 }
311
312 if input.len() < 5 {
314 return Err(FormatError::FilterError(
315 "scaleoffset: chunk shorter than header".to_string(),
316 ));
317 }
318 let minbits = u32::from_le_bytes([input[0], input[1], input[2], input[3]]);
319 if minbits > full_bits {
320 return Err(FormatError::FilterError(
321 "scaleoffset: minbits exceeds datatype size".to_string(),
322 ));
323 }
324 let minval_size = (input[4] as usize).min(8);
325 if input.len() < 5 + minval_size {
326 return Err(FormatError::FilterError(
327 "scaleoffset: chunk too short for minval".to_string(),
328 ));
329 }
330 let mut minval_bytes = [0u8; 8];
331 minval_bytes[..minval_size].copy_from_slice(&input[5..5 + minval_size]);
332 let minval = u64::from_le_bytes(minval_bytes);
333
334 if minbits == full_bits {
336 let start = HEADER_LEN;
337 if input.len() < start + size_out {
338 return Err(FormatError::FilterError(
339 "scaleoffset: chunk too short for raw payload".to_string(),
340 ));
341 }
342 return Ok(input[start..start + size_out].to_vec());
343 }
344
345 let mut out = Vec::with_capacity(size_out);
351 if minbits == 0 {
352 let mask = p.width_mask();
353 for _ in 0..p.nelmts {
354 write_value(&mut out, minval & mask, p.size, p.order);
355 }
356 return Ok(out);
357 }
358
359 if input.len() < HEADER_LEN {
360 return Err(FormatError::FilterError(
361 "scaleoffset: chunk too short for packed payload".to_string(),
362 ));
363 }
364 let payload = &input[HEADER_LEN..];
365 if (payload.len() as u64) * 8 < p.nelmts as u64 * minbits as u64 {
370 return Err(FormatError::FilterError(
371 "scaleoffset: payload too short for packed data".to_string(),
372 ));
373 }
374 let mut reader = BitReader::new(payload);
375
376 if p.class == CLS_INTEGER {
377 reconstruct_integer(&mut out, &mut reader, &p, minbits, minval, cd)?;
378 } else {
379 reconstruct_float(&mut out, &mut reader, &p, minbits, minval, cd)?;
380 }
381 Ok(out)
382}
383
384pub fn compress(input: &[u8], filter: &FilterDescription) -> Result<Vec<u8>, FormatError> {
390 let cd = &filter.client_data;
391 let p = Parms::parse(cd)?;
392
393 if p.filavail == FILL_DEFINED {
394 return Err(FormatError::FilterError(
395 "scaleoffset: encoding with a defined fill value is not supported".to_string(),
396 ));
397 }
398 if p.class == CLS_INTEGER && p.scale_type != SO_INT {
399 return Err(FormatError::FilterError(
400 "scaleoffset: integer class requires integer scale type".to_string(),
401 ));
402 }
403 if p.class == CLS_FLOAT && p.scale_type != SO_FLOAT_DSCALE {
404 return Err(FormatError::FilterError(
405 "scaleoffset: float class requires D-scale scale type".to_string(),
406 ));
407 }
408
409 let expected = (p.nelmts as u64 * p.size as u64).to_usize()?;
413 if input.len() != expected {
414 return Err(FormatError::CompressionError(
415 "scaleoffset: chunk size does not match nelmts * datatype size".to_string(),
416 ));
417 }
418 if p.nelmts == 0 {
419 return Ok(emit(0, 0, &[]));
420 }
421
422 let signed = cd[PARM_SIGN] == SGN_2;
423 #[expect(
424 clippy::cast_possible_truncation,
425 reason = "p.size is validated to 1..=8, so p.size * 8 is 8..=64 and fits in u32"
426 )]
427 let full_bits = (p.size * 8) as u32;
428
429 let (minbits, minval, offsets) = if p.class == CLS_INTEGER {
430 precompress_integer(input, &p, signed)
431 } else {
432 precompress_float(input, &p)
433 };
434
435 if minbits >= full_bits {
437 return Ok(emit_raw(full_bits, minval, input));
438 }
439 let payload = pack_offsets(&offsets, minbits, p.nelmts)?;
440 Ok(emit(minbits, minval, &payload))
441}
442
443fn reconstruct_integer(
446 out: &mut Vec<u8>,
447 reader: &mut BitReader<'_>,
448 p: &Parms,
449 minbits: u32,
450 minval: u64,
451 cd: &[u32],
452) -> Result<(), FormatError> {
453 let mask = p.width_mask();
454 let sentinel = sentinel(minbits);
455 let filval = if p.filavail == FILL_DEFINED {
457 Some(read_fill_bits(cd, p.size)? & mask)
458 } else {
459 None
460 };
461 for _ in 0..p.nelmts {
462 let d = reader.read(minbits);
463 let bits = match filval {
464 Some(fv) if d == sentinel => fv,
465 _ => d.wrapping_add(minval) & mask,
466 };
467 write_value(out, bits, p.size, p.order);
468 }
469 Ok(())
470}
471
472fn precompress_integer(input: &[u8], p: &Parms, signed: bool) -> (u32, u64, Vec<u64>) {
473 #[expect(
478 clippy::cast_possible_truncation,
479 reason = "p.size is validated to 1..=8, so p.size * 8 is 8..=64 and fits in u32"
480 )]
481 let full_bits = (p.size * 8) as u32;
482 let read_as_i128 = |i: usize| -> i128 {
483 let bits = read_value(&input[i * p.size..(i + 1) * p.size], p.size, p.order);
484 if signed {
485 sign_extend(bits, p.size) as i128
486 } else {
487 bits as i128
488 }
489 };
490
491 let first = read_as_i128(0);
492 let (mut min, mut max) = (first, first);
493 for i in 1..p.nelmts {
494 let v = read_as_i128(i);
495 if v < min {
496 min = v;
497 }
498 if v > max {
499 max = v;
500 }
501 }
502
503 #[expect(
504 clippy::cast_possible_truncation,
505 reason = "min is an element value read from at most 8 bytes (p.size 1..=8), so it fits in i64/u64 despite the i128 accumulator type"
506 )]
507 let minval = if signed {
508 (min as i64) as u64
509 } else {
510 min as u64
511 };
512
513 let width_max: u128 = p.width_mask() as u128;
516 let spread = (max - min) as u128;
517 if spread > width_max.saturating_sub(2) {
518 return (full_bits, minval, Vec::new());
519 }
520
521 #[expect(
522 clippy::cast_possible_truncation,
523 reason = "the guard above returns early unless spread <= width_max - 2, and width_max <= u64::MAX, so spread fits in u64"
524 )]
525 let minbits = ceil_log2((spread as u64) + 1);
526 if minbits >= full_bits {
527 return (full_bits, minval, Vec::new());
528 }
529
530 #[expect(
531 clippy::cast_possible_truncation,
532 reason = "each offset (value - min) is at most spread <= width_max <= u64::MAX, so it fits in u64"
533 )]
534 let offsets = (0..p.nelmts)
535 .map(|i| (read_as_i128(i) - min) as u64)
536 .collect();
537 (minbits, minval, offsets)
538}
539
540fn reconstruct_float(
543 out: &mut Vec<u8>,
544 reader: &mut BitReader<'_>,
545 p: &Parms,
546 minbits: u32,
547 minval: u64,
548 cd: &[u32],
549) -> Result<(), FormatError> {
550 let sentinel = sentinel(minbits);
551 let decimals = p.scale_factor;
552 let filval = if p.filavail == FILL_DEFINED {
553 Some(read_fill_bits(cd, p.size)?)
554 } else {
555 None
556 };
557 if p.size == 4 {
561 #[expect(
562 clippy::cast_possible_truncation,
563 reason = "size == 4 branch: minval holds a 4-byte f32 bit pattern in its low 32 bits, so narrowing to u32 reconstructs that pattern"
564 )]
565 let min = f32::from_bits(minval as u32);
566 let pow = pow10_f32(decimals);
567 for _ in 0..p.nelmts {
568 let d = reader.read(minbits);
569 #[expect(
570 clippy::cast_possible_wrap,
571 reason = "d is a minbits-wide offset (minbits < full_bits <= 64); reinterpreting it as i64 matches the reference's signed reconstruction"
572 )]
573 let bits = if let Some(fv) = filval.filter(|_| d == sentinel) {
574 fv
575 } else {
576 ((d as i64 as f32) / pow + min).to_bits() as u64
577 };
578 write_value(out, bits, 4, p.order);
579 }
580 } else {
581 let min = f64::from_bits(minval);
582 let pow = pow10_f64(decimals);
583 for _ in 0..p.nelmts {
584 let d = reader.read(minbits);
585 #[expect(
586 clippy::cast_possible_wrap,
587 reason = "d is a minbits-wide offset (minbits < full_bits <= 64); reinterpreting it as i64 matches the reference's signed reconstruction"
588 )]
589 let bits = if let Some(fv) = filval.filter(|_| d == sentinel) {
590 fv
591 } else {
592 ((d as i64 as f64) / pow + min).to_bits()
593 };
594 write_value(out, bits, 8, p.order);
595 }
596 }
597 Ok(())
598}
599
600fn precompress_float(input: &[u8], p: &Parms) -> (u32, u64, Vec<u64>) {
601 let decimals = p.scale_factor;
606 #[expect(
607 clippy::cast_possible_truncation,
608 reason = "p.size is validated to 1..=8, so p.size * 8 is 8..=64 and fits in u32"
609 )]
610 let full_bits = (p.size * 8) as u32;
611 if p.size == 4 {
612 #[expect(
613 clippy::cast_possible_truncation,
614 reason = "read_value with size 4 returns a u64 whose meaningful bits are the low 32, so narrowing to u32 reconstructs the f32 bit pattern"
615 )]
616 let read_f32 = |i: usize| -> f32 {
617 f32::from_bits(read_value(&input[i * 4..i * 4 + 4], 4, p.order) as u32)
618 };
619 let first = read_f32(0);
620 let (mut min, mut max) = (first, first);
621 for i in 1..p.nelmts {
622 let v = read_f32(i);
623 if v < min {
624 min = v;
625 }
626 if v > max {
627 max = v;
628 }
629 }
630 let pow = pow10_f32(decimals);
631 let min_scaled = min * pow;
632 let residual = max * pow - min_scaled;
634 let minval = min.to_bits() as u64;
635 if residual > (1u64 << 31) as f32 {
636 return (full_bits, minval, Vec::new());
637 }
638 let minbits = ceil_log2((round_half_away_f32(residual) as u64) + 1);
639 if minbits >= full_bits {
640 return (full_bits, minval, Vec::new());
641 }
642 let offsets = (0..p.nelmts)
643 .map(|i| round_half_away_f32(read_f32(i) * pow - min_scaled) as u64)
644 .collect();
645 (minbits, minval, offsets)
646 } else {
647 let read_f64 =
648 |i: usize| -> f64 { f64::from_bits(read_value(&input[i * 8..i * 8 + 8], 8, p.order)) };
649 let first = read_f64(0);
650 let (mut min, mut max) = (first, first);
651 for i in 1..p.nelmts {
652 let v = read_f64(i);
653 if v < min {
654 min = v;
655 }
656 if v > max {
657 max = v;
658 }
659 }
660 let pow = pow10_f64(decimals);
661 let min_scaled = min * pow;
662 let residual = max * pow - min_scaled;
663 let minval = min.to_bits();
664 if residual > (1u64 << 63) as f64 {
665 return (full_bits, minval, Vec::new());
666 }
667 let minbits = ceil_log2((round_half_away_f64(residual) as u64) + 1);
668 if minbits >= full_bits {
669 return (full_bits, minval, Vec::new());
670 }
671 let offsets = (0..p.nelmts)
672 .map(|i| round_half_away_f64(read_f64(i) * pow - min_scaled) as u64)
673 .collect();
674 (minbits, minval, offsets)
675 }
676}
677
678fn emit(minbits: u32, minval: u64, payload: &[u8]) -> Vec<u8> {
683 let mut out = Vec::with_capacity(HEADER_LEN + payload.len().max(1));
684 write_header(&mut out, minbits, minval);
685 if payload.is_empty() {
686 out.push(0);
688 } else {
689 out.extend_from_slice(payload);
690 }
691 out
692}
693
694fn emit_raw(full_bits: u32, minval: u64, input: &[u8]) -> Vec<u8> {
696 let mut out = Vec::with_capacity(HEADER_LEN + input.len());
697 write_header(&mut out, full_bits, minval);
698 out.extend_from_slice(input);
699 out
700}
701
702fn write_header(out: &mut Vec<u8>, minbits: u32, minval: u64) {
703 out.extend_from_slice(&minbits.to_le_bytes()); out.push(8); out.extend_from_slice(&minval.to_le_bytes()); out.extend_from_slice(&[0u8; HEADER_LEN - 13]); }
708
709fn pack_offsets(offsets: &[u64], minbits: u32, nelmts: usize) -> Result<Vec<u8>, FormatError> {
723 let payload_len = (nelmts as u64 * minbits as u64 / 8 + 1).to_usize()?;
728 if minbits == 0 {
729 return Ok(vec![0u8; payload_len]);
732 }
733 let mut buf = Vec::with_capacity(payload_len);
734 let mask: u64 = if minbits == 64 {
735 u64::MAX
736 } else {
737 (1u64 << minbits) - 1
738 };
739 let mut acc: u64 = 0;
740 let mut nbits: u32 = 0;
741 for &v in offsets {
742 while nbits >= 8 {
744 nbits -= 8;
745 buf.push(((acc >> nbits) & 0xFF) as u8);
746 }
747 let combined = ((acc as u128) << minbits) | (v & mask) as u128;
750 let total = nbits + minbits;
751 if total <= 64 {
752 #[expect(
753 clippy::cast_possible_truncation,
754 reason = "this branch is guarded by total <= 64, and combined occupies exactly `total` bits, so it fits in u64"
755 )]
756 {
757 acc = combined as u64;
758 }
759 nbits = total;
760 } else {
761 let drop = total - 64;
764 #[expect(
765 clippy::cast_possible_truncation,
766 reason = "combined >> drop keeps the high 64 bits of a (64 + drop)-bit value, which fits in u64"
767 )]
768 let top = (combined >> drop) as u64;
769 buf.extend_from_slice(&top.to_be_bytes());
770 #[expect(
771 clippy::cast_possible_truncation,
772 reason = "combined & ((1 << drop) - 1) masks to the low `drop` bits (drop is 1..=7 here), which fits in u64"
773 )]
774 {
775 acc = (combined & ((1u128 << drop) - 1)) as u64;
776 }
777 nbits = drop;
778 }
779 }
780 while nbits >= 8 {
781 nbits -= 8;
782 buf.push(((acc >> nbits) & 0xFF) as u8);
783 }
784 if nbits > 0 {
785 buf.push(((acc << (8 - nbits)) & 0xFF) as u8);
788 }
789 while buf.len() < payload_len {
793 buf.push(0);
794 }
795 Ok(buf)
796}
797
798struct BitReader<'a> {
802 payload: &'a [u8],
803 bit_pos: usize,
804}
805
806impl<'a> BitReader<'a> {
807 fn new(payload: &'a [u8]) -> Self {
808 Self {
809 payload,
810 bit_pos: 0,
811 }
812 }
813
814 #[inline]
817 fn read(&mut self, minbits: u32) -> u64 {
818 debug_assert!((1..=64).contains(&minbits));
819 let byte = self.bit_pos >> 3;
820 #[expect(
821 clippy::cast_possible_truncation,
822 reason = "bit_pos & 7 masks to 0..=7, which trivially fits in u32"
823 )]
824 let off = (self.bit_pos & 7) as u32;
825
826 let (hi, lo) = if byte + 9 <= self.payload.len() {
829 let hi = u64::from_be_bytes(self.payload[byte..byte + 8].try_into().unwrap());
830 let lo = self.payload[byte + 8] as u64;
831 (hi, lo)
832 } else {
833 let mut window = [0u8; 9];
834 let take = self.payload.len().saturating_sub(byte).min(9);
835 window[..take].copy_from_slice(&self.payload[byte..byte + take]);
836 let hi = u64::from_be_bytes(window[..8].try_into().unwrap());
837 let lo = window[8] as u64;
838 (hi, lo)
839 };
840
841 let combined = if off == 0 {
845 hi
846 } else {
847 (hi << off) | (lo >> (8 - off))
848 };
849 self.bit_pos += minbits as usize;
850 combined >> (64 - minbits)
851 }
852}
853
854#[cfg(test)]
857fn unpack_bits(payload: &[u8], nelmts: usize, minbits: u32) -> Result<Vec<u64>, FormatError> {
858 let total_bits = nelmts * minbits as usize;
859 if payload.len() * 8 < total_bits {
860 return Err(FormatError::FilterError(
861 "scaleoffset: payload too short for packed data".to_string(),
862 ));
863 }
864 let mut reader = BitReader::new(payload);
865 Ok((0..nelmts).map(|_| reader.read(minbits)).collect())
866}
867
868fn read_value(chunk: &[u8], size: usize, order: u32) -> u64 {
873 let mut bytes = [0u8; 8];
874 if order == ORDER_LE {
875 bytes[..size].copy_from_slice(&chunk[..size]);
876 } else {
877 for (k, &b) in chunk[..size].iter().enumerate() {
878 bytes[size - 1 - k] = b;
879 }
880 }
881 u64::from_le_bytes(bytes)
882}
883
884fn write_value(out: &mut Vec<u8>, bits: u64, size: usize, order: u32) {
886 let le = bits.to_le_bytes();
887 if order == ORDER_LE {
888 out.extend_from_slice(&le[..size]);
889 } else {
890 for k in (0..size).rev() {
891 out.push(le[k]);
892 }
893 }
894}
895
896#[expect(
897 clippy::cast_possible_wrap,
898 reason = "size >= 8: reinterpreting all 64 stored bits as i64 is the intentional sign reinterpretation of a full-width value; size < 8: ((bits << shift) as i64) >> shift sign-extends the `size`-byte value, an intentional wrap"
899)]
900fn sign_extend(bits: u64, size: usize) -> i64 {
901 if size >= 8 {
902 bits as i64
903 } else {
904 let shift = 64 - size * 8;
905 ((bits << shift) as i64) >> shift
906 }
907}
908
909fn sentinel(minbits: u32) -> u64 {
911 (1u64 << minbits).wrapping_sub(1)
912}
913
914fn read_fill_bits(cd: &[u32], size: usize) -> Result<u64, FormatError> {
917 let entries = size.div_ceil(4);
918 if cd.len() < PARM_FILVAL + entries {
919 return Err(FormatError::FilterError(
920 "scaleoffset: cd_values missing fill value".to_string(),
921 ));
922 }
923 let mut bytes = [0u8; 8];
924 let mut off = 0;
925 let mut idx = PARM_FILVAL;
926 while off < size {
927 let take = (size - off).min(4);
928 bytes[off..off + take].copy_from_slice(&cd[idx].to_le_bytes()[..take]);
929 off += take;
930 idx += 1;
931 }
932 Ok(u64::from_le_bytes(bytes))
933}
934
935fn pow10_f64(exp: i32) -> f64 {
939 let mut result = 1.0f64;
940 let mut base = 10.0f64;
941 let mut n = exp.unsigned_abs();
942 while n > 0 {
943 if n & 1 == 1 {
944 result *= base;
945 }
946 base *= base;
947 n >>= 1;
948 }
949 if exp < 0 { 1.0 / result } else { result }
950}
951
952#[expect(
954 clippy::cast_possible_truncation,
955 reason = "narrowing 10^exp from f64 to f32 is the intended value conversion; out-of-range magnitudes saturate to +/-inf, matching the C reference"
956)]
957fn pow10_f32(exp: i32) -> f32 {
958 pow10_f64(exp) as f32
959}
960
961#[expect(
964 clippy::cast_possible_truncation,
965 reason = "float-to-int `as` saturates in Rust, so rounding x +/- 0.5 to i64 is safe even for out-of-range inputs (matches C llround)"
966)]
967fn round_half_away_f64(x: f64) -> i64 {
968 if x >= 0.0 {
969 (x + 0.5) as i64
970 } else {
971 (x - 0.5) as i64
972 }
973}
974
975#[expect(
977 clippy::cast_possible_truncation,
978 reason = "float-to-int `as` saturates in Rust, so rounding x +/- 0.5 to i64 is safe even for out-of-range inputs (matches C lroundf)"
979)]
980fn round_half_away_f32(x: f32) -> i64 {
981 if x >= 0.0 {
982 (x + 0.5) as i64
983 } else {
984 (x - 0.5) as i64
985 }
986}
987
988fn ceil_log2(num: u64) -> u32 {
991 let mut v = 0u32;
992 let mut lower_bound = 1u64;
993 let mut val = num;
994 loop {
995 val >>= 1;
996 if val == 0 {
997 break;
998 }
999 v += 1;
1000 lower_bound <<= 1;
1001 }
1002 if num == lower_bound { v } else { v + 1 }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008
1009 fn int_filter(size: u32, signed: bool, order: u32, nelmts: u32) -> FilterDescription {
1010 let ty = ScaleOffsetType {
1011 class: CLS_INTEGER,
1012 sign: if signed { SGN_2 } else { SGN_NONE },
1013 order,
1014 };
1015 FilterDescription {
1016 filter_id: crate::filter_pipeline::FILTER_SCALEOFFSET,
1017 name: None,
1018 flags: 0,
1019 client_data: build_cd_values(ScaleOffset::Integer(0), ty, size, nelmts).unwrap(),
1020 }
1021 }
1022
1023 fn float_filter(size: u32, decimals: i32, order: u32, nelmts: u32) -> FilterDescription {
1024 let ty = ScaleOffsetType {
1025 class: CLS_FLOAT,
1026 sign: SGN_NONE,
1027 order,
1028 };
1029 FilterDescription {
1030 filter_id: crate::filter_pipeline::FILTER_SCALEOFFSET,
1031 name: None,
1032 flags: 0,
1033 client_data: build_cd_values(ScaleOffset::FloatDScale(decimals), ty, size, nelmts)
1034 .unwrap(),
1035 }
1036 }
1037
1038 #[test]
1039 fn scale_offset_mode_recovers_and_refuses() {
1040 let f = int_filter(4, true, ORDER_LE, 16);
1042 assert_eq!(
1043 scale_offset_mode(&f.client_data),
1044 Some(ScaleOffset::Integer(0))
1045 );
1046
1047 let f = float_filter(8, 3, ORDER_LE, 16);
1049 assert_eq!(
1050 scale_offset_mode(&f.client_data),
1051 Some(ScaleOffset::FloatDScale(3))
1052 );
1053
1054 let mut fill_defined = int_filter(4, true, ORDER_LE, 16);
1056 fill_defined.client_data[PARM_FILAVAIL] = FILL_DEFINED;
1057 assert_eq!(scale_offset_mode(&fill_defined.client_data), None);
1058
1059 assert_eq!(scale_offset_mode(&[]), None);
1061 assert_eq!(scale_offset_mode(&[SO_INT, 0]), None);
1062 }
1063
1064 #[test]
1065 fn ceil_log2_matches_reference() {
1066 assert_eq!(ceil_log2(0), 1);
1067 assert_eq!(ceil_log2(1), 0);
1068 assert_eq!(ceil_log2(2), 1);
1069 assert_eq!(ceil_log2(3), 2);
1070 assert_eq!(ceil_log2(4), 2);
1071 assert_eq!(ceil_log2(5), 3);
1072 assert_eq!(ceil_log2(255), 8);
1073 assert_eq!(ceil_log2(256), 8);
1074 assert_eq!(ceil_log2(257), 9);
1075 }
1076
1077 fn roundtrip_u32(vals: &[u32], order: u32) {
1078 let mut raw = Vec::new();
1079 for &v in vals {
1080 if order == ORDER_LE {
1081 raw.extend_from_slice(&v.to_le_bytes());
1082 } else {
1083 raw.extend_from_slice(&v.to_be_bytes());
1084 }
1085 }
1086 let f = int_filter(4, false, order, vals.len() as u32);
1087 let comp = compress(&raw, &f).unwrap();
1088 let dec = decompress(&comp, &f).unwrap();
1089 assert_eq!(dec, raw);
1090 }
1091
1092 #[test]
1093 fn integer_unsigned_roundtrip_le_and_be() {
1094 let vals = [100u32, 105, 101, 110, 100, 128];
1095 roundtrip_u32(&vals, ORDER_LE);
1096 roundtrip_u32(&vals, ORDER_BE);
1097 }
1098
1099 #[test]
1100 fn integer_signed_roundtrip_with_negatives() {
1101 let vals: [i16; 6] = [-100, -50, -100, 0, 27, -99];
1102 for &order in &[ORDER_LE, ORDER_BE] {
1103 let mut raw = Vec::new();
1104 for &v in &vals {
1105 if order == ORDER_LE {
1106 raw.extend_from_slice(&v.to_le_bytes());
1107 } else {
1108 raw.extend_from_slice(&v.to_be_bytes());
1109 }
1110 }
1111 let f = int_filter(2, true, order, vals.len() as u32);
1112 let comp = compress(&raw, &f).unwrap();
1113 let dec = decompress(&comp, &f).unwrap();
1114 assert_eq!(dec, raw, "order {order}");
1115 }
1116 }
1117
1118 #[test]
1119 fn integer_all_equal_uses_minbits_zero() {
1120 let vals = [7u32; 5];
1121 let mut raw = Vec::new();
1122 for &v in &vals {
1123 raw.extend_from_slice(&v.to_le_bytes());
1124 }
1125 let f = int_filter(4, false, ORDER_LE, vals.len() as u32);
1126 let comp = compress(&raw, &f).unwrap();
1127 assert_eq!(comp.len(), HEADER_LEN + 1);
1129 assert_eq!(u32::from_le_bytes([comp[0], comp[1], comp[2], comp[3]]), 0);
1130 let dec = decompress(&comp, &f).unwrap();
1131 assert_eq!(dec, raw);
1132 }
1133
1134 #[test]
1135 fn integer_full_range_uses_raw_path() {
1136 let vals = [0u32, u32::MAX, 123];
1138 let mut raw = Vec::new();
1139 for &v in &vals {
1140 raw.extend_from_slice(&v.to_le_bytes());
1141 }
1142 let f = int_filter(4, false, ORDER_LE, vals.len() as u32);
1143 let comp = compress(&raw, &f).unwrap();
1144 assert_eq!(comp.len(), HEADER_LEN + raw.len());
1145 assert_eq!(u32::from_le_bytes([comp[0], comp[1], comp[2], comp[3]]), 32);
1146 let dec = decompress(&comp, &f).unwrap();
1147 assert_eq!(dec, raw);
1148 }
1149
1150 #[test]
1151 fn integer_u8_roundtrip() {
1152 let raw = vec![10u8, 11, 12, 250, 10, 200];
1153 let f = int_filter(1, false, ORDER_LE, raw.len() as u32);
1154 let comp = compress(&raw, &f).unwrap();
1155 let dec = decompress(&comp, &f).unwrap();
1156 assert_eq!(dec, raw);
1157 }
1158
1159 #[test]
1160 fn integer_i64_roundtrip() {
1161 let vals: [i64; 4] = [-1_000_000, 5, -999_999, 42];
1162 let mut raw = Vec::new();
1163 for &v in &vals {
1164 raw.extend_from_slice(&v.to_le_bytes());
1165 }
1166 let f = int_filter(8, true, ORDER_LE, vals.len() as u32);
1167 let comp = compress(&raw, &f).unwrap();
1168 let dec = decompress(&comp, &f).unwrap();
1169 assert_eq!(dec, raw);
1170 }
1171
1172 #[test]
1173 fn float_dscale_roundtrip_within_tolerance() {
1174 let vals = [1.234f64, 1.235, 1.250, 1.111, 1.234, 1.999];
1175 let decimals = 3;
1176 let mut raw = Vec::new();
1177 for &v in &vals {
1178 raw.extend_from_slice(&v.to_le_bytes());
1179 }
1180 let f = float_filter(8, decimals, ORDER_LE, vals.len() as u32);
1181 let comp = compress(&raw, &f).unwrap();
1182 let dec = decompress(&comp, &f).unwrap();
1183 let got: Vec<f64> = dec
1184 .chunks_exact(8)
1185 .map(|c| f64::from_le_bytes(c.try_into().unwrap()))
1186 .collect();
1187 let tol = 0.5 * 10f64.powi(-decimals);
1188 for (g, v) in got.iter().zip(vals.iter()) {
1189 assert!((g - v).abs() <= tol, "got {g}, want {v}");
1190 }
1191 }
1192
1193 #[test]
1194 fn float32_dscale_roundtrip_be() {
1195 let vals = [10.25f32, 10.50, 10.75, 10.00, 10.25];
1196 let decimals = 2;
1197 let mut raw = Vec::new();
1198 for &v in &vals {
1199 raw.extend_from_slice(&v.to_be_bytes());
1200 }
1201 let f = float_filter(4, decimals, ORDER_BE, vals.len() as u32);
1202 let comp = compress(&raw, &f).unwrap();
1203 let dec = decompress(&comp, &f).unwrap();
1204 let got: Vec<f32> = dec
1205 .chunks_exact(4)
1206 .map(|c| f32::from_be_bytes(c.try_into().unwrap()))
1207 .collect();
1208 let tol = 0.5 * 10f32.powi(-decimals);
1209 for (g, v) in got.iter().zip(vals.iter()) {
1210 assert!((g - v).abs() <= tol, "got {g}, want {v}");
1211 }
1212 }
1213
1214 #[test]
1215 fn truncated_chunk_errors_not_panics() {
1216 let f = int_filter(4, false, ORDER_LE, 4);
1219 let mut bad = Vec::new();
1220 bad.extend_from_slice(&3u32.to_le_bytes()); bad.push(8); bad.extend_from_slice(&0u64.to_le_bytes()); assert!(matches!(
1225 decompress(&bad, &f),
1226 Err(FormatError::FilterError(_))
1227 ));
1228 }
1229
1230 #[test]
1231 fn header_byte_layout() {
1232 let vals = [5u32, 9, 6, 5];
1234 let mut raw = Vec::new();
1235 for &v in &vals {
1236 raw.extend_from_slice(&v.to_le_bytes());
1237 }
1238 let f = int_filter(4, false, ORDER_LE, vals.len() as u32);
1239 let comp = compress(&raw, &f).unwrap();
1240 assert_eq!(u32::from_le_bytes([comp[0], comp[1], comp[2], comp[3]]), 3);
1241 assert_eq!(comp[4], 8); let minval = u64::from_le_bytes(comp[5..13].try_into().unwrap());
1243 assert_eq!(minval, 5);
1244 assert_eq!(&comp[13..21], &[0u8; 8]); }
1246
1247 #[test]
1248 fn build_cd_values_rejects_mismatched_mode() {
1249 let int_ty = ScaleOffsetType {
1250 class: CLS_INTEGER,
1251 sign: SGN_2,
1252 order: ORDER_LE,
1253 };
1254 assert!(build_cd_values(ScaleOffset::FloatDScale(2), int_ty, 4, 10).is_err());
1255 let float_ty = ScaleOffsetType {
1256 class: CLS_FLOAT,
1257 sign: SGN_NONE,
1258 order: ORDER_LE,
1259 };
1260 assert!(build_cd_values(ScaleOffset::Integer(0), float_ty, 8, 10).is_err());
1261 }
1262
1263 struct Rng(u64);
1267 impl Rng {
1268 fn new(seed: u64) -> Self {
1269 Self(seed | 1)
1270 }
1271 fn next(&mut self) -> u64 {
1272 let mut x = self.0;
1273 x ^= x << 13;
1274 x ^= x >> 7;
1275 x ^= x << 17;
1276 self.0 = x;
1277 x
1278 }
1279 fn range(&mut self, hi: u64) -> u64 {
1280 self.next() % hi
1282 }
1283 }
1284
1285 #[test]
1286 fn pack_unpack_equivalence_random() {
1287 let mut rng = Rng::new(0x00C0_FFEE_F00D_1234);
1290 for _ in 0..400 {
1291 let minbits = (rng.range(64) + 1) as u32; let nelmts = (rng.range(257) + 1) as usize; let mask = if minbits == 64 {
1294 u64::MAX
1295 } else {
1296 (1u64 << minbits) - 1
1297 };
1298 let offsets: Vec<u64> = (0..nelmts).map(|_| rng.next() & mask).collect();
1299 let packed = pack_offsets(&offsets, minbits, nelmts).unwrap();
1300 assert_eq!(packed.len(), nelmts * minbits as usize / 8 + 1);
1302 let unpacked = unpack_bits(&packed, nelmts, minbits).unwrap();
1303 assert_eq!(
1304 unpacked, offsets,
1305 "minbits={minbits}, nelmts={nelmts}, seed=0xC0FFEEF00D1234"
1306 );
1307 }
1308 }
1309
1310 fn roundtrip_random<T: Copy>(
1311 seed: u64,
1312 size: u32,
1313 signed: bool,
1314 order: u32,
1315 encode: impl Fn(&mut Rng) -> T,
1316 to_bytes: impl Fn(T, u32) -> Vec<u8>,
1317 ) {
1318 let mut rng = Rng::new(seed);
1319 for trial in 0..40 {
1320 let nelmts = (rng.range(199) + 1) as usize;
1321 let mut raw = Vec::with_capacity(nelmts * size as usize);
1322 for _ in 0..nelmts {
1323 raw.extend_from_slice(&to_bytes(encode(&mut rng), order));
1324 }
1325 let f = int_filter(size, signed, order, nelmts as u32);
1326 let comp = compress(&raw, &f).unwrap();
1327 let dec = decompress(&comp, &f).unwrap();
1328 assert_eq!(
1329 dec, raw,
1330 "trial {trial}: size={size}, signed={signed}, order={order}"
1331 );
1332 }
1333 }
1334
1335 #[test]
1336 fn integer_roundtrip_random_u8() {
1337 roundtrip_random::<u8>(0x11, 1, false, ORDER_LE, |r| r.next() as u8, |v, _| vec![v]);
1338 }
1339
1340 #[test]
1341 fn integer_roundtrip_random_u16_le_and_be() {
1342 for &order in &[ORDER_LE, ORDER_BE] {
1343 roundtrip_random::<u16>(
1344 0x22 ^ order as u64,
1345 2,
1346 false,
1347 order,
1348 |r| r.next() as u16,
1349 |v, o| {
1350 if o == ORDER_LE {
1351 v.to_le_bytes().to_vec()
1352 } else {
1353 v.to_be_bytes().to_vec()
1354 }
1355 },
1356 );
1357 }
1358 }
1359
1360 #[test]
1361 fn integer_roundtrip_random_i32_narrow_and_wide() {
1362 let mut rng = Rng::new(0x33);
1364 for trial in 0..40 {
1365 let nelmts = (rng.range(199) + 1) as usize;
1366 let narrow = rng.next() & 1 == 0;
1367 let mut raw = Vec::with_capacity(nelmts * 4);
1368 for _ in 0..nelmts {
1369 let v: i32 = if narrow {
1370 (rng.range(1024) as i32) - 512 } else {
1372 rng.next() as i32 };
1374 raw.extend_from_slice(&v.to_le_bytes());
1375 }
1376 let f = int_filter(4, true, ORDER_LE, nelmts as u32);
1377 let comp = compress(&raw, &f).unwrap();
1378 let dec = decompress(&comp, &f).unwrap();
1379 assert_eq!(dec, raw, "trial {trial}: narrow={narrow}");
1380 }
1381 }
1382
1383 #[test]
1384 fn integer_roundtrip_random_i64() {
1385 roundtrip_random::<i64>(
1386 0x44,
1387 8,
1388 true,
1389 ORDER_LE,
1390 |r| {
1391 let span = 1u64 << (r.range(48) as u32 + 1);
1394 let v = (r.next() % span) as i64;
1395 let off = (r.next() as i64) >> 1;
1396 v.wrapping_add(off)
1397 },
1398 |v, _| v.to_le_bytes().to_vec(),
1399 );
1400 }
1401
1402 #[test]
1403 fn float_dscale_roundtrip_random_within_tolerance() {
1404 let mut rng = Rng::new(0x55);
1405 for trial in 0..30 {
1406 let nelmts = (rng.range(99) + 1) as usize;
1407 let decimals = (rng.range(5) as i32) + 1; let mut raw = Vec::with_capacity(nelmts * 8);
1413 let mut vals = Vec::with_capacity(nelmts);
1414 for _ in 0..nelmts {
1415 let v = (rng.next() as i32 as f64) * 1e-9; vals.push(v);
1417 raw.extend_from_slice(&v.to_le_bytes());
1418 }
1419 let f = float_filter(8, decimals, ORDER_LE, nelmts as u32);
1420 let comp = compress(&raw, &f).unwrap();
1421 let dec = decompress(&comp, &f).unwrap();
1422 let got: Vec<f64> = dec
1423 .chunks_exact(8)
1424 .map(|c| f64::from_le_bytes(c.try_into().unwrap()))
1425 .collect();
1426 let tol = 0.501 * 10f64.powi(-decimals);
1428 for (g, v) in got.iter().zip(vals.iter()) {
1429 assert!(
1430 (g - v).abs() <= tol,
1431 "trial {trial}: got {g}, want {v} (tol {tol})"
1432 );
1433 }
1434 }
1435 }
1436
1437 #[test]
1445 fn minbits_zero_with_fill_defined_emits_minval_not_filval() {
1446 let nelmts = 5u32;
1447 let size = 4u32;
1448 let mut cd = vec![0u32; TOTAL_NPARMS];
1449 cd[PARM_SCALETYPE] = SO_INT;
1450 cd[PARM_SCALEFACTOR] = 0;
1451 cd[PARM_NELMTS] = nelmts;
1452 cd[PARM_CLASS] = CLS_INTEGER;
1453 cd[PARM_SIZE] = size;
1454 cd[PARM_SIGN] = SGN_NONE;
1455 cd[PARM_ORDER] = ORDER_LE;
1456 cd[PARM_FILAVAIL] = FILL_DEFINED;
1457 cd[PARM_FILVAL] = 99;
1458 let f = FilterDescription {
1459 filter_id: crate::filter_pipeline::FILTER_SCALEOFFSET,
1460 name: None,
1461 flags: 0,
1462 client_data: cd,
1463 };
1464 let mut chunk = Vec::with_capacity(HEADER_LEN + 1);
1466 chunk.extend_from_slice(&0u32.to_le_bytes());
1467 chunk.push(8);
1468 chunk.extend_from_slice(&7u64.to_le_bytes());
1469 chunk.extend_from_slice(&[0u8; HEADER_LEN - 13]);
1470 chunk.push(0);
1471 let out = decompress(&chunk, &f).unwrap();
1472 let got: Vec<u32> = out
1473 .chunks_exact(4)
1474 .map(|c| u32::from_le_bytes(c.try_into().unwrap()))
1475 .collect();
1476 assert_eq!(
1477 got,
1478 vec![7u32; nelmts as usize],
1479 "minbits==0 with FILL_DEFINED must emit minval, not filval"
1480 );
1481 }
1482
1483 #[test]
1489 fn fill_defined_sentinel_emits_filval_not_minval() {
1490 let nelmts = 4u32;
1491 let size = 4u32;
1492 let mut cd = vec![0u32; TOTAL_NPARMS];
1493 cd[PARM_SCALETYPE] = SO_INT;
1494 cd[PARM_SCALEFACTOR] = 0;
1495 cd[PARM_NELMTS] = nelmts;
1496 cd[PARM_CLASS] = CLS_INTEGER;
1497 cd[PARM_SIZE] = size;
1498 cd[PARM_SIGN] = SGN_NONE;
1499 cd[PARM_ORDER] = ORDER_LE;
1500 cd[PARM_FILAVAIL] = FILL_DEFINED;
1501 cd[PARM_FILVAL] = 999;
1502 let f = FilterDescription {
1503 filter_id: crate::filter_pipeline::FILTER_SCALEOFFSET,
1504 name: None,
1505 flags: 0,
1506 client_data: cd,
1507 };
1508 let mut chunk = Vec::new();
1511 chunk.extend_from_slice(&3u32.to_le_bytes());
1512 chunk.push(8);
1513 chunk.extend_from_slice(&10u64.to_le_bytes());
1514 chunk.extend_from_slice(&[0u8; HEADER_LEN - 13]);
1515 chunk.extend_from_slice(&pack_offsets(&[0, 1, 7, 2], 3, 4).unwrap());
1516 let out = decompress(&chunk, &f).unwrap();
1517 let got: Vec<u32> = out
1518 .chunks_exact(4)
1519 .map(|c| u32::from_le_bytes(c.try_into().unwrap()))
1520 .collect();
1521 assert_eq!(got, vec![10u32, 11, 999, 12]);
1522 }
1523
1524 #[test]
1528 fn compress_rejects_fill_defined() {
1529 let nelmts = 4u32;
1530 let size = 4u32;
1531 let mut cd = vec![0u32; TOTAL_NPARMS];
1532 cd[PARM_SCALETYPE] = SO_INT;
1533 cd[PARM_NELMTS] = nelmts;
1534 cd[PARM_CLASS] = CLS_INTEGER;
1535 cd[PARM_SIZE] = size;
1536 cd[PARM_SIGN] = SGN_NONE;
1537 cd[PARM_ORDER] = ORDER_LE;
1538 cd[PARM_FILAVAIL] = FILL_DEFINED;
1539 cd[PARM_FILVAL] = 0;
1540 let f = FilterDescription {
1541 filter_id: crate::filter_pipeline::FILTER_SCALEOFFSET,
1542 name: None,
1543 flags: 0,
1544 client_data: cd,
1545 };
1546 let raw = vec![0u8; nelmts as usize * size as usize];
1547 assert!(matches!(
1548 compress(&raw, &f),
1549 Err(FormatError::FilterError(_))
1550 ));
1551 }
1552
1553 #[test]
1556 fn decompress_rejects_escale() {
1557 let mut cd = vec![0u32; TOTAL_NPARMS];
1558 cd[PARM_SCALETYPE] = SO_FLOAT_ESCALE;
1559 cd[PARM_NELMTS] = 4;
1560 cd[PARM_CLASS] = CLS_FLOAT;
1561 cd[PARM_SIZE] = 4;
1562 cd[PARM_ORDER] = ORDER_LE;
1563 let f = FilterDescription {
1564 filter_id: crate::filter_pipeline::FILTER_SCALEOFFSET,
1565 name: None,
1566 flags: 0,
1567 client_data: cd,
1568 };
1569 let chunk = vec![0u8; HEADER_LEN + 4];
1570 assert!(matches!(
1571 decompress(&chunk, &f),
1572 Err(FormatError::FilterError(_))
1573 ));
1574 }
1575
1576 #[test]
1579 fn decompress_rejects_oversized_minbits_header() {
1580 let f = int_filter(4, false, ORDER_LE, 4);
1581 let mut bad = Vec::new();
1583 bad.extend_from_slice(&33u32.to_le_bytes());
1584 bad.push(8);
1585 bad.extend_from_slice(&0u64.to_le_bytes());
1586 bad.extend_from_slice(&[0u8; HEADER_LEN - 13]);
1587 bad.extend_from_slice(&[0u8; 16]); assert!(matches!(
1589 decompress(&bad, &f),
1590 Err(FormatError::FilterError(_))
1591 ));
1592 }
1593
1594 #[test]
1597 fn integer_i8_roundtrip_with_negatives() {
1598 let vals: [i8; 6] = [-100, -50, 0, 27, -99, 100];
1599 let raw: Vec<u8> = vals.iter().map(|&v| v as u8).collect();
1600 let f = int_filter(1, true, ORDER_LE, vals.len() as u32);
1601 let comp = compress(&raw, &f).unwrap();
1602 let dec = decompress(&comp, &f).unwrap();
1603 assert_eq!(dec, raw);
1604 }
1605
1606 #[test]
1611 fn single_element_chunk_roundtrip() {
1612 let raw = 42u32.to_le_bytes().to_vec();
1613 let f = int_filter(4, false, ORDER_LE, 1);
1614 let comp = compress(&raw, &f).unwrap();
1615 let dec = decompress(&comp, &f).unwrap();
1616 assert_eq!(dec, raw);
1617
1618 let raw = 3.14f64.to_le_bytes().to_vec();
1619 let f = float_filter(8, 3, ORDER_LE, 1);
1620 let comp = compress(&raw, &f).unwrap();
1621 let dec = decompress(&comp, &f).unwrap();
1622 let got = f64::from_le_bytes(dec.as_slice().try_into().unwrap());
1623 assert!((got - 3.14).abs() <= 0.5e-3);
1624 }
1625
1626 #[test]
1627 fn scale_offset_type_from_datatype_classes() {
1628 let i32_ty = Datatype::FixedPoint {
1629 size: 4,
1630 byte_order: DatatypeByteOrder::LittleEndian,
1631 signed: true,
1632 bit_offset: 0,
1633 bit_precision: 32,
1634 };
1635 let so = scale_offset_type_from_datatype(&i32_ty).unwrap();
1636 assert_eq!(so.class, CLS_INTEGER);
1637 assert_eq!(so.sign, SGN_2);
1638 assert_eq!(so.order, ORDER_LE);
1639
1640 let f64_ty = Datatype::FloatingPoint {
1641 size: 8,
1642 byte_order: DatatypeByteOrder::BigEndian,
1643 bit_offset: 0,
1644 bit_precision: 64,
1645 exponent_location: 52,
1646 exponent_size: 11,
1647 mantissa_location: 0,
1648 mantissa_size: 52,
1649 exponent_bias: 1023,
1650 };
1651 let so = scale_offset_type_from_datatype(&f64_ty).unwrap();
1652 assert_eq!(so.class, CLS_FLOAT);
1653 assert_eq!(so.order, ORDER_BE);
1654 }
1655}