use crate::blocks::common::{read_links, BlockHeader, ParseBlock, BLOCK_HEADER_SIZE};
use crate::error::{Mf4Error, Result};
use byteorder::{LittleEndian, ReadBytesExt};
use std::io::Cursor;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaArrayType {
Array,
ScaleAxis,
Lookup,
Unknown(u8),
}
impl CaArrayType {
fn from_u8(value: u8) -> Self {
match value {
0 => CaArrayType::Array,
1 => CaArrayType::ScaleAxis,
2 => CaArrayType::Lookup,
v => CaArrayType::Unknown(v),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaStorage {
CnTemplate,
CgTemplate,
DgTemplate,
Unknown(u8),
}
impl CaStorage {
fn from_u8(value: u8) -> Self {
match value {
0 => CaStorage::CnTemplate,
1 => CaStorage::CgTemplate,
2 => CaStorage::DgTemplate,
v => CaStorage::Unknown(v),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CaFlags {
pub dynamic_size: bool,
pub input_quantity: bool,
pub output_quantity: bool,
pub comparison_quantity: bool,
pub axis: bool,
pub fixed_axis: bool,
pub inverse_layout: bool,
pub left_open_interval: bool,
pub standard_axis: bool,
}
impl CaFlags {
fn from_u32(value: u32) -> Self {
CaFlags {
dynamic_size: (value & 0x0001) != 0,
input_quantity: (value & 0x0002) != 0,
output_quantity: (value & 0x0004) != 0,
comparison_quantity: (value & 0x0008) != 0,
axis: (value & 0x0010) != 0,
fixed_axis: (value & 0x0020) != 0,
inverse_layout: (value & 0x0040) != 0,
left_open_interval: (value & 0x0080) != 0,
standard_axis: (value & 0x0100) != 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AxisRef {
pub dg: u64,
pub cg: u64,
pub cn: u64,
}
#[derive(Debug, Clone)]
pub struct CaBlock {
pub header: BlockHeader,
pub ca_composition: u64,
pub ca_data: Vec<u64>,
pub ca_axis_conversion: Vec<u64>,
pub ca_axis: Vec<AxisRef>,
pub ca_type: CaArrayType,
pub ca_storage: CaStorage,
pub ca_ndim: u16,
pub flags: CaFlags,
pub ca_byte_offset_base: i32,
pub ca_invalidation_bit_base: u32,
pub ca_dim_size: Vec<u64>,
pub ca_axis_values: Vec<f64>,
pub ca_dynamic_size: Vec<AxisRef>,
}
impl CaBlock {
pub const MIN_DATA_SIZE: usize = 1 + 1 + 2 + 4 + 4 + 4;
pub fn total_elements(&self) -> u64 {
self.ca_dim_size.iter().copied().product()
}
pub fn shape(&self) -> &[u64] {
&self.ca_dim_size
}
pub fn template_offset(&self) -> u64 {
self.ca_composition
}
}
impl ParseBlock for CaBlock {
fn parse(data: &[u8], offset: u64) -> Result<Self> {
let header = BlockHeader::parse(data, offset)?;
header.validate_type(b"##CA", offset)?;
let links_start = BLOCK_HEADER_SIZE;
let all_links = read_links(data, links_start, header.link_count as usize)?;
let data_start = header.data_offset();
let data_section = data
.get(data_start..)
.ok_or_else(|| Mf4Error::truncated(offset, data_start, data.len()))?;
if data_section.len() < Self::MIN_DATA_SIZE {
return Err(Mf4Error::truncated(
offset,
Self::MIN_DATA_SIZE,
data_section.len(),
));
}
let mut cursor = Cursor::new(data_section);
let ca_type = CaArrayType::from_u8(cursor.read_u8()?);
let ca_storage = CaStorage::from_u8(cursor.read_u8()?);
let ca_ndim = cursor.read_u16::<LittleEndian>()?;
let flags_raw = cursor.read_u32::<LittleEndian>()?;
let flags = CaFlags::from_u32(flags_raw);
let ca_byte_offset_base = cursor.read_i32::<LittleEndian>()?;
let ca_invalidation_bit_base = cursor.read_u32::<LittleEndian>()?;
if ca_ndim == 0 {
return Err(Mf4Error::invalid_block_size("CA", ca_ndim as u64, 1));
}
let mut ca_dim_size = Vec::with_capacity(ca_ndim as usize);
for _ in 0..ca_ndim {
ca_dim_size.push(cursor.read_u64::<LittleEndian>()?);
}
let ndim = ca_ndim as usize;
let mut link_idx = 0usize;
let ca_composition = *all_links.first().unwrap_or(&0);
link_idx += 1;
let mut ca_data = Vec::new();
if ca_storage == CaStorage::CgTemplate || ca_storage == CaStorage::DgTemplate {
let elements = ca_dim_size
.iter()
.try_fold(1usize, |acc, &d| acc.checked_mul(d as usize))
.ok_or_else(|| Mf4Error::invalid_block_size("CA", u64::MAX, 1))?;
let end = (link_idx + elements).min(all_links.len());
if link_idx <= all_links.len() {
ca_data = all_links[link_idx..end].to_vec();
}
link_idx = link_idx.saturating_add(elements);
}
let mut ca_dynamic_size = Vec::new();
if flags.dynamic_size {
for _ in 0..ndim {
let Some(triple) = all_links.get(link_idx..link_idx + 3) else {
break;
};
ca_dynamic_size.push(AxisRef {
dg: triple[0],
cg: triple[1],
cn: triple[2],
});
link_idx += 3;
}
}
if flags.input_quantity {
link_idx = link_idx.saturating_add(ndim * 3);
}
if flags.output_quantity {
link_idx = link_idx.saturating_add(3);
}
if flags.comparison_quantity {
link_idx = link_idx.saturating_add(3);
}
let mut ca_axis_conversion = Vec::new();
let mut ca_axis = Vec::new();
if flags.axis {
let end = (link_idx + ndim).min(all_links.len());
ca_axis_conversion = all_links[link_idx.min(end)..end].to_vec();
link_idx = end;
if !flags.fixed_axis {
for _ in 0..ndim {
let Some(triple) = all_links.get(link_idx..link_idx + 3) else {
break;
};
ca_axis.push(AxisRef {
dg: triple[0],
cg: triple[1],
cn: triple[2],
});
link_idx += 3;
}
}
}
let mut ca_axis_values = Vec::new();
if flags.fixed_axis {
for &dim in &ca_dim_size {
for _ in 0..dim {
ca_axis_values.push(cursor.read_f64::<LittleEndian>()?);
}
}
}
Ok(CaBlock {
header,
ca_composition,
ca_data,
ca_axis_conversion,
ca_axis,
ca_type,
ca_storage,
ca_ndim,
flags,
ca_byte_offset_base,
ca_invalidation_bit_base,
ca_dim_size,
ca_axis_values,
ca_dynamic_size,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const FLAG_DYNAMIC_SIZE: u32 = 1 << 0;
const FLAG_INPUT_QUANTITY: u32 = 1 << 1;
const FLAG_OUTPUT_QUANTITY: u32 = 1 << 2;
const FLAG_COMPARISON_QUANTITY: u32 = 1 << 3;
const FLAG_AXIS: u32 = 1 << 4;
const FLAG_FIXED_AXIS: u32 = 1 << 5;
const FLAG_INVERSE_LAYOUT: u32 = 1 << 6;
const FLAG_LEFT_OPEN_INTERVAL: u32 = 1 << 7;
const FLAG_STANDARD_AXIS: u32 = 1 << 8;
const COMPOSITION: u64 = 1_000;
const DATA: u64 = 2_000;
const DYNAMIC_SIZE: u64 = 3_000;
const INPUT: u64 = 4_000;
const OUTPUT: u64 = 5_000;
const COMPARISON: u64 = 6_000;
const AXIS_CC: u64 = 7_000;
const AXIS: u64 = 8_000;
fn create_ca_block(
ca_type: u8,
ca_storage: u8,
ndim: u16,
flags: u32,
dim_sizes: &[u64],
) -> Vec<u8> {
let mut links: Vec<u64> = vec![COMPOSITION];
if ca_storage == 1 || ca_storage == 2 {
let count: u64 = dim_sizes.iter().product();
for i in 0..count {
links.push(DATA + i);
}
}
if flags & FLAG_DYNAMIC_SIZE != 0 {
for i in 0..ndim as u64 {
links.extend([
DYNAMIC_SIZE + i * 10,
DYNAMIC_SIZE + i * 10 + 1,
DYNAMIC_SIZE + i * 10 + 2,
]);
}
}
if flags & FLAG_INPUT_QUANTITY != 0 {
for i in 0..ndim as u64 {
links.extend([INPUT + i * 10, INPUT + i * 10 + 1, INPUT + i * 10 + 2]);
}
}
if flags & FLAG_OUTPUT_QUANTITY != 0 {
links.extend([OUTPUT, OUTPUT + 1, OUTPUT + 2]);
}
if flags & FLAG_COMPARISON_QUANTITY != 0 {
links.extend([COMPARISON, COMPARISON + 1, COMPARISON + 2]);
}
if flags & FLAG_AXIS != 0 {
for i in 0..ndim as u64 {
links.push(AXIS_CC + i * 100);
}
if flags & FLAG_FIXED_AXIS == 0 {
for i in 0..ndim as u64 {
links.extend([AXIS + i * 10, AXIS + i * 10 + 1, AXIS + i * 10 + 2]);
}
}
}
let link_count = links.len() as u64;
let links_bytes: Vec<u8> = links.iter().flat_map(|l| l.to_le_bytes()).collect();
let mut data_section: Vec<u8> = Vec::new();
data_section.push(ca_type);
data_section.push(ca_storage);
data_section.extend_from_slice(&ndim.to_le_bytes());
data_section.extend_from_slice(&flags.to_le_bytes());
data_section.extend_from_slice(&0i32.to_le_bytes()); data_section.extend_from_slice(&0u32.to_le_bytes()); for &d in dim_sizes {
data_section.extend_from_slice(&d.to_le_bytes());
}
if flags & FLAG_FIXED_AXIS != 0 {
for &d in dim_sizes {
for j in 0..d {
data_section.extend_from_slice(&(j as f64).to_le_bytes());
}
}
}
let total_len = BLOCK_HEADER_SIZE + links_bytes.len() + data_section.len();
let mut data = vec![0u8; total_len];
data[0..4].copy_from_slice(b"##CA");
data[4..8].copy_from_slice(&[0, 0, 0, 0]);
data[8..16].copy_from_slice(&(total_len as u64).to_le_bytes());
data[16..24].copy_from_slice(&link_count.to_le_bytes());
data[BLOCK_HEADER_SIZE..BLOCK_HEADER_SIZE + links_bytes.len()]
.copy_from_slice(&links_bytes);
let ds = BLOCK_HEADER_SIZE + links_bytes.len();
data[ds..ds + data_section.len()].copy_from_slice(&data_section);
data
}
#[test]
fn the_storage_codes_are_the_ones_the_standard_assigns() {
for (code, expected) in [
(0u8, CaStorage::CnTemplate),
(1, CaStorage::CgTemplate),
(2, CaStorage::DgTemplate),
(3, CaStorage::Unknown(3)),
] {
let data = create_ca_block(0, code, 1, 0, &[2]);
let ca = CaBlock::parse(&data, 0).unwrap();
assert_eq!(ca.ca_storage, expected, "ca_storage = {code}");
}
}
#[test]
fn every_flag_bit_is_the_one_the_standard_assigns() {
type Case = (u32, fn(&CaFlags) -> bool, &'static str);
let cases: [Case; 9] = [
(FLAG_DYNAMIC_SIZE, |f| f.dynamic_size, "dynamic size"),
(FLAG_INPUT_QUANTITY, |f| f.input_quantity, "input quantity"),
(
FLAG_OUTPUT_QUANTITY,
|f| f.output_quantity,
"output quantity",
),
(
FLAG_COMPARISON_QUANTITY,
|f| f.comparison_quantity,
"comparison quantity",
),
(FLAG_AXIS, |f| f.axis, "axis"),
(FLAG_FIXED_AXIS, |f| f.fixed_axis, "fixed axis"),
(FLAG_INVERSE_LAYOUT, |f| f.inverse_layout, "inverse layout"),
(
FLAG_LEFT_OPEN_INTERVAL,
|f| f.left_open_interval,
"left-open interval",
),
(FLAG_STANDARD_AXIS, |f| f.standard_axis, "standard axis"),
];
for (bit, is_set, name) in cases {
let ca = CaBlock::parse(&create_ca_block(0, 0, 1, bit, &[2]), 0).unwrap();
assert!(is_set(&ca.flags), "{name} should be set for {bit:#x}");
let ca = CaBlock::parse(&create_ca_block(0, 0, 1, 0, &[2]), 0).unwrap();
assert!(!is_set(&ca.flags), "{name} should be clear when no flag is");
}
}
#[test]
fn test_ca_block_array_contiguous() {
let data = create_ca_block(0, 0, 2, 0, &[3, 4]);
let ca = CaBlock::parse(&data, 0).unwrap();
assert_eq!(ca.ca_type, CaArrayType::Array);
assert_eq!(ca.ca_storage, CaStorage::CnTemplate);
assert_eq!(ca.ca_ndim, 2);
assert_eq!(ca.ca_composition, COMPOSITION);
assert_eq!(ca.ca_dim_size, vec![3, 4]);
assert_eq!(ca.total_elements(), 12);
assert_eq!(ca.shape(), &[3, 4]);
assert!(ca.ca_axis.is_empty(), "no axis flag, so no axis references");
assert!(ca.ca_axis_conversion.is_empty());
}
#[test]
fn test_ca_block_fixed_axis() {
let data = create_ca_block(0, 0, 1, FLAG_AXIS | FLAG_FIXED_AXIS, &[3]);
let ca = CaBlock::parse(&data, 0).unwrap();
assert!(ca.flags.axis && ca.flags.fixed_axis);
assert_eq!(ca.ca_dim_size, vec![3]);
assert_eq!(
ca.ca_axis_values,
vec![0.0, 1.0, 2.0],
"every axis value, not one per dimension"
);
assert_eq!(ca.ca_axis_conversion, vec![AXIS_CC]);
assert!(
ca.ca_axis.is_empty(),
"a fixed axis has no channel to point at"
);
}
#[test]
fn an_axis_that_is_not_fixed_is_a_triple_per_dimension() {
let data = create_ca_block(0, 0, 2, FLAG_AXIS, &[3, 4]);
let ca = CaBlock::parse(&data, 0).unwrap();
assert_eq!(ca.ca_axis_conversion, vec![AXIS_CC, AXIS_CC + 100]);
assert_eq!(
ca.ca_axis,
vec![
AxisRef {
dg: AXIS,
cg: AXIS + 1,
cn: AXIS + 2
},
AxisRef {
dg: AXIS + 10,
cg: AXIS + 11,
cn: AXIS + 12
},
]
);
}
#[test]
fn a_dynamic_size_dimension_is_a_triple_naming_its_real_count() {
let data = create_ca_block(0, 0, 2, FLAG_DYNAMIC_SIZE, &[3, 4]);
let ca = CaBlock::parse(&data, 0).unwrap();
assert_eq!(
ca.ca_dynamic_size,
vec![
AxisRef {
dg: DYNAMIC_SIZE,
cg: DYNAMIC_SIZE + 1,
cn: DYNAMIC_SIZE + 2
},
AxisRef {
dg: DYNAMIC_SIZE + 10,
cg: DYNAMIC_SIZE + 11,
cn: DYNAMIC_SIZE + 12
},
]
);
}
#[test]
fn the_quantity_links_are_skipped_without_shifting_the_axis() {
let flags = FLAG_DYNAMIC_SIZE
| FLAG_INPUT_QUANTITY
| FLAG_OUTPUT_QUANTITY
| FLAG_COMPARISON_QUANTITY
| FLAG_AXIS
| FLAG_FIXED_AXIS;
let data = create_ca_block(0, 0, 2, flags, &[2, 3]);
let ca = CaBlock::parse(&data, 0).unwrap();
assert_eq!(ca.ca_composition, COMPOSITION);
assert_eq!(ca.ca_axis_conversion, vec![AXIS_CC, AXIS_CC + 100]);
assert_eq!(ca.ca_axis_values, vec![0.0, 1.0, 0.0, 1.0, 2.0]);
}
#[test]
fn a_cg_template_array_carries_one_cg_link_per_element() {
let data = create_ca_block(0, 1, 2, FLAG_AXIS | FLAG_FIXED_AXIS, &[2, 3]);
let ca = CaBlock::parse(&data, 0).unwrap();
assert_eq!(ca.ca_storage, CaStorage::CgTemplate);
assert_eq!(
ca.ca_data,
vec![DATA, DATA + 1, DATA + 2, DATA + 3, DATA + 4, DATA + 5]
);
assert_eq!(
ca.ca_axis_conversion,
vec![AXIS_CC, AXIS_CC + 100],
"six data links must not be mistaken for axis conversions"
);
}
#[test]
fn a_dg_template_array_reserves_one_data_link_per_element() {
let data = create_ca_block(0, 2, 2, FLAG_AXIS | FLAG_FIXED_AXIS, &[2, 3]);
let ca = CaBlock::parse(&data, 0).unwrap();
assert_eq!(ca.ca_storage, CaStorage::DgTemplate);
assert_eq!(
ca.ca_data,
vec![DATA, DATA + 1, DATA + 2, DATA + 3, DATA + 4, DATA + 5]
);
assert_eq!(
ca.ca_axis_conversion,
vec![AXIS_CC, AXIS_CC + 100],
"six data links must not be mistaken for axis conversions"
);
}
#[test]
fn a_scale_axis_block_still_carries_its_composition_link_first() {
let data = create_ca_block(1, 0, 1, 0, &[5]);
let ca = CaBlock::parse(&data, 0).unwrap();
assert_eq!(ca.ca_type, CaArrayType::ScaleAxis);
assert_eq!(ca.ca_composition, COMPOSITION);
}
#[test]
fn the_array_type_codes_are_the_ones_the_standard_assigns() {
for (code, expected) in [
(0u8, CaArrayType::Array),
(1, CaArrayType::ScaleAxis),
(2, CaArrayType::Lookup),
(3, CaArrayType::Unknown(3)),
] {
let ca = CaBlock::parse(&create_ca_block(code, 0, 1, 0, &[2]), 0).unwrap();
assert_eq!(ca.ca_type, expected, "ca_type = {code}");
}
}
#[test]
fn test_ca_block_zero_dims_rejected() {
let data = create_ca_block(0, 1, 0, 0, &[]);
let result = CaBlock::parse(&data, 0);
assert!(result.is_err());
}
#[test]
fn test_ca_block_invalid_type_rejected() {
let mut data = create_ca_block(0, 1, 1, 0, &[3]);
data[0..4].copy_from_slice(b"##XX");
let result = CaBlock::parse(&data, 0);
assert!(matches!(result, Err(Mf4Error::InvalidBlockId { .. })));
}
}