use crate::blocks::common::{read_links, BlockHeader, ParseBlock, BLOCK_HEADER_SIZE};
use crate::error::{Mf4Error, Result};
const FIXED_LINKS: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChElement {
pub data_group: u64,
pub channel_group: u64,
pub channel: u64,
}
use byteorder::{LittleEndian, ReadBytesExt};
use std::io::Cursor;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChType {
Group,
Function,
Structure,
MapList,
InputVariable,
OutputVariable,
LocalVariable,
CalibrationDefinition,
CalibrationObject,
Unknown(u8),
}
impl ChType {
fn from_u8(value: u8) -> Self {
match value {
0 => ChType::Group,
1 => ChType::Function,
2 => ChType::Structure,
3 => ChType::MapList,
4 => ChType::InputVariable,
5 => ChType::OutputVariable,
6 => ChType::LocalVariable,
7 => ChType::CalibrationDefinition,
8 => ChType::CalibrationObject,
v => ChType::Unknown(v),
}
}
}
#[derive(Debug, Clone)]
pub struct ChBlock {
pub header: BlockHeader,
pub ch_next: u64,
pub ch_first: u64,
pub tx_name: u64,
pub md_comment: u64,
pub ch_element: Vec<ChElement>,
pub ch_element_count: u32,
pub ch_type: ChType,
}
impl ChBlock {
pub const MIN_DATA_SIZE: usize = 4 + 1 + 3;
}
impl ParseBlock for ChBlock {
fn parse(data: &[u8], offset: u64) -> Result<Self> {
let header = BlockHeader::parse(data, offset)?;
header.validate_type(b"##CH", offset)?;
let links_start = BLOCK_HEADER_SIZE;
let all_links = read_links(data, links_start, header.link_count as usize)?;
if all_links.len() < FIXED_LINKS {
return Err(Mf4Error::invalid_block_size(
"CH",
header.link_count * 8,
(FIXED_LINKS * 8) as u64,
));
}
let ch_next = all_links[0];
let ch_first = all_links[1];
let tx_name = all_links[2];
let md_comment = all_links[3];
let ch_element: Vec<ChElement> = all_links[FIXED_LINKS..]
.chunks_exact(3)
.map(|t| ChElement {
data_group: t[0],
channel_group: t[1],
channel: t[2],
})
.collect();
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 ch_element_count = cursor.read_u32::<LittleEndian>()?;
let ch_type = ChType::from_u8(cursor.read_u8()?);
let _reserved = [cursor.read_u8()?, cursor.read_u8()?, cursor.read_u8()?];
Ok(ChBlock {
header,
ch_next,
ch_first,
tx_name,
md_comment,
ch_element,
ch_element_count,
ch_type,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_ch_block(
elements: &[(u64, u64, u64)],
ch_type: u8,
first_child: u64,
) -> Vec<u8> {
let mut links = vec![100u64, first_child, 200, 300];
for (dg, cg, cn) in elements {
links.extend_from_slice(&[*dg, *cg, *cn]);
}
let total_len = BLOCK_HEADER_SIZE + links.len() * 8 + 8;
let mut data = vec![0u8; total_len];
data[0..4].copy_from_slice(b"##CH");
data[8..16].copy_from_slice(&(total_len as u64).to_le_bytes());
data[16..24].copy_from_slice(&(links.len() as u64).to_le_bytes());
for (i, link) in links.iter().enumerate() {
let at = BLOCK_HEADER_SIZE + i * 8;
data[at..at + 8].copy_from_slice(&link.to_le_bytes());
}
let d = BLOCK_HEADER_SIZE + links.len() * 8;
data[d..d + 4].copy_from_slice(&(elements.len() as u32).to_le_bytes());
data[d + 4] = ch_type;
data
}
#[test]
fn elements_are_read_as_triples_not_single_links() {
let data = create_test_ch_block(&[(10, 20, 30), (40, 50, 60)], 0, 0);
let ch = ChBlock::parse(&data, 0).unwrap();
assert_eq!(ch.ch_element_count, 2);
assert_eq!(
ch.ch_element,
vec![
ChElement {
data_group: 10,
channel_group: 20,
channel: 30
},
ChElement {
data_group: 40,
channel_group: 50,
channel: 60
},
]
);
}
#[test]
fn hierarchy_type_follows_the_standards_numbering() {
let spec = [
(0u8, ChType::Group),
(1, ChType::Function),
(2, ChType::Structure),
(3, ChType::MapList),
(4, ChType::InputVariable),
(5, ChType::OutputVariable),
(6, ChType::LocalVariable),
(7, ChType::CalibrationDefinition),
(8, ChType::CalibrationObject),
];
for (raw, expected) in spec {
let data = create_test_ch_block(&[], raw, 0);
let ch = ChBlock::parse(&data, 0).unwrap();
assert_eq!(ch.ch_type, expected, "ch_type {raw} decoded wrongly");
}
let data = create_test_ch_block(&[], 9, 0);
assert_eq!(
ChBlock::parse(&data, 0).unwrap().ch_type,
ChType::Unknown(9),
"9 upward is undefined and must not be given a name"
);
}
#[test]
fn the_child_link_is_read_and_does_not_displace_the_name() {
let data = create_test_ch_block(&[], 0, 999);
let ch = ChBlock::parse(&data, 0).unwrap();
assert_eq!(ch.ch_next, 100);
assert_eq!(ch.ch_first, 999, "the child link should be read");
assert_eq!(ch.tx_name, 200, "the name link should not be displaced");
assert_eq!(ch.md_comment, 300);
}
#[test]
fn a_node_with_no_elements_parses() {
let data = create_test_ch_block(&[], 1, 0);
let ch = ChBlock::parse(&data, 0).unwrap();
assert_eq!(ch.ch_element_count, 0);
assert!(ch.ch_element.is_empty());
assert_eq!(ch.ch_first, 0);
}
#[test]
fn rejects_a_block_of_the_wrong_type() {
let mut data = create_test_ch_block(&[(1, 2, 3)], 0, 0);
data[0..4].copy_from_slice(b"##DG");
assert!(ChBlock::parse(&data, 0).is_err());
}
#[test]
fn rejects_a_block_without_its_fixed_links() {
let mut data = create_test_ch_block(&[], 0, 0);
data[16..24].copy_from_slice(&2u64.to_le_bytes()); assert!(ChBlock::parse(&data, 0).is_err());
}
}