#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{format, string::ToString, vec, vec::Vec};
use crate::convert::TryToUsize;
use crate::datatype::{Datatype, DatatypeByteOrder};
use crate::error::FormatError;
use crate::fill_value::FillPattern;
use crate::filter_pipeline::FilterDescription;
const PARM_SCALETYPE: usize = 0;
const PARM_SCALEFACTOR: usize = 1;
const PARM_NELMTS: usize = 2;
const PARM_CLASS: usize = 3;
const PARM_SIZE: usize = 4;
const PARM_SIGN: usize = 5;
const PARM_ORDER: usize = 6;
const PARM_FILAVAIL: usize = 7;
const PARM_FILVAL: usize = 8;
const TOTAL_NPARMS: usize = 20;
const CORE_NPARMS: usize = PARM_FILVAL;
const SO_FLOAT_DSCALE: u32 = 0;
const SO_FLOAT_ESCALE: u32 = 1;
const SO_INT: u32 = 2;
const CLS_INTEGER: u32 = 0;
const CLS_FLOAT: u32 = 1;
const SGN_NONE: u32 = 0;
const SGN_2: u32 = 1;
const ORDER_LE: u32 = 0;
const ORDER_BE: u32 = 1;
const FILL_UNDEFINED: u32 = 0;
const FILL_DEFINED: u32 = 1;
pub(crate) const HEADER_LEN: usize = 21;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScaleOffset {
Integer(u32),
FloatDScale(i32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScaleOffsetType {
class: u32,
sign: u32,
order: u32,
}
pub fn scale_offset_type_from_datatype(dt: &Datatype) -> Option<ScaleOffsetType> {
match dt {
Datatype::FixedPoint {
size,
byte_order,
signed,
..
} if matches!(*size, 1 | 2 | 4 | 8) => Some(ScaleOffsetType {
class: CLS_INTEGER,
sign: if *signed { SGN_2 } else { SGN_NONE },
order: order_code(byte_order)?,
}),
Datatype::FloatingPoint {
size, byte_order, ..
} if matches!(*size, 4 | 8) => Some(ScaleOffsetType {
class: CLS_FLOAT,
sign: SGN_NONE,
order: order_code(byte_order)?,
}),
_ => None,
}
}
fn order_code(order: &DatatypeByteOrder) -> Option<u32> {
match order {
DatatypeByteOrder::LittleEndian => Some(ORDER_LE),
DatatypeByteOrder::BigEndian => Some(ORDER_BE),
DatatypeByteOrder::Vax => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FillAvailability {
Defined,
Undefined,
}
impl FillAvailability {
pub fn with_value(self, fill: FillPattern<'_>) -> Result<ScaleOffsetFill<'_>, FormatError> {
match self {
Self::Defined => Ok(ScaleOffsetFill::Defined(fill.element()?)),
Self::Undefined => Ok(ScaleOffsetFill::Undefined),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScaleOffsetFill<'a> {
Defined(Option<&'a [u8]>),
Undefined,
}
pub fn build_cd_values(
mode: ScaleOffset,
ty: ScaleOffsetType,
size: u32,
nelmts: u32,
fill: ScaleOffsetFill<'_>,
) -> Result<Vec<u32>, FormatError> {
let (scale_type, scale_factor) = match (mode, ty.class) {
(ScaleOffset::Integer(minbits), CLS_INTEGER) => (SO_INT, minbits),
(ScaleOffset::FloatDScale(decimals), CLS_FLOAT) => (SO_FLOAT_DSCALE, decimals as u32),
(ScaleOffset::Integer(_), _) => {
return Err(FormatError::FilterError(
"scaleoffset: integer mode requires an integer dataset".to_string(),
));
}
(ScaleOffset::FloatDScale(_), _) => {
return Err(FormatError::FilterError(
"scaleoffset: float D-scale mode requires a floating-point dataset".to_string(),
));
}
};
if size == 0 || size > 8 {
return Err(FormatError::FilterError(format!(
"scaleoffset: unsupported datatype size {size}"
)));
}
let mut cd = vec![0u32; TOTAL_NPARMS];
cd[PARM_SCALETYPE] = scale_type;
cd[PARM_SCALEFACTOR] = scale_factor;
cd[PARM_NELMTS] = nelmts;
cd[PARM_CLASS] = ty.class;
cd[PARM_SIZE] = size;
cd[PARM_SIGN] = ty.sign;
cd[PARM_ORDER] = ty.order;
match fill {
ScaleOffsetFill::Undefined => cd[PARM_FILAVAIL] = FILL_UNDEFINED,
ScaleOffsetFill::Defined(bytes) => {
let width = size.to_usize()?;
let bits = match bytes {
None => 0,
Some(b) if b.len() == width => read_value(b, width, ty.order),
Some(b) => {
return Err(FormatError::FilterError(format!(
"scaleoffset: fill value is {} bytes for a {width}-byte datatype",
b.len()
)));
}
};
cd[PARM_FILAVAIL] = FILL_DEFINED;
write_fill_bits(&mut cd, bits, width);
}
}
Ok(cd)
}
pub(crate) fn scale_offset_mode(cd_values: &[u32]) -> Option<(ScaleOffset, FillAvailability)> {
let scale_type = *cd_values.get(PARM_SCALETYPE)?;
let scale_factor = *cd_values.get(PARM_SCALEFACTOR)?;
let fill = match *cd_values.get(PARM_FILAVAIL)? {
FILL_UNDEFINED => FillAvailability::Undefined,
FILL_DEFINED => FillAvailability::Defined,
_ => return None,
};
let mode = match scale_type {
SO_INT => ScaleOffset::Integer(scale_factor),
#[expect(
clippy::cast_possible_wrap,
reason = "scale_factor is a small decimal scale factor (D-scale parameter); the reference treats it as a signed int"
)]
SO_FLOAT_DSCALE => ScaleOffset::FloatDScale(scale_factor as i32),
_ => return None,
};
Some((mode, fill))
}
struct Parms {
scale_type: u32,
scale_factor: i32,
nelmts: usize,
class: u32,
size: usize,
order: u32,
filavail: u32,
}
impl Parms {
fn parse(cd: &[u32]) -> Result<Parms, FormatError> {
if cd.len() < CORE_NPARMS {
return Err(FormatError::FilterError(
"scaleoffset: too few cd_values".to_string(),
));
}
let class = cd[PARM_CLASS];
if class != CLS_INTEGER && class != CLS_FLOAT {
return Err(FormatError::FilterError(
"scaleoffset: unsupported datatype class".to_string(),
));
}
let size = cd[PARM_SIZE] as usize;
if size == 0 || size > 8 {
return Err(FormatError::FilterError(
"scaleoffset: unsupported datatype size".to_string(),
));
}
if class == CLS_FLOAT && size != 4 && size != 8 {
return Err(FormatError::FilterError(
"scaleoffset: float size must be 4 or 8".to_string(),
));
}
let order = cd[PARM_ORDER];
if order != ORDER_LE && order != ORDER_BE {
return Err(FormatError::FilterError(
"scaleoffset: bad byte order".to_string(),
));
}
#[expect(
clippy::cast_possible_wrap,
reason = "scale_factor is a small filter parameter (decimal scale factor or bit width); the reference stores it in a signed int"
)]
Ok(Parms {
scale_type: cd[PARM_SCALETYPE],
scale_factor: cd[PARM_SCALEFACTOR] as i32,
nelmts: cd[PARM_NELMTS].to_usize()?,
class,
size,
order,
filavail: cd[PARM_FILAVAIL],
})
}
fn width_mask(&self) -> u64 {
if self.size >= 8 {
u64::MAX
} else {
(1u64 << (self.size * 8)) - 1
}
}
}
fn pass_through_or_refuse(p: &Parms) -> Result<bool, FormatError> {
if p.scale_type == SO_FLOAT_DSCALE {
return Ok(false);
}
#[expect(
clippy::cast_possible_truncation,
reason = "p.size is validated to 1..=8, so p.size * 8 is 8..=64 and fits in u32"
)]
let type_bits = (p.size * 8) as u32;
#[expect(
clippy::cast_possible_wrap,
reason = "type_bits is 8..=64, so it fits in a non-negative i32"
)]
let type_bits_i = type_bits as i32;
if p.scale_factor > type_bits_i {
return Err(FormatError::FilterError(format!(
"scaleoffset: requested minimum bit width {} exceeds the {type_bits}-bit datatype",
p.scale_factor
)));
}
Ok(p.scale_factor == type_bits_i)
}
pub fn decompress(
input: &[u8],
filter: &FilterDescription,
max_output: Option<usize>,
) -> Result<Vec<u8>, FormatError> {
let cd = &filter.client_data;
let p = Parms::parse(cd)?;
if p.scale_type == SO_FLOAT_ESCALE {
return Err(FormatError::FilterError(
"scaleoffset: float E-scale method is not supported".to_string(),
));
}
#[expect(
clippy::cast_possible_truncation,
reason = "p.size is validated to 1..=8, so p.size * 8 is 8..=64 and fits in u32"
)]
let full_bits = (p.size * 8) as u32;
let size_out = (p.nelmts as u64 * p.size as u64).to_usize()?;
if let Some(cap) = max_output
&& size_out > cap
{
return Err(FormatError::FilterError(format!(
"scaleoffset: chunk declares {size_out} decoded bytes, more than the \
{cap} the chunk can hold"
)));
}
if pass_through_or_refuse(&p)? {
return Ok(input.to_vec());
}
if input.len() < 5 {
return Err(FormatError::FilterError(
"scaleoffset: chunk shorter than header".to_string(),
));
}
let minbits = u32::from_le_bytes([input[0], input[1], input[2], input[3]]);
if minbits > full_bits {
return Err(FormatError::FilterError(
"scaleoffset: minbits exceeds datatype size".to_string(),
));
}
let minval_size = (input[4] as usize).min(8);
if input.len() < 5 + minval_size {
return Err(FormatError::FilterError(
"scaleoffset: chunk too short for minval".to_string(),
));
}
let mut minval_bytes = [0u8; 8];
minval_bytes[..minval_size].copy_from_slice(&input[5..5 + minval_size]);
let minval = u64::from_le_bytes(minval_bytes);
if minbits == full_bits {
let start = HEADER_LEN;
if input.len() < start + size_out {
return Err(FormatError::FilterError(
"scaleoffset: chunk too short for raw payload".to_string(),
));
}
return Ok(input[start..start + size_out].to_vec());
}
let mut out = Vec::with_capacity(size_out);
if minbits == 0 {
let mask = p.width_mask();
let bits = if p.filavail == FILL_DEFINED {
read_fill_bits(cd, p.size)?
} else {
minval & mask
};
for _ in 0..p.nelmts {
write_value(&mut out, bits, p.size, p.order);
}
return Ok(out);
}
if input.len() < HEADER_LEN {
return Err(FormatError::FilterError(
"scaleoffset: chunk too short for packed payload".to_string(),
));
}
let payload = &input[HEADER_LEN..];
if (payload.len() as u64) * 8 < p.nelmts as u64 * minbits as u64 {
return Err(FormatError::FilterError(
"scaleoffset: payload too short for packed data".to_string(),
));
}
let mut reader = BitReader::new(payload);
if p.class == CLS_INTEGER {
reconstruct_integer(&mut out, &mut reader, &p, minbits, minval, cd)?;
} else {
reconstruct_float(&mut out, &mut reader, &p, minbits, minval, cd)?;
}
Ok(out)
}
pub fn compress(input: &[u8], filter: &FilterDescription) -> Result<Vec<u8>, FormatError> {
let cd = &filter.client_data;
let p = Parms::parse(cd)?;
if p.class == CLS_INTEGER && p.scale_type != SO_INT {
return Err(FormatError::FilterError(
"scaleoffset: integer class requires integer scale type".to_string(),
));
}
if p.class == CLS_FLOAT && p.scale_type != SO_FLOAT_DSCALE {
return Err(FormatError::FilterError(
"scaleoffset: float class requires D-scale scale type".to_string(),
));
}
if pass_through_or_refuse(&p)? {
return Ok(input.to_vec());
}
let expected = (p.nelmts as u64 * p.size as u64).to_usize()?;
if input.len() != expected {
return Err(FormatError::FilterError(
"scaleoffset: chunk size does not match nelmts * datatype size".to_string(),
));
}
if p.nelmts == 0 {
return Ok(emit(0, 0, &[]));
}
let signed = cd[PARM_SIGN] == SGN_2;
#[expect(
clippy::cast_possible_truncation,
reason = "p.size is validated to 1..=8, so p.size * 8 is 8..=64 and fits in u32"
)]
let full_bits = (p.size * 8) as u32;
let filval = if p.filavail == FILL_DEFINED {
Some(read_fill_bits(cd, p.size)?)
} else {
None
};
let (minbits, minval, offsets) = if p.class == CLS_INTEGER {
precompress_integer(input, &p, signed, filval)
} else {
precompress_float(input, &p, filval)
};
if minbits >= full_bits {
return Ok(emit_raw(full_bits, minval, input));
}
let payload = pack_offsets(&offsets, minbits, p.nelmts)?;
Ok(emit(minbits, minval, &payload))
}
fn reconstruct_integer(
out: &mut Vec<u8>,
reader: &mut BitReader<'_>,
p: &Parms,
minbits: u32,
minval: u64,
cd: &[u32],
) -> Result<(), FormatError> {
let mask = p.width_mask();
let sentinel = sentinel(minbits);
let filval = if p.filavail == FILL_DEFINED {
Some(read_fill_bits(cd, p.size)? & mask)
} else {
None
};
for _ in 0..p.nelmts {
let d = reader.read(minbits);
let bits = match filval {
Some(fv) if d == sentinel => fv,
_ => d.wrapping_add(minval) & mask,
};
write_value(out, bits, p.size, p.order);
}
Ok(())
}
fn precompress_integer(
input: &[u8],
p: &Parms,
signed: bool,
filval: Option<u64>,
) -> (u32, u64, Vec<u64>) {
#[expect(
clippy::cast_possible_truncation,
reason = "p.size is validated to 1..=8, so p.size * 8 is 8..=64 and fits in u32"
)]
let full_bits = (p.size * 8) as u32;
let read_as_i128 = |i: usize| -> i128 {
let bits = read_value(&input[i * p.size..(i + 1) * p.size], p.size, p.order);
if signed {
sign_extend(bits, p.size) as i128
} else {
bits as i128
}
};
let filval = filval.map(|bits| {
if signed {
sign_extend(bits, p.size) as i128
} else {
(bits & p.width_mask()) as i128
}
});
let (min, max) = match filval {
None => {
let first = read_as_i128(0);
let (mut min, mut max) = (first, first);
for i in 1..p.nelmts {
let v = read_as_i128(i);
if v < min {
min = v;
}
if v > max {
max = v;
}
}
(min, max)
}
Some(fv) => {
let (mut min, mut max) = (0i128, 0i128);
let mut seen = false;
for i in 0..p.nelmts {
let v = read_as_i128(i);
if v == fv {
continue;
}
if !seen {
min = v;
max = v;
seen = true;
continue;
}
if v < min {
min = v;
}
if v > max {
max = v;
}
}
(min, max)
}
};
#[expect(
clippy::cast_possible_truncation,
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"
)]
let minval = if signed {
(min as i64) as u64
} else {
min as u64
};
let width_max: u128 = p.width_mask() as u128;
let spread = (max - min) as u128;
if spread > width_max.saturating_sub(2) {
let schar_quirk = signed && p.size == 1 && filval.is_none();
return (full_bits, if schar_quirk { minval } else { 0 }, Vec::new());
}
#[expect(
clippy::cast_possible_truncation,
reason = "the guard above returns early unless spread <= width_max - 2, and width_max <= u64::MAX, so spread + 2 fits in u64"
)]
let span = (spread as u64) + 1;
let minbits = ceil_log2(if filval.is_some() { span + 1 } else { span });
if minbits >= full_bits {
return (full_bits, minval, Vec::new());
}
#[expect(
clippy::cast_possible_truncation,
reason = "each offset (value - min) is at most spread <= width_max <= u64::MAX, so it fits in u64"
)]
let offsets = match filval {
None => (0..p.nelmts)
.map(|i| (read_as_i128(i) - min) as u64)
.collect(),
Some(fv) => {
let sentinel = sentinel(minbits);
(0..p.nelmts)
.map(|i| {
let v = read_as_i128(i);
if v == fv { sentinel } else { (v - min) as u64 }
})
.collect()
}
};
(minbits, minval, offsets)
}
fn reconstruct_float(
out: &mut Vec<u8>,
reader: &mut BitReader<'_>,
p: &Parms,
minbits: u32,
minval: u64,
cd: &[u32],
) -> Result<(), FormatError> {
let sentinel = sentinel(minbits);
let decimals = p.scale_factor;
let filval = if p.filavail == FILL_DEFINED {
Some(read_fill_bits(cd, p.size)?)
} else {
None
};
if p.size == 4 {
#[expect(
clippy::cast_possible_truncation,
reason = "size == 4 branch: minval holds a 4-byte f32 bit pattern in its low 32 bits, so narrowing to u32 reconstructs that pattern"
)]
let min = f32::from_bits(minval as u32);
let pow = pow10_f32(decimals);
for _ in 0..p.nelmts {
let d = reader.read(minbits);
#[expect(
clippy::cast_possible_wrap,
reason = "d is a minbits-wide offset (minbits < full_bits <= 64); reinterpreting it as i64 matches the reference's signed reconstruction"
)]
let bits = if let Some(fv) = filval.filter(|_| d == sentinel) {
fv
} else {
((d as i64 as f32) / pow + min).to_bits() as u64
};
write_value(out, bits, 4, p.order);
}
} else {
let min = f64::from_bits(minval);
let pow = pow10_f64(decimals);
for _ in 0..p.nelmts {
let d = reader.read(minbits);
#[expect(
clippy::cast_possible_wrap,
reason = "d is a minbits-wide offset (minbits < full_bits <= 64); reinterpreting it as i64 matches the reference's signed reconstruction"
)]
let bits = if let Some(fv) = filval.filter(|_| d == sentinel) {
fv
} else {
((d as i64 as f64) / pow + min).to_bits()
};
write_value(out, bits, 8, p.order);
}
}
Ok(())
}
fn precompress_float(input: &[u8], p: &Parms, filval: Option<u64>) -> (u32, u64, Vec<u64>) {
let decimals = p.scale_factor;
#[expect(
clippy::cast_possible_truncation,
reason = "p.size is validated to 1..=8, so p.size * 8 is 8..=64 and fits in u32"
)]
let full_bits = (p.size * 8) as u32;
if p.size == 4 {
#[expect(
clippy::cast_possible_truncation,
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"
)]
let read_f32 = |i: usize| -> f32 {
f32::from_bits(read_value(&input[i * 4..i * 4 + 4], 4, p.order) as u32)
};
#[expect(
clippy::cast_possible_truncation,
reason = "the fill value of a 4-byte float occupies the low 32 bits of the recovered pattern"
)]
let fill = filval.map(|bits| f32::from_bits(bits as u32));
let scan_tol = pow10_f64(-decimals);
let is_fill_in_scan = |v: f32| fill.is_some_and(|fv| f64::from(v - fv).abs() < scan_tol);
let (min, max) = match fill {
None => {
let first = read_f32(0);
let (mut min, mut max) = (first, first);
for i in 1..p.nelmts {
let v = read_f32(i);
if v < min {
min = v;
}
if v > max {
max = v;
}
}
(min, max)
}
Some(_) => {
let (mut min, mut max) = (0f32, 0f32);
let mut seen = false;
for i in 0..p.nelmts {
let v = read_f32(i);
if is_fill_in_scan(v) {
continue;
}
if !seen {
min = v;
max = v;
seen = true;
continue;
}
if v < min {
min = v;
}
if v > max {
max = v;
}
}
(min, max)
}
};
let pow = pow10_f32(decimals);
let min_scaled = min * pow;
let residual = max * pow - min_scaled;
let minval = min.to_bits() as u64;
if residual > (1u64 << 31) as f32 {
return (full_bits, 0, Vec::new());
}
let span = (round_half_away_f32(residual) as u64) + 1;
let minbits = ceil_log2(if fill.is_some() { span + 1 } else { span });
if minbits >= full_bits {
return (full_bits, minval, Vec::new());
}
let modify_tol = pow10_f32(-decimals);
let sentinel = sentinel(minbits);
let offsets = (0..p.nelmts)
.map(|i| {
let v = read_f32(i);
match fill {
Some(fv) if (v - fv).abs() < modify_tol => sentinel,
_ => round_half_away_f32(v * pow - min_scaled) as u64,
}
})
.collect();
(minbits, minval, offsets)
} else {
let read_f64 =
|i: usize| -> f64 { f64::from_bits(read_value(&input[i * 8..i * 8 + 8], 8, p.order)) };
let fill = filval.map(f64::from_bits);
let tol = pow10_f64(-decimals);
let (min, max) = match fill {
None => {
let first = read_f64(0);
let (mut min, mut max) = (first, first);
for i in 1..p.nelmts {
let v = read_f64(i);
if v < min {
min = v;
}
if v > max {
max = v;
}
}
(min, max)
}
Some(fv) => {
let (mut min, mut max) = (0f64, 0f64);
let mut seen = false;
for i in 0..p.nelmts {
let v = read_f64(i);
if (v - fv).abs() < tol {
continue;
}
if !seen {
min = v;
max = v;
seen = true;
continue;
}
if v < min {
min = v;
}
if v > max {
max = v;
}
}
(min, max)
}
};
let pow = pow10_f64(decimals);
let min_scaled = min * pow;
let residual = max * pow - min_scaled;
let minval = min.to_bits();
if residual > (1u64 << 63) as f64 {
return (full_bits, 0, Vec::new());
}
let span = (round_half_away_f64(residual) as u64) + 1;
let minbits = ceil_log2(if fill.is_some() { span + 1 } else { span });
if minbits >= full_bits {
return (full_bits, minval, Vec::new());
}
let sentinel = sentinel(minbits);
let offsets = (0..p.nelmts)
.map(|i| {
let v = read_f64(i);
match fill {
Some(fv) if (v - fv).abs() < tol => sentinel,
_ => round_half_away_f64(v * pow - min_scaled) as u64,
}
})
.collect();
(minbits, minval, offsets)
}
}
fn emit(minbits: u32, minval: u64, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(HEADER_LEN + payload.len().max(1));
write_header(&mut out, minbits, minval);
if payload.is_empty() {
out.push(0);
} else {
out.extend_from_slice(payload);
}
out
}
fn emit_raw(full_bits: u32, minval: u64, input: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(HEADER_LEN + input.len());
write_header(&mut out, full_bits, minval);
out.extend_from_slice(input);
out
}
fn write_header(out: &mut Vec<u8>, minbits: u32, minval: u64) {
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]); }
fn pack_offsets(offsets: &[u64], minbits: u32, nelmts: usize) -> Result<Vec<u8>, FormatError> {
let payload_len = (nelmts as u64 * minbits as u64 / 8 + 1).to_usize()?;
if minbits == 0 {
return Ok(vec![0u8; payload_len]);
}
let mut buf = Vec::with_capacity(payload_len);
let mask: u64 = if minbits == 64 {
u64::MAX
} else {
(1u64 << minbits) - 1
};
let mut acc: u64 = 0;
let mut nbits: u32 = 0;
for &v in offsets {
while nbits >= 8 {
nbits -= 8;
buf.push(((acc >> nbits) & 0xFF) as u8);
}
let combined = ((acc as u128) << minbits) | (v & mask) as u128;
let total = nbits + minbits;
if total <= 64 {
#[expect(
clippy::cast_possible_truncation,
reason = "this branch is guarded by total <= 64, and combined occupies exactly `total` bits, so it fits in u64"
)]
{
acc = combined as u64;
}
nbits = total;
} else {
let drop = total - 64;
#[expect(
clippy::cast_possible_truncation,
reason = "combined >> drop keeps the high 64 bits of a (64 + drop)-bit value, which fits in u64"
)]
let top = (combined >> drop) as u64;
buf.extend_from_slice(&top.to_be_bytes());
#[expect(
clippy::cast_possible_truncation,
reason = "combined & ((1 << drop) - 1) masks to the low `drop` bits (drop is 1..=7 here), which fits in u64"
)]
{
acc = (combined & ((1u128 << drop) - 1)) as u64;
}
nbits = drop;
}
}
while nbits >= 8 {
nbits -= 8;
buf.push(((acc >> nbits) & 0xFF) as u8);
}
if nbits > 0 {
buf.push(((acc << (8 - nbits)) & 0xFF) as u8);
}
while buf.len() < payload_len {
buf.push(0);
}
Ok(buf)
}
struct BitReader<'a> {
payload: &'a [u8],
bit_pos: usize,
}
impl<'a> BitReader<'a> {
fn new(payload: &'a [u8]) -> Self {
Self {
payload,
bit_pos: 0,
}
}
#[inline]
fn read(&mut self, minbits: u32) -> u64 {
debug_assert!((1..=64).contains(&minbits));
let byte = self.bit_pos >> 3;
#[expect(
clippy::cast_possible_truncation,
reason = "bit_pos & 7 masks to 0..=7, which trivially fits in u32"
)]
let off = (self.bit_pos & 7) as u32;
let (hi, lo) = if byte + 9 <= self.payload.len() {
let hi = u64::from_be_bytes(self.payload[byte..byte + 8].try_into().unwrap());
let lo = self.payload[byte + 8] as u64;
(hi, lo)
} else {
let mut window = [0u8; 9];
let take = self.payload.len().saturating_sub(byte).min(9);
window[..take].copy_from_slice(&self.payload[byte..byte + take]);
let hi = u64::from_be_bytes(window[..8].try_into().unwrap());
let lo = window[8] as u64;
(hi, lo)
};
let combined = if off == 0 {
hi
} else {
(hi << off) | (lo >> (8 - off))
};
self.bit_pos += minbits as usize;
combined >> (64 - minbits)
}
}
#[cfg(test)]
fn unpack_bits(payload: &[u8], nelmts: usize, minbits: u32) -> Result<Vec<u64>, FormatError> {
let total_bits = nelmts * minbits as usize;
if payload.len() * 8 < total_bits {
return Err(FormatError::FilterError(
"scaleoffset: payload too short for packed data".to_string(),
));
}
let mut reader = BitReader::new(payload);
Ok((0..nelmts).map(|_| reader.read(minbits)).collect())
}
fn read_value(chunk: &[u8], size: usize, order: u32) -> u64 {
let mut bytes = [0u8; 8];
if order == ORDER_LE {
bytes[..size].copy_from_slice(&chunk[..size]);
} else {
for (k, &b) in chunk[..size].iter().enumerate() {
bytes[size - 1 - k] = b;
}
}
u64::from_le_bytes(bytes)
}
fn write_value(out: &mut Vec<u8>, bits: u64, size: usize, order: u32) {
let le = bits.to_le_bytes();
if order == ORDER_LE {
out.extend_from_slice(&le[..size]);
} else {
for k in (0..size).rev() {
out.push(le[k]);
}
}
}
#[expect(
clippy::cast_possible_wrap,
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"
)]
fn sign_extend(bits: u64, size: usize) -> i64 {
if size >= 8 {
bits as i64
} else {
let shift = 64 - size * 8;
((bits << shift) as i64) >> shift
}
}
fn sentinel(minbits: u32) -> u64 {
(1u64 << minbits).wrapping_sub(1)
}
fn write_fill_bits(cd: &mut [u32], bits: u64, size: usize) {
let le = bits.to_le_bytes();
let mut off = 0;
let mut idx = PARM_FILVAL;
while off < size {
let take = (size - off).min(4);
let mut entry = [0u8; 4];
entry[..take].copy_from_slice(&le[off..off + take]);
cd[idx] = u32::from_le_bytes(entry);
off += take;
idx += 1;
}
}
fn read_fill_bits(cd: &[u32], size: usize) -> Result<u64, FormatError> {
let entries = size.div_ceil(4);
if cd.len() < PARM_FILVAL + entries {
return Err(FormatError::FilterError(
"scaleoffset: cd_values missing fill value".to_string(),
));
}
let mut bytes = [0u8; 8];
let mut off = 0;
let mut idx = PARM_FILVAL;
while off < size {
let take = (size - off).min(4);
bytes[off..off + take].copy_from_slice(&cd[idx].to_le_bytes()[..take]);
off += take;
idx += 1;
}
Ok(u64::from_le_bytes(bytes))
}
fn pow10_f64(exp: i32) -> f64 {
let mut result = 1.0f64;
let mut base = 10.0f64;
let mut n = exp.unsigned_abs();
while n > 0 {
if n & 1 == 1 {
result *= base;
}
base *= base;
n >>= 1;
}
if exp < 0 { 1.0 / result } else { result }
}
#[expect(
clippy::cast_possible_truncation,
reason = "narrowing 10^exp from f64 to f32 is the intended value conversion; out-of-range magnitudes saturate to +/-inf, matching the C reference"
)]
fn pow10_f32(exp: i32) -> f32 {
pow10_f64(exp) as f32
}
#[expect(
clippy::cast_possible_truncation,
reason = "float-to-int `as` saturates in Rust, so rounding to i64 is safe even for out-of-range inputs (matches C llround)"
)]
fn round_half_away_f64(x: f64) -> i64 {
const INTEGRAL: f64 = (1u64 << 52) as f64;
if !(x > -INTEGRAL && x < INTEGRAL) {
return x as i64;
}
let trunc = x as i64;
let frac = x - trunc as f64;
if frac >= 0.5 {
trunc + 1
} else if frac <= -0.5 {
trunc - 1
} else {
trunc
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "float-to-int `as` saturates in Rust, so rounding to i64 is safe even for out-of-range inputs (matches C lroundf)"
)]
fn round_half_away_f32(x: f32) -> i64 {
const INTEGRAL: f32 = (1u32 << 23) as f32;
if !(x > -INTEGRAL && x < INTEGRAL) {
return x as i64;
}
let trunc = x as i64;
let frac = x - trunc as f32;
if frac >= 0.5 {
trunc + 1
} else if frac <= -0.5 {
trunc - 1
} else {
trunc
}
}
fn ceil_log2(num: u64) -> u32 {
let mut v = 0u32;
let mut lower_bound = 1u64;
let mut val = num;
loop {
val >>= 1;
if val == 0 {
break;
}
v += 1;
lower_bound <<= 1;
}
if num == lower_bound { v } else { v + 1 }
}
#[cfg(test)]
mod tests {
use super::*;
fn int_filter(size: u32, signed: bool, order: u32, nelmts: u32) -> FilterDescription {
let ty = ScaleOffsetType {
class: CLS_INTEGER,
sign: if signed { SGN_2 } else { SGN_NONE },
order,
};
FilterDescription {
filter_id: crate::filter_pipeline::FILTER_SCALEOFFSET,
name: None,
flags: 0,
client_data: build_cd_values(
ScaleOffset::Integer(0),
ty,
size,
nelmts,
ScaleOffsetFill::Undefined,
)
.unwrap(),
}
}
fn float_filter(size: u32, decimals: i32, order: u32, nelmts: u32) -> FilterDescription {
let ty = ScaleOffsetType {
class: CLS_FLOAT,
sign: SGN_NONE,
order,
};
FilterDescription {
filter_id: crate::filter_pipeline::FILTER_SCALEOFFSET,
name: None,
flags: 0,
client_data: build_cd_values(
ScaleOffset::FloatDScale(decimals),
ty,
size,
nelmts,
ScaleOffsetFill::Undefined,
)
.unwrap(),
}
}
#[test]
fn scale_offset_mode_recovers_and_refuses() {
let f = int_filter(4, true, ORDER_LE, 16);
assert_eq!(
scale_offset_mode(&f.client_data),
Some((ScaleOffset::Integer(0), FillAvailability::Undefined))
);
let f = float_filter(8, 3, ORDER_LE, 16);
assert_eq!(
scale_offset_mode(&f.client_data),
Some((ScaleOffset::FloatDScale(3), FillAvailability::Undefined))
);
let mut fill_defined = int_filter(4, true, ORDER_LE, 16);
fill_defined.client_data[PARM_FILAVAIL] = FILL_DEFINED;
assert_eq!(
scale_offset_mode(&fill_defined.client_data),
Some((ScaleOffset::Integer(0), FillAvailability::Defined))
);
let mut bad = int_filter(4, true, ORDER_LE, 16);
bad.client_data[PARM_FILAVAIL] = 2;
assert_eq!(scale_offset_mode(&bad.client_data), None);
assert_eq!(scale_offset_mode(&[]), None);
assert_eq!(scale_offset_mode(&[SO_INT, 0]), None);
}
#[test]
fn the_fill_undefined_fallback_carries_the_signed_char_minimum() {
let minval_of = |raw: &[u8], size: u32, signed: bool| {
let nelmts = raw.len() as u32 / size;
let f = int_filter(size, signed, ORDER_LE, nelmts);
let out = compress(raw, &f).unwrap();
assert_eq!(
u32::from_le_bytes(out[..4].try_into().unwrap()),
size * 8,
"the fixture must reach the full-precision fallback"
);
u64::from_le_bytes(out[5..13].try_into().unwrap())
};
let i8_raw: Vec<u8> = [-128i8, 126, -128, 126, -1, -2, -3, -4]
.iter()
.map(|&v| v as u8)
.collect();
assert_eq!(minval_of(&i8_raw, 1, true), (-128i64) as u64);
let u8_raw: Vec<u8> = vec![1, 255, 1, 255, 2, 3, 4, 5];
assert_eq!(minval_of(&u8_raw, 1, false), 0);
let i16_raw: Vec<u8> = [-32768i16, 32766, -32768, 32766, -1, -2, -3, -4]
.iter()
.flat_map(|v| v.to_le_bytes())
.collect();
assert_eq!(
minval_of(&i16_raw, 2, true),
0,
"signed, but two bytes wide"
);
let mut defined = int_filter(1, true, ORDER_LE, i8_raw.len() as u32);
defined.client_data[PARM_FILAVAIL] = FILL_DEFINED;
let out = compress(&i8_raw, &defined).unwrap();
assert_eq!(u64::from_le_bytes(out[5..13].try_into().unwrap()), 0);
}
#[test]
fn a_fill_value_round_trips_through_the_filter_parameters() {
for (size, bits) in [
(1u32, 0xA5u64),
(2, 0xBEEF),
(4, 0xDEAD_BEEF),
(8, 0x0123_4567_89AB_CDEF),
] {
for order in [ORDER_LE, ORDER_BE] {
let ty = ScaleOffsetType {
class: CLS_INTEGER,
sign: SGN_NONE,
order,
};
let mut bytes = Vec::new();
write_value(&mut bytes, bits, size as usize, order);
let cd = build_cd_values(
ScaleOffset::Integer(0),
ty,
size,
4,
ScaleOffsetFill::Defined(Some(&bytes)),
)
.unwrap();
assert_eq!(cd[PARM_FILAVAIL], FILL_DEFINED);
assert_eq!(
read_fill_bits(&cd, size as usize).unwrap(),
bits,
"size {size}, order {order}"
);
let used = (size as usize).div_ceil(4);
assert!(cd[PARM_FILVAL + used..].iter().all(|&e| e == 0));
}
}
let ty = ScaleOffsetType {
class: CLS_INTEGER,
sign: SGN_NONE,
order: ORDER_LE,
};
let cd = build_cd_values(
ScaleOffset::Integer(0),
ty,
4,
4,
ScaleOffsetFill::Defined(None),
)
.unwrap();
assert_eq!(cd[PARM_FILAVAIL], FILL_DEFINED);
assert_eq!(read_fill_bits(&cd, 4).unwrap(), 0);
for width in [0u32, 9, 16] {
assert!(
build_cd_values(
ScaleOffset::Integer(0),
ty,
width,
4,
ScaleOffsetFill::Defined(Some(&vec![0u8; width as usize]))
)
.is_err(),
"a {width}-byte datatype"
);
}
for bad in [vec![1u8], vec![1u8; 8]] {
assert!(
build_cd_values(
ScaleOffset::Integer(0),
ty,
4,
4,
ScaleOffsetFill::Defined(Some(&bad))
)
.is_err(),
"a {}-byte fill value for a 4-byte datatype",
bad.len()
);
}
}
#[test]
fn rounding_is_llround_and_not_the_rounded_sum() {
for (x, want) in [
(0.5f64, 1i64),
(-0.5, -1),
(1.5, 2),
(-1.5, -2),
(2.5, 3),
(-2.5, -3),
(0.0, 0),
(0.4, 0),
(-0.4, 0),
(7.0, 7),
] {
assert_eq!(round_half_away_f64(x), want, "f64 round({x})");
}
let below_half = 0.499_999_999_999_999_94f64;
assert!(below_half < 0.5 && below_half + 0.5 == 1.0);
assert_eq!(round_half_away_f64(below_half), 0);
assert_eq!(round_half_away_f64(-below_half), 0);
let below_half_f32 = 0.499_999_97f32;
assert!(below_half_f32 < 0.5 && below_half_f32 + 0.5 == 1.0);
assert_eq!(round_half_away_f32(below_half_f32), 0);
assert_eq!(round_half_away_f32(-below_half_f32), 0);
assert_eq!(round_half_away_f32(0.5), 1);
assert_eq!(round_half_away_f32(-2.5), -3);
assert_eq!(round_half_away_f64(1e300), i64::MAX);
assert_eq!(round_half_away_f64(-1e300), i64::MIN);
assert_eq!(round_half_away_f32(1e30), i64::MAX);
assert_eq!(round_half_away_f64(f64::NAN), 0);
assert_eq!(round_half_away_f32(f32::NAN), 0);
let integral = (1u64 << 52) as f64;
assert_eq!(round_half_away_f64(integral), 1 << 52);
assert_eq!(round_half_away_f64(integral - 1.0), (1 << 52) - 1);
assert_eq!(round_half_away_f64(-integral), -(1 << 52));
}
#[test]
fn ceil_log2_matches_reference() {
assert_eq!(ceil_log2(0), 1);
assert_eq!(ceil_log2(1), 0);
assert_eq!(ceil_log2(2), 1);
assert_eq!(ceil_log2(3), 2);
assert_eq!(ceil_log2(4), 2);
assert_eq!(ceil_log2(5), 3);
assert_eq!(ceil_log2(255), 8);
assert_eq!(ceil_log2(256), 8);
assert_eq!(ceil_log2(257), 9);
}
fn roundtrip_u32(vals: &[u32], order: u32) {
let mut raw = Vec::new();
for &v in vals {
if order == ORDER_LE {
raw.extend_from_slice(&v.to_le_bytes());
} else {
raw.extend_from_slice(&v.to_be_bytes());
}
}
let f = int_filter(4, false, order, vals.len() as u32);
let comp = compress(&raw, &f).unwrap();
let dec = decompress(&comp, &f, None).unwrap();
assert_eq!(dec, raw);
}
#[test]
fn integer_unsigned_roundtrip_le_and_be() {
let vals = [100u32, 105, 101, 110, 100, 128];
roundtrip_u32(&vals, ORDER_LE);
roundtrip_u32(&vals, ORDER_BE);
}
#[test]
fn integer_signed_roundtrip_with_negatives() {
let vals: [i16; 6] = [-100, -50, -100, 0, 27, -99];
for &order in &[ORDER_LE, ORDER_BE] {
let mut raw = Vec::new();
for &v in &vals {
if order == ORDER_LE {
raw.extend_from_slice(&v.to_le_bytes());
} else {
raw.extend_from_slice(&v.to_be_bytes());
}
}
let f = int_filter(2, true, order, vals.len() as u32);
let comp = compress(&raw, &f).unwrap();
let dec = decompress(&comp, &f, None).unwrap();
assert_eq!(dec, raw, "order {order}");
}
}
#[test]
fn integer_all_equal_uses_minbits_zero() {
let vals = [7u32; 5];
let mut raw = Vec::new();
for &v in &vals {
raw.extend_from_slice(&v.to_le_bytes());
}
let f = int_filter(4, false, ORDER_LE, vals.len() as u32);
let comp = compress(&raw, &f).unwrap();
assert_eq!(comp.len(), HEADER_LEN + 1);
assert_eq!(u32::from_le_bytes([comp[0], comp[1], comp[2], comp[3]]), 0);
let dec = decompress(&comp, &f, None).unwrap();
assert_eq!(dec, raw);
}
#[test]
fn integer_full_range_uses_raw_path() {
let vals = [0u32, u32::MAX, 123];
let mut raw = Vec::new();
for &v in &vals {
raw.extend_from_slice(&v.to_le_bytes());
}
let f = int_filter(4, false, ORDER_LE, vals.len() as u32);
let comp = compress(&raw, &f).unwrap();
assert_eq!(comp.len(), HEADER_LEN + raw.len());
assert_eq!(u32::from_le_bytes([comp[0], comp[1], comp[2], comp[3]]), 32);
let dec = decompress(&comp, &f, None).unwrap();
assert_eq!(dec, raw);
}
#[test]
fn integer_u8_roundtrip() {
let raw = vec![10u8, 11, 12, 250, 10, 200];
let f = int_filter(1, false, ORDER_LE, raw.len() as u32);
let comp = compress(&raw, &f).unwrap();
let dec = decompress(&comp, &f, None).unwrap();
assert_eq!(dec, raw);
}
#[test]
fn integer_i64_roundtrip() {
let vals: [i64; 4] = [-1_000_000, 5, -999_999, 42];
let mut raw = Vec::new();
for &v in &vals {
raw.extend_from_slice(&v.to_le_bytes());
}
let f = int_filter(8, true, ORDER_LE, vals.len() as u32);
let comp = compress(&raw, &f).unwrap();
let dec = decompress(&comp, &f, None).unwrap();
assert_eq!(dec, raw);
}
#[test]
fn float_dscale_roundtrip_within_tolerance() {
let vals = [1.234f64, 1.235, 1.250, 1.111, 1.234, 1.999];
let decimals = 3;
let mut raw = Vec::new();
for &v in &vals {
raw.extend_from_slice(&v.to_le_bytes());
}
let f = float_filter(8, decimals, ORDER_LE, vals.len() as u32);
let comp = compress(&raw, &f).unwrap();
let dec = decompress(&comp, &f, None).unwrap();
let got: Vec<f64> = dec
.as_chunks::<8>()
.0
.iter()
.map(|c| f64::from_le_bytes(*c))
.collect();
let tol = 0.5 * 10f64.powi(-decimals);
for (g, v) in got.iter().zip(vals.iter()) {
assert!((g - v).abs() <= tol, "got {g}, want {v}");
}
}
#[test]
fn float32_dscale_roundtrip_be() {
let vals = [10.25f32, 10.50, 10.75, 10.00, 10.25];
let decimals = 2;
let mut raw = Vec::new();
for &v in &vals {
raw.extend_from_slice(&v.to_be_bytes());
}
let f = float_filter(4, decimals, ORDER_BE, vals.len() as u32);
let comp = compress(&raw, &f).unwrap();
let dec = decompress(&comp, &f, None).unwrap();
let got: Vec<f32> = dec
.as_chunks::<4>()
.0
.iter()
.map(|c| f32::from_be_bytes(*c))
.collect();
let tol = 0.5 * 10f32.powi(-decimals);
for (g, v) in got.iter().zip(vals.iter()) {
assert!((g - v).abs() <= tol, "got {g}, want {v}");
}
}
#[test]
fn truncated_chunk_errors_not_panics() {
let f = int_filter(4, false, ORDER_LE, 4);
let mut bad = Vec::new();
bad.extend_from_slice(&3u32.to_le_bytes()); bad.push(8); bad.extend_from_slice(&0u64.to_le_bytes()); assert!(matches!(
decompress(&bad, &f, None),
Err(FormatError::FilterError(_))
));
}
#[test]
fn header_byte_layout() {
let vals = [5u32, 9, 6, 5];
let mut raw = Vec::new();
for &v in &vals {
raw.extend_from_slice(&v.to_le_bytes());
}
let f = int_filter(4, false, ORDER_LE, vals.len() as u32);
let comp = compress(&raw, &f).unwrap();
assert_eq!(u32::from_le_bytes([comp[0], comp[1], comp[2], comp[3]]), 3);
assert_eq!(comp[4], 8); let minval = u64::from_le_bytes(comp[5..13].try_into().unwrap());
assert_eq!(minval, 5);
assert_eq!(&comp[13..21], &[0u8; 8]); }
#[test]
fn decompress_refuses_a_claim_larger_than_the_chunk() {
let mut chunk = vec![0u8; HEADER_LEN];
chunk[4] = 8;
let huge = int_filter(8, true, ORDER_LE, 1 << 28);
let err = decompress(&chunk, &huge, Some(4096)).unwrap_err();
let FormatError::FilterError(msg) = &err else {
panic!("expected a filter error, got {err}");
};
assert!(msg.contains("scaleoffset"), "{msg}");
let fits = int_filter(8, true, ORDER_LE, 512);
assert_eq!(decompress(&chunk, &fits, Some(4096)).unwrap().len(), 4096);
assert_eq!(decompress(&chunk, &fits, None).unwrap().len(), 4096);
}
#[test]
fn build_cd_values_rejects_mismatched_mode() {
let int_ty = ScaleOffsetType {
class: CLS_INTEGER,
sign: SGN_2,
order: ORDER_LE,
};
assert!(
build_cd_values(
ScaleOffset::FloatDScale(2),
int_ty,
4,
10,
ScaleOffsetFill::Undefined
)
.is_err()
);
let float_ty = ScaleOffsetType {
class: CLS_FLOAT,
sign: SGN_NONE,
order: ORDER_LE,
};
assert!(
build_cd_values(
ScaleOffset::Integer(0),
float_ty,
8,
10,
ScaleOffsetFill::Undefined
)
.is_err()
);
}
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Self(seed | 1)
}
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn range(&mut self, hi: u64) -> u64 {
self.next() % hi
}
}
#[test]
fn pack_unpack_equivalence_random() {
let mut rng = Rng::new(0x00C0_FFEE_F00D_1234);
for _ in 0..400 {
let minbits = (rng.range(64) + 1) as u32; let nelmts = (rng.range(257) + 1) as usize; let mask = if minbits == 64 {
u64::MAX
} else {
(1u64 << minbits) - 1
};
let offsets: Vec<u64> = (0..nelmts).map(|_| rng.next() & mask).collect();
let packed = pack_offsets(&offsets, minbits, nelmts).unwrap();
assert_eq!(packed.len(), nelmts * minbits as usize / 8 + 1);
let unpacked = unpack_bits(&packed, nelmts, minbits).unwrap();
assert_eq!(
unpacked, offsets,
"minbits={minbits}, nelmts={nelmts}, seed=0xC0FFEEF00D1234"
);
}
}
fn roundtrip_random<T: Copy>(
seed: u64,
size: u32,
signed: bool,
order: u32,
encode: impl Fn(&mut Rng) -> T,
to_bytes: impl Fn(T, u32) -> Vec<u8>,
) {
let mut rng = Rng::new(seed);
for trial in 0..40 {
let nelmts = (rng.range(199) + 1) as usize;
let mut raw = Vec::with_capacity(nelmts * size as usize);
for _ in 0..nelmts {
raw.extend_from_slice(&to_bytes(encode(&mut rng), order));
}
let f = int_filter(size, signed, order, nelmts as u32);
let comp = compress(&raw, &f).unwrap();
let dec = decompress(&comp, &f, None).unwrap();
assert_eq!(
dec, raw,
"trial {trial}: size={size}, signed={signed}, order={order}"
);
}
}
#[test]
fn integer_roundtrip_random_u8() {
roundtrip_random::<u8>(0x11, 1, false, ORDER_LE, |r| r.next() as u8, |v, _| vec![v]);
}
#[test]
fn integer_roundtrip_random_u16_le_and_be() {
for &order in &[ORDER_LE, ORDER_BE] {
roundtrip_random::<u16>(
0x22 ^ order as u64,
2,
false,
order,
|r| r.next() as u16,
|v, o| {
if o == ORDER_LE {
v.to_le_bytes().to_vec()
} else {
v.to_be_bytes().to_vec()
}
},
);
}
}
#[test]
fn integer_roundtrip_random_i32_narrow_and_wide() {
let mut rng = Rng::new(0x33);
for trial in 0..40 {
let nelmts = (rng.range(199) + 1) as usize;
let narrow = rng.next() & 1 == 0;
let mut raw = Vec::with_capacity(nelmts * 4);
for _ in 0..nelmts {
let v: i32 = if narrow {
(rng.range(1024) as i32) - 512 } else {
rng.next() as i32 };
raw.extend_from_slice(&v.to_le_bytes());
}
let f = int_filter(4, true, ORDER_LE, nelmts as u32);
let comp = compress(&raw, &f).unwrap();
let dec = decompress(&comp, &f, None).unwrap();
assert_eq!(dec, raw, "trial {trial}: narrow={narrow}");
}
}
#[test]
fn integer_roundtrip_random_i64() {
roundtrip_random::<i64>(
0x44,
8,
true,
ORDER_LE,
|r| {
let span = 1u64 << (r.range(48) as u32 + 1);
let v = (r.next() % span) as i64;
let off = (r.next() as i64) >> 1;
v.wrapping_add(off)
},
|v, _| v.to_le_bytes().to_vec(),
);
}
#[test]
fn float_dscale_roundtrip_random_within_tolerance() {
let mut rng = Rng::new(0x55);
for trial in 0..30 {
let nelmts = (rng.range(99) + 1) as usize;
let decimals = (rng.range(5) as i32) + 1; let mut raw = Vec::with_capacity(nelmts * 8);
let mut vals = Vec::with_capacity(nelmts);
for _ in 0..nelmts {
let v = (rng.next() as i32 as f64) * 1e-9; vals.push(v);
raw.extend_from_slice(&v.to_le_bytes());
}
let f = float_filter(8, decimals, ORDER_LE, nelmts as u32);
let comp = compress(&raw, &f).unwrap();
let dec = decompress(&comp, &f, None).unwrap();
let got: Vec<f64> = dec
.as_chunks::<8>()
.0
.iter()
.map(|c| f64::from_le_bytes(*c))
.collect();
let tol = 0.501 * 10f64.powi(-decimals);
for (g, v) in got.iter().zip(vals.iter()) {
assert!(
(g - v).abs() <= tol,
"trial {trial}: got {g}, want {v} (tol {tol})"
);
}
}
}
#[test]
fn minbits_zero_with_fill_defined_reads_as_the_fill_value() {
let nelmts = 5u32;
let size = 4u32;
let mut cd = vec![0u32; TOTAL_NPARMS];
cd[PARM_SCALETYPE] = SO_INT;
cd[PARM_SCALEFACTOR] = 0;
cd[PARM_NELMTS] = nelmts;
cd[PARM_CLASS] = CLS_INTEGER;
cd[PARM_SIZE] = size;
cd[PARM_SIGN] = SGN_NONE;
cd[PARM_ORDER] = ORDER_LE;
cd[PARM_FILAVAIL] = FILL_DEFINED;
cd[PARM_FILVAL] = 99;
let f = FilterDescription {
filter_id: crate::filter_pipeline::FILTER_SCALEOFFSET,
name: None,
flags: 0,
client_data: cd,
};
let mut chunk = Vec::with_capacity(HEADER_LEN + 1);
chunk.extend_from_slice(&0u32.to_le_bytes());
chunk.push(8);
chunk.extend_from_slice(&7u64.to_le_bytes());
chunk.extend_from_slice(&[0u8; HEADER_LEN - 13]);
chunk.push(0);
let out = decompress(&chunk, &f, None).unwrap();
let got: Vec<u32> = out
.as_chunks::<4>()
.0
.iter()
.map(|c| u32::from_le_bytes(*c))
.collect();
assert_eq!(
got,
vec![99u32; nelmts as usize],
"minbits==0 with FILL_DEFINED reads as the fill value, not minval"
);
}
#[test]
fn fill_defined_sentinel_emits_filval_not_minval() {
let nelmts = 4u32;
let size = 4u32;
let mut cd = vec![0u32; TOTAL_NPARMS];
cd[PARM_SCALETYPE] = SO_INT;
cd[PARM_SCALEFACTOR] = 0;
cd[PARM_NELMTS] = nelmts;
cd[PARM_CLASS] = CLS_INTEGER;
cd[PARM_SIZE] = size;
cd[PARM_SIGN] = SGN_NONE;
cd[PARM_ORDER] = ORDER_LE;
cd[PARM_FILAVAIL] = FILL_DEFINED;
cd[PARM_FILVAL] = 999;
let f = FilterDescription {
filter_id: crate::filter_pipeline::FILTER_SCALEOFFSET,
name: None,
flags: 0,
client_data: cd,
};
let mut chunk = Vec::new();
chunk.extend_from_slice(&3u32.to_le_bytes());
chunk.push(8);
chunk.extend_from_slice(&10u64.to_le_bytes());
chunk.extend_from_slice(&[0u8; HEADER_LEN - 13]);
chunk.extend_from_slice(&pack_offsets(&[0, 1, 7, 2], 3, 4).unwrap());
let out = decompress(&chunk, &f, None).unwrap();
let got: Vec<u32> = out
.as_chunks::<4>()
.0
.iter()
.map(|c| u32::from_le_bytes(*c))
.collect();
assert_eq!(got, vec![10u32, 11, 999, 12]);
}
fn int_filter_with_fill(
size: u32,
signed: bool,
order: u32,
nelmts: u32,
fill: u64,
) -> FilterDescription {
let mut bytes = Vec::new();
write_value(&mut bytes, fill, size as usize, order);
let mut f = int_filter(size, signed, order, nelmts);
f.client_data = build_cd_values(
ScaleOffset::Integer(0),
ScaleOffsetType {
class: CLS_INTEGER,
sign: if signed { SGN_2 } else { SGN_NONE },
order,
},
size,
nelmts,
ScaleOffsetFill::Defined(Some(&bytes)),
)
.unwrap();
f
}
#[test]
fn a_big_endian_fill_value_is_recognized() {
let fill = 0x0BADu64;
let values: Vec<u16> = vec![0x0BAD, 3, 4, 0x0BAD, 5];
for order in [ORDER_LE, ORDER_BE] {
let raw: Vec<u8> = values
.iter()
.flat_map(|v| {
if order == ORDER_LE {
v.to_le_bytes()
} else {
v.to_be_bytes()
}
})
.collect();
let f = int_filter_with_fill(2, false, order, values.len() as u32, fill);
let packed = compress(&raw, &f).unwrap();
assert_eq!(
u32::from_le_bytes(packed[..4].try_into().unwrap()),
2,
"order {order}: the fill value must be recognized, not packed"
);
assert_eq!(
decompress(&packed, &f, None).unwrap(),
raw,
"order {order}: round trip"
);
}
}
#[test]
fn compress_round_trips_with_a_defined_fill_value() {
let fill = 0xDEAD_u32;
for (label, values) in [
("mixed", vec![10u32, fill, 11, 12, fill, 10]),
("all fill", vec![fill; 6]),
("no fill", vec![10u32, 11, 12, 13, 14, 15]),
("sentinel collision", vec![10u32, 11, 12, 13, fill, 13]),
] {
for order in [ORDER_LE, ORDER_BE] {
let f = int_filter_with_fill(4, false, order, values.len() as u32, u64::from(fill));
let raw: Vec<u8> = values
.iter()
.flat_map(|v| {
let le = v.to_le_bytes();
if order == ORDER_LE {
le.to_vec()
} else {
le.iter().rev().copied().collect()
}
})
.collect();
let packed = compress(&raw, &f).unwrap();
let out = decompress(&packed, &f, None).unwrap();
assert_eq!(out, raw, "{label}, order {order}");
}
}
}
fn float_filter_with_fill(
size: u32,
decimals: i32,
order: u32,
nelmts: u32,
fill: u64,
) -> FilterDescription {
let mut f = float_filter(size, decimals, order, nelmts);
f.client_data[PARM_FILAVAIL] = FILL_DEFINED;
for (k, entry) in fill
.to_le_bytes()
.chunks(4)
.take((size as usize).div_ceil(4))
.enumerate()
{
let mut w = [0u8; 4];
w[..entry.len()].copy_from_slice(entry);
f.client_data[PARM_FILVAL + k] = u32::from_le_bytes(w);
}
f
}
#[test]
fn float_compress_round_trips_with_a_defined_fill_value() {
let decimals = 3;
for size in [4u32, 8] {
for order in [ORDER_LE, ORDER_BE] {
for (label, values) in [
("mixed", vec![1.5f64, -999.0, 2.25, -999.0, 3.125]),
("all fill", vec![-999.0f64; 5]),
("no fill", vec![1.5f64, 2.25, 3.125, 4.0, 5.5]),
(
"sentinel collision",
vec![0.0f64, 1.023, 0.5, -999.0, 1.023],
),
] {
let fill_bits = if size == 4 {
u64::from((-999.0f32).to_bits())
} else {
(-999.0f64).to_bits()
};
let n = values.len() as u32;
let f = float_filter_with_fill(size, decimals, order, n, fill_bits);
let raw: Vec<u8> = values
.iter()
.flat_map(|v| {
let le = if size == 4 {
(*v as f32).to_bits().to_le_bytes().to_vec()
} else {
v.to_bits().to_le_bytes().to_vec()
};
if order == ORDER_LE {
le
} else {
le.iter().rev().copied().collect()
}
})
.collect();
let packed = compress(&raw, &f).unwrap();
let out = decompress(&packed, &f, None).unwrap();
let got: Vec<f64> = out
.chunks_exact(size as usize)
.map(|c| {
let mut b = c.to_vec();
if order == ORDER_BE {
b.reverse();
}
if size == 4 {
f64::from(f32::from_bits(u32::from_le_bytes(b.try_into().unwrap())))
} else {
f64::from_bits(u64::from_le_bytes(b.try_into().unwrap()))
}
})
.collect();
let tol = 0.501 * 10f64.powi(-decimals);
assert_eq!(
got.len(),
values.len(),
"{label}, size {size}, order {order}"
);
for (g, v) in got.iter().zip(values.iter()) {
assert!(
(g - v).abs() <= tol,
"{label}, size {size}, order {order}: got {g}, want {v}"
);
}
}
}
}
}
#[test]
fn a_full_width_scale_factor_passes_the_chunk_through_both_ways() {
let raw: Vec<u8> = (0..40u8).collect();
for size in [1u32, 2, 4, 8] {
let bits = size * 8;
let nelmts = raw.len() as u32 / size;
let mut f = int_filter(size, false, ORDER_LE, nelmts);
f.client_data[PARM_SCALEFACTOR] = bits;
assert_eq!(compress(&raw, &f).unwrap(), raw, "size {size}: compress");
assert_eq!(
decompress(&raw, &f, None).unwrap(),
raw,
"size {size}: decompress"
);
f.client_data[PARM_SCALEFACTOR] = bits + 1;
assert!(
matches!(compress(&raw, &f), Err(FormatError::FilterError(_))),
"size {size}: compress must refuse a width past the datatype"
);
assert!(
matches!(decompress(&raw, &f, None), Err(FormatError::FilterError(_))),
"size {size}: decompress must refuse it too"
);
}
}
#[test]
fn a_pass_through_width_does_not_skip_the_class_check() {
let raw = vec![0u8; 32];
let mut f = float_filter(4, 0, ORDER_LE, 8);
f.client_data[PARM_SCALETYPE] = SO_INT;
f.client_data[PARM_SCALEFACTOR] = 32;
assert!(matches!(
compress(&raw, &f),
Err(FormatError::FilterError(_))
));
let mut f = int_filter(4, false, ORDER_LE, 8);
f.client_data[PARM_SCALETYPE] = SO_FLOAT_ESCALE;
f.client_data[PARM_SCALEFACTOR] = 32;
assert!(matches!(
compress(&raw, &f),
Err(FormatError::FilterError(_))
));
}
#[test]
fn the_second_full_precision_return_carries_the_chunk_minimum() {
let values: Vec<u8> = vec![9, 1, 128, 9, 1, 128, 2, 3];
let f = int_filter_with_fill(1, false, ORDER_LE, values.len() as u32, 9);
let packed = compress(&values, &f).unwrap();
assert_eq!(
u32::from_le_bytes(packed[..4].try_into().unwrap()),
8,
"the fixture must reach the full width"
);
assert_eq!(
u64::from_le_bytes(packed[5..13].try_into().unwrap()),
1,
"the second full-precision return carries the chunk minimum, not zero"
);
assert_eq!(&packed[HEADER_LEN..], &values[..]);
}
#[test]
fn a_defined_fill_value_reserves_exactly_one_code_point() {
let fill = 0xDEAD_u32;
for span in 1..=40u32 {
let mut values: Vec<u32> = (0..span).collect();
values.push(fill);
let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
let n = values.len() as u32;
let minbits_of = |f: &FilterDescription| {
let packed = compress(&raw, f).unwrap();
u32::from_le_bytes(packed[..4].try_into().unwrap())
};
let plain: Vec<u8> = (0..span).flat_map(|v| v.to_le_bytes()).collect();
let packed = compress(&plain, &int_filter(4, false, ORDER_LE, span)).unwrap();
let plain_minbits = u32::from_le_bytes(packed[..4].try_into().unwrap());
assert_eq!(plain_minbits, ceil_log2(u64::from(span)), "span {span}");
assert_eq!(
minbits_of(&int_filter_with_fill(
4,
false,
ORDER_LE,
n,
u64::from(fill)
)),
ceil_log2(u64::from(span) + 1),
"span {span}: a defined fill value must reserve one code point, \
no more and no less"
);
}
}
#[test]
fn decompress_rejects_escale() {
let mut cd = vec![0u32; TOTAL_NPARMS];
cd[PARM_SCALETYPE] = SO_FLOAT_ESCALE;
cd[PARM_NELMTS] = 4;
cd[PARM_CLASS] = CLS_FLOAT;
cd[PARM_SIZE] = 4;
cd[PARM_ORDER] = ORDER_LE;
let f = FilterDescription {
filter_id: crate::filter_pipeline::FILTER_SCALEOFFSET,
name: None,
flags: 0,
client_data: cd,
};
let chunk = vec![0u8; HEADER_LEN + 4];
assert!(matches!(
decompress(&chunk, &f, None),
Err(FormatError::FilterError(_))
));
}
#[test]
fn decompress_rejects_oversized_minbits_header() {
let f = int_filter(4, false, ORDER_LE, 4);
let mut bad = Vec::new();
bad.extend_from_slice(&33u32.to_le_bytes());
bad.push(8);
bad.extend_from_slice(&0u64.to_le_bytes());
bad.extend_from_slice(&[0u8; HEADER_LEN - 13]);
bad.extend_from_slice(&[0u8; 16]); assert!(matches!(
decompress(&bad, &f, None),
Err(FormatError::FilterError(_))
));
}
#[test]
fn integer_i8_roundtrip_with_negatives() {
let vals: [i8; 6] = [-100, -50, 0, 27, -99, 100];
let raw: Vec<u8> = vals.iter().map(|&v| v as u8).collect();
let f = int_filter(1, true, ORDER_LE, vals.len() as u32);
let comp = compress(&raw, &f).unwrap();
let dec = decompress(&comp, &f, None).unwrap();
assert_eq!(dec, raw);
}
#[test]
fn single_element_chunk_roundtrip() {
let raw = 42u32.to_le_bytes().to_vec();
let f = int_filter(4, false, ORDER_LE, 1);
let comp = compress(&raw, &f).unwrap();
let dec = decompress(&comp, &f, None).unwrap();
assert_eq!(dec, raw);
let raw = 3.14f64.to_le_bytes().to_vec();
let f = float_filter(8, 3, ORDER_LE, 1);
let comp = compress(&raw, &f).unwrap();
let dec = decompress(&comp, &f, None).unwrap();
let got = f64::from_le_bytes(dec.as_slice().try_into().unwrap());
assert!((got - 3.14).abs() <= 0.5e-3);
}
#[test]
fn scale_offset_type_from_datatype_classes() {
let i32_ty = Datatype::FixedPoint {
size: 4,
byte_order: DatatypeByteOrder::LittleEndian,
signed: true,
bit_offset: 0,
bit_precision: 32,
};
let so = scale_offset_type_from_datatype(&i32_ty).unwrap();
assert_eq!(so.class, CLS_INTEGER);
assert_eq!(so.sign, SGN_2);
assert_eq!(so.order, ORDER_LE);
let f64_ty = Datatype::FloatingPoint {
size: 8,
byte_order: DatatypeByteOrder::BigEndian,
bit_offset: 0,
bit_precision: 64,
exponent_location: 52,
exponent_size: 11,
mantissa_location: 0,
mantissa_size: 52,
exponent_bias: 1023,
};
let so = scale_offset_type_from_datatype(&f64_ty).unwrap();
assert_eq!(so.class, CLS_FLOAT);
assert_eq!(so.order, ORDER_BE);
}
}