use crate::binary::{decode_wmo_i16, decode_wmo_i8};
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum DataRepresentation {
SimplePacking(ScaledPackingParams),
Grib1SecondOrderPacking(Grib1SecondOrderPackingParams),
ComplexPacking(ComplexPackingParams),
IeeePacking(IeeePackingParams),
Jpeg2000Packing(Jpeg2000PackingParams),
PngPacking(PngPackingParams),
CcsdsPacking(CcsdsPackingParams),
RunLengthPacking(RunLengthPackingParams),
Unsupported(u16),
}
#[derive(Debug, Clone, PartialEq)]
pub struct ScaledPackingParams {
pub encoded_values: usize,
pub reference_value: f32,
pub binary_scale: i16,
pub decimal_scale: i16,
pub bits_per_value: u8,
pub original_field_type: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Grib1SecondOrderLayout {
RowByRow,
ConstantWidth,
VariableWidth,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Grib1SecondOrderPackingParams {
pub(crate) packing: ScaledPackingParams,
pub(crate) layout: Grib1SecondOrderLayout,
pub(crate) group_count: usize,
pub(crate) group_widths: Vec<u8>,
pub(crate) group_lengths: Vec<usize>,
pub(crate) first_order_values_offset: usize,
pub(crate) second_order_values_offset: usize,
pub(crate) unused_bits: u8,
}
impl Grib1SecondOrderPackingParams {
pub const fn packing(&self) -> &ScaledPackingParams {
&self.packing
}
pub const fn layout(&self) -> Grib1SecondOrderLayout {
self.layout
}
pub const fn group_count(&self) -> usize {
self.group_count
}
pub fn group_widths(&self) -> &[u8] {
&self.group_widths
}
pub fn group_lengths(&self) -> &[usize] {
&self.group_lengths
}
pub const fn first_order_values_offset(&self) -> usize {
self.first_order_values_offset
}
pub const fn second_order_values_offset(&self) -> usize {
self.second_order_values_offset
}
pub const fn unused_bits(&self) -> u8 {
self.unused_bits
}
pub fn group_width(&self, group_index: usize) -> Option<u8> {
match self.layout {
Grib1SecondOrderLayout::ConstantWidth => self.group_widths.first().copied(),
Grib1SecondOrderLayout::RowByRow | Grib1SecondOrderLayout::VariableWidth => {
self.group_widths.get(group_index).copied()
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum IeeePrecision {
Single,
Double,
Quadruple,
}
impl IeeePrecision {
pub const fn code(self) -> u8 {
match self {
Self::Single => 1,
Self::Double => 2,
Self::Quadruple => 3,
}
}
pub const fn bytes_per_value(self) -> usize {
match self {
Self::Single => 4,
Self::Double => 8,
Self::Quadruple => 16,
}
}
pub const fn bits_per_value(self) -> u8 {
match self {
Self::Single => 32,
Self::Double => 64,
Self::Quadruple => 128,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IeeePackingParams {
pub encoded_values: usize,
pub precision: IeeePrecision,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RunLengthPackingParams {
pub encoded_values: usize,
pub bits_per_value: u8,
pub max_level_value: u16,
pub decimal_scale: i16,
pub level_values: Vec<u16>,
}
impl RunLengthPackingParams {
pub fn representative_value(&self, level: u16) -> Option<f64> {
if level == 0 || level > self.max_level_value {
return None;
}
let scaled = *self.level_values.get(usize::from(level - 1))?;
Some(f64::from(scaled) * 10.0_f64.powi(-i32::from(self.decimal_scale)))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Jpeg2000PackingParams {
pub packing: ScaledPackingParams,
pub compression_type: u8,
pub target_compression_ratio: u8,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PngPackingParams {
pub packing: ScaledPackingParams,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CcsdsFlags(u8);
impl CcsdsFlags {
pub const NONE: Self = Self(0);
pub const SIGNED: Self = Self(1 << 0);
pub const THREE_BYTE: Self = Self(1 << 1);
pub const MSB: Self = Self(1 << 2);
pub const PREPROCESS: Self = Self(1 << 3);
pub const RESTRICTED: Self = Self(1 << 4);
pub const PAD_RSI: Self = Self(1 << 5);
pub const NOT_ENFORCE: Self = Self(1 << 6);
const KNOWN_BITS: u8 = Self::SIGNED.0
| Self::THREE_BYTE.0
| Self::MSB.0
| Self::PREPROCESS.0
| Self::RESTRICTED.0
| Self::PAD_RSI.0
| Self::NOT_ENFORCE.0;
pub const DEFAULT: Self = Self(Self::THREE_BYTE.0 | Self::MSB.0 | Self::PREPROCESS.0);
pub const fn from_bits(bits: u8) -> Option<Self> {
if bits & !Self::KNOWN_BITS == 0 {
Some(Self(bits))
} else {
None
}
}
pub const fn bits(self) -> u8 {
self.0
}
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
}
impl Default for CcsdsFlags {
fn default() -> Self {
Self::DEFAULT
}
}
impl std::ops::BitOr for CcsdsFlags {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for CcsdsFlags {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CcsdsBlockSize(u8);
impl CcsdsBlockSize {
pub const EIGHT: Self = Self(8);
pub const SIXTEEN: Self = Self(16);
pub const THIRTY_TWO: Self = Self(32);
pub const SIXTY_FOUR: Self = Self(64);
pub const fn new(samples: u8) -> Option<Self> {
if samples != 0 && samples.is_multiple_of(2) {
Some(Self(samples))
} else {
None
}
}
pub const fn samples(self) -> u8 {
self.0
}
pub const fn is_standard(self) -> bool {
matches!(self.0, 8 | 16 | 32 | 64)
}
}
impl Default for CcsdsBlockSize {
fn default() -> Self {
Self::THIRTY_TWO
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CcsdsPackingParams {
packing: ScaledPackingParams,
flags: CcsdsFlags,
block_size: CcsdsBlockSize,
reference_sample_interval: u16,
}
impl CcsdsPackingParams {
pub fn new(
packing: ScaledPackingParams,
flags: CcsdsFlags,
block_size: CcsdsBlockSize,
reference_sample_interval: u16,
) -> Result<Self> {
validate_ccsds_parameters(
packing.bits_per_value,
flags,
block_size,
reference_sample_interval,
)
.map_err(Error::ValueOutOfRange)?;
Ok(Self {
packing,
flags,
block_size,
reference_sample_interval,
})
}
pub const fn packing(&self) -> &ScaledPackingParams {
&self.packing
}
pub const fn flags(&self) -> CcsdsFlags {
self.flags
}
pub const fn block_size(&self) -> CcsdsBlockSize {
self.block_size
}
pub const fn reference_sample_interval(&self) -> u16 {
self.reference_sample_interval
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ComplexPackingParams {
pub encoded_values: usize,
pub reference_value: f32,
pub binary_scale: i16,
pub decimal_scale: i16,
pub group_reference_bits: u8,
pub original_field_type: u8,
pub group_splitting_method: u8,
pub missing_value_management: u8,
pub primary_missing_substitute: u32,
pub secondary_missing_substitute: u32,
pub num_groups: usize,
pub group_width_reference: u8,
pub group_width_bits: u8,
pub group_length_reference: u32,
pub group_length_increment: u8,
pub true_length_last_group: u32,
pub scaled_group_length_bits: u8,
pub spatial_differencing: Option<SpatialDifferencingParams>,
}
impl ComplexPackingParams {
pub const fn primary_missing_substitute(&self) -> Option<u32> {
match self.missing_value_management {
1 | 2 => Some(self.primary_missing_substitute),
_ => None,
}
}
pub const fn secondary_missing_substitute(&self) -> Option<u32> {
match self.missing_value_management {
2 => Some(self.secondary_missing_substitute),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SpatialDifferencingParams {
pub order: u8,
pub descriptor_octets: u8,
}
impl DataRepresentation {
pub const fn template_number(&self) -> u16 {
match self {
Self::SimplePacking(_) => 0,
Self::Grib1SecondOrderPacking(_) => 1005,
Self::ComplexPacking(params) => {
if params.spatial_differencing.is_some() {
3
} else {
2
}
}
Self::IeeePacking(_) => 4,
Self::Jpeg2000Packing(_) => 40,
Self::PngPacking(_) => 41,
Self::CcsdsPacking(_) => 42,
Self::RunLengthPacking(_) => 200,
Self::Unsupported(template) => *template,
}
}
pub fn parse(section_bytes: &[u8]) -> Result<Self> {
if section_bytes.len() < 11 {
return Err(Error::InvalidSection {
section: 5,
reason: format!("expected at least 11 bytes, got {}", section_bytes.len()),
});
}
if section_bytes[4] != 5 {
return Err(Error::InvalidSection {
section: section_bytes[4],
reason: "not a data representation section".into(),
});
}
let template = u16::from_be_bytes(section_bytes[9..11].try_into().unwrap());
match template {
0 => parse_simple_packing(section_bytes),
2 => parse_complex_packing(section_bytes, false),
3 => parse_complex_packing(section_bytes, true),
4 => parse_ieee_packing(section_bytes),
40 => parse_jpeg2000_packing(section_bytes),
41 => parse_png_packing(section_bytes),
42 => parse_ccsds_packing(section_bytes),
200 => parse_run_length_packing(section_bytes),
_ => Ok(Self::Unsupported(template)),
}
}
pub fn encoded_values(&self) -> Option<usize> {
match self {
Self::SimplePacking(params) => Some(params.encoded_values),
Self::Grib1SecondOrderPacking(params) => Some(params.packing.encoded_values),
Self::ComplexPacking(params) => Some(params.encoded_values),
Self::IeeePacking(params) => Some(params.encoded_values),
Self::Jpeg2000Packing(params) => Some(params.packing.encoded_values),
Self::PngPacking(params) => Some(params.packing.encoded_values),
Self::CcsdsPacking(params) => Some(params.packing().encoded_values),
Self::RunLengthPacking(params) => Some(params.encoded_values),
Self::Unsupported(_) => None,
}
}
}
fn parse_simple_packing(data: &[u8]) -> Result<DataRepresentation> {
Ok(DataRepresentation::SimplePacking(
parse_scaled_packing_base(data, 0, 21)?,
))
}
fn parse_ieee_packing(data: &[u8]) -> Result<DataRepresentation> {
if data.len() < 12 {
return Err(Error::InvalidSection {
section: 5,
reason: format!("template 5.4 requires 12 bytes, got {}", data.len()),
});
}
let precision = match data[11] {
1 => IeeePrecision::Single,
2 => IeeePrecision::Double,
3 => IeeePrecision::Quadruple,
code => {
return Err(Error::InvalidSection {
section: 5,
reason: format!("template 5.4 has invalid IEEE precision code {code}"),
});
}
};
Ok(DataRepresentation::IeeePacking(IeeePackingParams {
encoded_values: u32::from_be_bytes(data[5..9].try_into().unwrap()) as usize,
precision,
}))
}
fn parse_run_length_packing(data: &[u8]) -> Result<DataRepresentation> {
if data.len() < 17 {
return Err(Error::InvalidSection {
section: 5,
reason: format!(
"template 5.200 requires at least 17 bytes, got {}",
data.len()
),
});
}
let bits_per_value = data[11];
let max_level_value = u16::from_be_bytes(data[12..14].try_into().unwrap());
let number_of_level_values = usize::from(u16::from_be_bytes(data[14..16].try_into().unwrap()));
let required = number_of_level_values
.checked_mul(2)
.and_then(|level_bytes| 17_usize.checked_add(level_bytes))
.ok_or(Error::ArithmeticOverflow {
operation: "computing template 5.200 length",
})?;
if data.len() < required {
return Err(Error::InvalidSection {
section: 5,
reason: format!(
"template 5.200 declares {number_of_level_values} level values and requires {required} bytes, got {}",
data.len()
),
});
}
validate_run_length_parameters(bits_per_value, max_level_value, number_of_level_values)
.map_err(|reason| Error::InvalidSection {
section: 5,
reason: format!("invalid template 5.200 parameters: {reason}"),
})?;
let mut level_values = Vec::new();
level_values
.try_reserve_exact(number_of_level_values)
.map_err(|error| {
Error::allocation("template 5.200 level values", number_of_level_values, error)
})?;
for bytes in data[17..required].chunks_exact(2) {
level_values.push(u16::from_be_bytes(
bytes.try_into().expect("two-byte level value"),
));
}
Ok(DataRepresentation::RunLengthPacking(
RunLengthPackingParams {
encoded_values: u32::from_be_bytes(data[5..9].try_into().unwrap()) as usize,
bits_per_value,
max_level_value,
decimal_scale: decode_wmo_i8(data[16]),
level_values,
},
))
}
fn validate_run_length_parameters(
bits_per_value: u8,
max_level_value: u16,
number_of_level_values: usize,
) -> std::result::Result<(), String> {
if bits_per_value > 64 {
return Err(format!(
"bits per value must not exceed 64, got {bits_per_value}"
));
}
if max_level_value == 0 {
if number_of_level_values != 0 {
return Err(format!(
"zero maximum level requires an empty level table, got {number_of_level_values} entries"
));
}
return Ok(());
}
if bits_per_value == 0 {
return Err(format!(
"bits per value must be between 1 and 64, got {bits_per_value}"
));
}
if usize::from(max_level_value) > number_of_level_values {
return Err(format!(
"maximum level {max_level_value} exceeds the {number_of_level_values}-entry level table"
));
}
let max_code = if bits_per_value == 64 {
u64::MAX
} else {
(1_u64 << bits_per_value) - 1
};
if u64::from(max_level_value) >= max_code {
return Err(format!(
"maximum level {max_level_value} leaves no run-length codes at {bits_per_value} bits"
));
}
Ok(())
}
fn parse_scaled_packing_base(
data: &[u8],
template: u16,
required: usize,
) -> Result<ScaledPackingParams> {
if data.len() < required {
return Err(Error::InvalidSection {
section: 5,
reason: format!(
"template 5.{template} requires {required} bytes, got {}",
data.len()
),
});
}
Ok(ScaledPackingParams {
encoded_values: u32::from_be_bytes(data[5..9].try_into().unwrap()) as usize,
reference_value: f32::from_be_bytes(data[11..15].try_into().unwrap()),
binary_scale: decode_wmo_i16(&data[15..17]).unwrap(),
decimal_scale: decode_wmo_i16(&data[17..19]).unwrap(),
bits_per_value: data[19],
original_field_type: data[20],
})
}
fn parse_jpeg2000_packing(data: &[u8]) -> Result<DataRepresentation> {
Ok(DataRepresentation::Jpeg2000Packing(Jpeg2000PackingParams {
packing: parse_scaled_packing_base(data, 40, 23)?,
compression_type: data[21],
target_compression_ratio: data[22],
}))
}
fn parse_png_packing(data: &[u8]) -> Result<DataRepresentation> {
Ok(DataRepresentation::PngPacking(PngPackingParams {
packing: parse_scaled_packing_base(data, 41, 21)?,
}))
}
fn parse_ccsds_packing(data: &[u8]) -> Result<DataRepresentation> {
let packing = parse_scaled_packing_base(data, 42, 25)?;
let flags = CcsdsFlags::from_bits(data[21]).ok_or_else(|| Error::InvalidSection {
section: 5,
reason: format!(
"template 5.42 has reserved CCSDS flag bits set: 0x{:02x}",
data[21]
),
})?;
let block_size = CcsdsBlockSize::new(data[22]).ok_or_else(|| Error::InvalidSection {
section: 5,
reason: format!(
"template 5.42 block size must be a non-zero even value, got {}",
data[22]
),
})?;
let reference_sample_interval = u16::from_be_bytes(data[23..25].try_into().unwrap());
validate_ccsds_parameters(
packing.bits_per_value,
flags,
block_size,
reference_sample_interval,
)
.map_err(|reason| Error::InvalidSection {
section: 5,
reason: format!("invalid template 5.42 parameters: {reason}"),
})?;
Ok(DataRepresentation::CcsdsPacking(CcsdsPackingParams {
packing,
flags,
block_size,
reference_sample_interval,
}))
}
fn validate_ccsds_parameters(
bits_per_value: u8,
flags: CcsdsFlags,
block_size: CcsdsBlockSize,
reference_sample_interval: u16,
) -> std::result::Result<(), String> {
if bits_per_value > 32 {
return Err(format!(
"CCSDS bits per value must be at most 32, got {bits_per_value}"
));
}
if !block_size.is_standard() && !flags.contains(CcsdsFlags::NOT_ENFORCE) {
return Err(format!(
"CCSDS block size {} requires the NOT_ENFORCE flag",
block_size.samples()
));
}
if !(1..=4096).contains(&reference_sample_interval) {
return Err(format!(
"CCSDS reference sample interval must be between 1 and 4096, got {reference_sample_interval}"
));
}
if flags.contains(CcsdsFlags::RESTRICTED) && bits_per_value > 4 {
return Err(format!(
"CCSDS RESTRICTED flag requires at most 4 bits per value, got {bits_per_value}"
));
}
Ok(())
}
fn parse_complex_packing(
data: &[u8],
with_spatial_differencing: bool,
) -> Result<DataRepresentation> {
let required = if with_spatial_differencing { 49 } else { 47 };
if data.len() < required {
return Err(Error::InvalidSection {
section: 5,
reason: format!(
"template 5.{} requires {required} bytes, got {}",
if with_spatial_differencing { 3 } else { 2 },
data.len()
),
});
}
let group_splitting_method = data[21];
if !matches!(group_splitting_method, 0 | 1) {
return Err(Error::UnsupportedGroupSplittingMethod(
group_splitting_method,
));
}
let missing_value_management = data[22];
if missing_value_management > 2 {
return Err(Error::UnsupportedMissingValueManagement(
missing_value_management,
));
}
let spatial_differencing = if with_spatial_differencing {
let order = data[47];
if !matches!(order, 1 | 2) {
return Err(Error::UnsupportedSpatialDifferencingOrder(order));
}
Some(SpatialDifferencingParams {
order,
descriptor_octets: data[48],
})
} else {
None
};
let group_length_reference = u32::from_be_bytes(data[37..41].try_into().unwrap());
let group_length_increment = data[41];
let true_length_last_group = u32::from_be_bytes(data[42..46].try_into().unwrap());
let scaled_group_length_bits = data[46];
if group_splitting_method == 0
&& (group_length_reference != 0
|| group_length_increment != 0
|| true_length_last_group != 0
|| scaled_group_length_bits != 0)
{
return Err(Error::InvalidSection {
section: 5,
reason: "complex packing group splitting method 0 requires all group-length fields to be zero"
.into(),
});
}
Ok(DataRepresentation::ComplexPacking(ComplexPackingParams {
encoded_values: u32::from_be_bytes(data[5..9].try_into().unwrap()) as usize,
reference_value: f32::from_be_bytes(data[11..15].try_into().unwrap()),
binary_scale: decode_wmo_i16(&data[15..17]).unwrap(),
decimal_scale: decode_wmo_i16(&data[17..19]).unwrap(),
group_reference_bits: data[19],
original_field_type: data[20],
group_splitting_method,
missing_value_management,
primary_missing_substitute: u32::from_be_bytes(data[23..27].try_into().unwrap()),
secondary_missing_substitute: u32::from_be_bytes(data[27..31].try_into().unwrap()),
num_groups: u32::from_be_bytes(data[31..35].try_into().unwrap()) as usize,
group_width_reference: data[35],
group_width_bits: data[36],
group_length_reference,
group_length_increment,
true_length_last_group,
scaled_group_length_bits,
spatial_differencing,
}))
}
#[cfg(test)]
mod tests {
use super::{
CcsdsBlockSize, CcsdsFlags, CcsdsPackingParams, ComplexPackingParams, DataRepresentation,
IeeePackingParams, IeeePrecision, Jpeg2000PackingParams, PngPackingParams,
RunLengthPackingParams, ScaledPackingParams, SpatialDifferencingParams,
};
use crate::binary::{encode_wmo_i16, encode_wmo_i8};
#[test]
fn parses_simple_packing_template() {
let mut section = vec![0u8; 21];
section[..4].copy_from_slice(&(21u32).to_be_bytes());
section[4] = 5;
section[5..9].copy_from_slice(&3u32.to_be_bytes());
section[9..11].copy_from_slice(&0u16.to_be_bytes());
section[11..15].copy_from_slice(&10.0f32.to_be_bytes());
section[15..17].copy_from_slice(&2i16.to_be_bytes());
section[17..19].copy_from_slice(&1i16.to_be_bytes());
section[19] = 8;
section[20] = 0;
let representation = DataRepresentation::parse(§ion).unwrap();
assert_eq!(representation.template_number(), 0);
assert_eq!(
representation,
DataRepresentation::SimplePacking(ScaledPackingParams {
encoded_values: 3,
reference_value: 10.0,
binary_scale: 2,
decimal_scale: 1,
bits_per_value: 8,
original_field_type: 0,
})
);
}
#[test]
fn parses_all_ieee_packing_precisions() {
for (code, precision) in [
(1, IeeePrecision::Single),
(2, IeeePrecision::Double),
(3, IeeePrecision::Quadruple),
] {
let representation = DataRepresentation::parse(&ieee_section(code)).unwrap();
assert_eq!(representation.template_number(), 4);
assert_eq!(
representation,
DataRepresentation::IeeePacking(IeeePackingParams {
encoded_values: 6,
precision,
})
);
assert_eq!(representation.encoded_values(), Some(6));
}
}
#[test]
fn rejects_invalid_or_truncated_ieee_packing_parameters() {
for code in [0, 4, 255] {
let error = DataRepresentation::parse(&ieee_section(code)).unwrap_err();
assert!(error.to_string().contains("invalid IEEE precision code"));
}
let error = DataRepresentation::parse(&ieee_section(1)[..11]).unwrap_err();
assert!(error.to_string().contains("requires 12 bytes"));
}
#[test]
fn parses_run_length_packing_template() {
let section = run_length_section(3, 2, -1, &[10, 20, 30]);
let representation = DataRepresentation::parse(§ion).unwrap();
assert_eq!(representation.template_number(), 200);
assert_eq!(
representation,
DataRepresentation::RunLengthPacking(RunLengthPackingParams {
encoded_values: 9,
bits_per_value: 3,
max_level_value: 2,
decimal_scale: -1,
level_values: vec![10, 20, 30],
})
);
let DataRepresentation::RunLengthPacking(params) = representation else {
unreachable!();
};
assert_eq!(params.representative_value(0), None);
assert_eq!(params.representative_value(1), Some(100.0));
assert_eq!(params.representative_value(2), Some(200.0));
assert_eq!(params.representative_value(3), None);
}
#[test]
fn parses_all_missing_run_length_parameters() {
let representation = DataRepresentation::parse(&run_length_section(0, 0, 0, &[])).unwrap();
assert_eq!(
representation,
DataRepresentation::RunLengthPacking(RunLengthPackingParams {
encoded_values: 9,
bits_per_value: 0,
max_level_value: 0,
decimal_scale: 0,
level_values: Vec::new(),
})
);
}
#[test]
fn rejects_invalid_run_length_parameters() {
for (section, reason) in [
(
run_length_section(3, 3, 0, &[10, 20]),
"exceeds the 2-entry level table",
),
(
run_length_section(2, 3, 0, &[10, 20, 30]),
"leaves no run-length codes",
),
(
run_length_section(0, 1, 0, &[10]),
"bits per value must be between 1 and 64",
),
(
run_length_section(65, 1, 0, &[10]),
"bits per value must not exceed 64",
),
(
run_length_section(65, 0, 0, &[]),
"bits per value must not exceed 64",
),
(
run_length_section(3, 0, 0, &[10]),
"zero maximum level requires an empty level table",
),
] {
let error = DataRepresentation::parse(§ion).unwrap_err();
assert!(error.to_string().contains(reason), "{error}");
}
let section = run_length_section(3, 2, 0, &[10, 20]);
let error = DataRepresentation::parse(§ion[..section.len() - 1]).unwrap_err();
assert!(error.to_string().contains("requires 21 bytes"));
}
#[test]
fn parses_jpeg2000_packing_template() {
let mut section = vec![0u8; 23];
section[..4].copy_from_slice(&(23u32).to_be_bytes());
section[4] = 5;
section[5..9].copy_from_slice(&4u32.to_be_bytes());
section[9..11].copy_from_slice(&40u16.to_be_bytes());
section[11..15].copy_from_slice(&3.5f32.to_be_bytes());
section[15..17].copy_from_slice(&encode_wmo_i16(-1).unwrap());
section[17..19].copy_from_slice(&2i16.to_be_bytes());
section[19] = 12;
section[20] = 0;
section[21] = 1;
section[22] = 255;
let representation = DataRepresentation::parse(§ion).unwrap();
assert_eq!(representation.template_number(), 40);
assert_eq!(
representation,
DataRepresentation::Jpeg2000Packing(Jpeg2000PackingParams {
packing: ScaledPackingParams {
encoded_values: 4,
reference_value: 3.5,
binary_scale: -1,
decimal_scale: 2,
bits_per_value: 12,
original_field_type: 0,
},
compression_type: 1,
target_compression_ratio: 255,
})
);
}
#[test]
fn parses_png_packing_template() {
let mut section = vec![0u8; 21];
section[..4].copy_from_slice(&(21u32).to_be_bytes());
section[4] = 5;
section[5..9].copy_from_slice(&6u32.to_be_bytes());
section[9..11].copy_from_slice(&41u16.to_be_bytes());
section[11..15].copy_from_slice(&1.25f32.to_be_bytes());
section[15..17].copy_from_slice(&1i16.to_be_bytes());
section[17..19].copy_from_slice(&encode_wmo_i16(-2).unwrap());
section[19] = 16;
section[20] = 1;
let representation = DataRepresentation::parse(§ion).unwrap();
assert_eq!(representation.template_number(), 41);
assert_eq!(
representation,
DataRepresentation::PngPacking(PngPackingParams {
packing: ScaledPackingParams {
encoded_values: 6,
reference_value: 1.25,
binary_scale: 1,
decimal_scale: -2,
bits_per_value: 16,
original_field_type: 1,
},
})
);
}
#[test]
fn exposes_only_enabled_complex_missing_value_substitutes() {
let mut params = ComplexPackingParams {
encoded_values: 1,
reference_value: 0.0,
binary_scale: 0,
decimal_scale: 0,
group_reference_bits: 1,
original_field_type: 0,
group_splitting_method: 1,
missing_value_management: 0,
primary_missing_substitute: 0x1122_3344,
secondary_missing_substitute: 0x5566_7788,
num_groups: 1,
group_width_reference: 0,
group_width_bits: 0,
group_length_reference: 1,
group_length_increment: 1,
true_length_last_group: 1,
scaled_group_length_bits: 0,
spatial_differencing: None,
};
assert_eq!(params.primary_missing_substitute(), None);
assert_eq!(params.secondary_missing_substitute(), None);
params.missing_value_management = 1;
assert_eq!(params.primary_missing_substitute(), Some(0x1122_3344));
assert_eq!(params.secondary_missing_substitute(), None);
params.missing_value_management = 2;
assert_eq!(params.primary_missing_substitute(), Some(0x1122_3344));
assert_eq!(params.secondary_missing_substitute(), Some(0x5566_7788));
}
#[test]
fn reports_complex_and_unknown_template_numbers() {
let mut params = ComplexPackingParams {
encoded_values: 0,
reference_value: 0.0,
binary_scale: 0,
decimal_scale: 0,
group_reference_bits: 0,
original_field_type: 0,
group_splitting_method: 1,
missing_value_management: 0,
primary_missing_substitute: 0,
secondary_missing_substitute: 0,
num_groups: 0,
group_width_reference: 0,
group_width_bits: 0,
group_length_reference: 0,
group_length_increment: 0,
true_length_last_group: 0,
scaled_group_length_bits: 0,
spatial_differencing: None,
};
assert_eq!(
DataRepresentation::ComplexPacking(params.clone()).template_number(),
2
);
params.spatial_differencing = Some(SpatialDifferencingParams {
order: 1,
descriptor_octets: 1,
});
assert_eq!(
DataRepresentation::ComplexPacking(params).template_number(),
3
);
assert_eq!(DataRepresentation::Unsupported(500).template_number(), 500);
}
#[test]
fn parses_ccsds_packing_template() {
let section = ccsds_section(20, CcsdsFlags::DEFAULT.bits(), 32, 128);
let representation = DataRepresentation::parse(§ion).unwrap();
assert_eq!(representation.template_number(), 42);
assert_eq!(
representation,
DataRepresentation::CcsdsPacking(
CcsdsPackingParams::new(
ScaledPackingParams {
encoded_values: 6,
reference_value: 1.25,
binary_scale: -1,
decimal_scale: 2,
bits_per_value: 20,
original_field_type: 0,
},
CcsdsFlags::DEFAULT,
CcsdsBlockSize::THIRTY_TWO,
128,
)
.unwrap()
)
);
}
#[test]
fn parses_unenforced_even_ccsds_block_size() {
let flags = CcsdsFlags::DEFAULT | CcsdsFlags::NOT_ENFORCE;
let section = ccsds_section(8, flags.bits(), 12, 1);
let parsed = DataRepresentation::parse(§ion).unwrap();
let DataRepresentation::CcsdsPacking(params) = parsed else {
panic!("expected CCSDS representation");
};
assert_eq!(params.flags(), flags);
assert_eq!(params.block_size().samples(), 12);
assert_eq!(params.reference_sample_interval(), 1);
}
#[test]
fn rejects_reserved_ccsds_flag_bit() {
let error = DataRepresentation::parse(&ccsds_section(8, 0x80, 32, 128)).unwrap_err();
assert!(matches!(
error,
crate::Error::InvalidSection { section: 5, .. }
));
assert!(error.to_string().contains("reserved CCSDS flag bits"));
}
#[test]
fn rejects_nonstandard_ccsds_block_without_flag() {
let error = DataRepresentation::parse(&ccsds_section(8, 0, 12, 128)).unwrap_err();
assert!(error.to_string().contains("requires the NOT_ENFORCE flag"));
}
#[test]
fn rejects_restricted_ccsds_width_above_four_bits() {
let error =
DataRepresentation::parse(&ccsds_section(5, CcsdsFlags::RESTRICTED.bits(), 32, 128))
.unwrap_err();
assert!(error.to_string().contains("requires at most 4 bits"));
}
#[test]
fn rejects_ccsds_reference_interval_outside_codec_range() {
for interval in [0, 4097] {
let error = DataRepresentation::parse(&ccsds_section(8, 0, 32, interval)).unwrap_err();
assert!(error
.to_string()
.contains("reference sample interval must be between 1 and 4096"));
}
}
fn ccsds_section(bits_per_value: u8, flags: u8, block_size: u8, rsi: u16) -> Vec<u8> {
let mut section = vec![0u8; 25];
section[..4].copy_from_slice(&25u32.to_be_bytes());
section[4] = 5;
section[5..9].copy_from_slice(&6u32.to_be_bytes());
section[9..11].copy_from_slice(&42u16.to_be_bytes());
section[11..15].copy_from_slice(&1.25f32.to_be_bytes());
section[15..17].copy_from_slice(&encode_wmo_i16(-1).unwrap());
section[17..19].copy_from_slice(&2i16.to_be_bytes());
section[19] = bits_per_value;
section[20] = 0;
section[21] = flags;
section[22] = block_size;
section[23..25].copy_from_slice(&rsi.to_be_bytes());
section
}
fn ieee_section(precision: u8) -> Vec<u8> {
let mut section = vec![0u8; 12];
section[..4].copy_from_slice(&12u32.to_be_bytes());
section[4] = 5;
section[5..9].copy_from_slice(&6u32.to_be_bytes());
section[9..11].copy_from_slice(&4u16.to_be_bytes());
section[11] = precision;
section
}
fn run_length_section(
bits_per_value: u8,
max_level_value: u16,
decimal_scale: i16,
level_values: &[u16],
) -> Vec<u8> {
let length = 17 + 2 * level_values.len();
let mut section = vec![0u8; length];
section[..4].copy_from_slice(&(length as u32).to_be_bytes());
section[4] = 5;
section[5..9].copy_from_slice(&9u32.to_be_bytes());
section[9..11].copy_from_slice(&200u16.to_be_bytes());
section[11] = bits_per_value;
section[12..14].copy_from_slice(&max_level_value.to_be_bytes());
section[14..16].copy_from_slice(&(level_values.len() as u16).to_be_bytes());
section[16] = encode_wmo_i8(decimal_scale).unwrap();
for (bytes, level) in section[17..].chunks_exact_mut(2).zip(level_values) {
bytes.copy_from_slice(&level.to_be_bytes());
}
section
}
}