use crate::blocks::common::{read_link, BlockHeader, ParseBlock, BLOCK_HEADER_SIZE};
use crate::error::{Mf4Error, Result};
use byteorder::{LittleEndian, ReadBytesExt};
use std::io::{Cursor, Read};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DataBlockType {
Data,
SortedData,
ReductionData,
DataValues,
DataInvalidation,
Compressed,
DataList,
ListData,
HeaderList,
}
#[derive(Debug, Clone)]
pub struct DtBlock {
pub header: BlockHeader,
pub data_offset: u64,
pub data_length: u64,
}
impl ParseBlock for DtBlock {
fn parse(data: &[u8], offset: u64) -> Result<Self> {
let header = BlockHeader::parse(data, offset)?;
if !matches!(
&header.block_type,
b"##DT" | b"##SD" | b"##RD" | b"##DV" | b"##DI"
) {
header.validate_type(b"##DT", offset)?;
}
let data_offset = offset + BLOCK_HEADER_SIZE as u64;
let data_length = header.length - BLOCK_HEADER_SIZE as u64;
Ok(DtBlock {
header,
data_offset,
data_length,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionType {
Deflate,
TransposedDeflate,
Zstd,
TransposedZstd,
Lz4,
TransposedLz4,
Unknown(u8),
}
impl CompressionType {
pub(crate) fn from_u8(value: u8) -> Self {
match value {
0 => CompressionType::Deflate,
1 => CompressionType::TransposedDeflate,
2 => CompressionType::Zstd,
3 => CompressionType::TransposedZstd,
4 => CompressionType::Lz4,
5 => CompressionType::TransposedLz4,
v => CompressionType::Unknown(v),
}
}
pub fn is_transposed(&self) -> bool {
matches!(
self,
CompressionType::TransposedDeflate
| CompressionType::TransposedZstd
| CompressionType::TransposedLz4
)
}
}
#[derive(Debug, Clone)]
pub struct DzBlock {
pub header: BlockHeader,
pub original_type: [u8; 2],
pub zip_type: CompressionType,
pub reserved: u8,
pub zip_parameter: u32,
pub original_size: u64,
pub compressed_size: u64,
pub compressed_data_offset: u64,
}
impl DzBlock {
pub const MIN_SIZE: u64 = BLOCK_HEADER_SIZE as u64 + 24;
}
impl ParseBlock for DzBlock {
fn parse(data: &[u8], offset: u64) -> Result<Self> {
let header = BlockHeader::parse(data, offset)?;
header.validate_type(b"##DZ", offset)?;
if header.length < Self::MIN_SIZE {
return Err(Mf4Error::invalid_block_size(
"DZ",
header.length,
Self::MIN_SIZE,
));
}
let data_start = BLOCK_HEADER_SIZE;
let data_section = data
.get(data_start..)
.ok_or_else(|| Mf4Error::truncated(offset, data_start, data.len()))?;
let mut cursor = Cursor::new(data_section);
let mut original_type = [0u8; 2];
cursor.read_exact(&mut original_type)?;
let zip_type_raw = cursor.read_u8()?;
let zip_type = CompressionType::from_u8(zip_type_raw);
let reserved = cursor.read_u8()?;
let zip_parameter = cursor.read_u32::<LittleEndian>()?;
let original_size = cursor.read_u64::<LittleEndian>()?;
let compressed_size = cursor.read_u64::<LittleEndian>()?;
let compressed_data_offset = offset + Self::MIN_SIZE;
Ok(DzBlock {
header,
original_type,
zip_type,
reserved,
zip_parameter,
original_size,
compressed_size,
compressed_data_offset,
})
}
}
#[derive(Debug, Clone)]
pub struct DlBlock {
pub header: BlockHeader,
pub dl_next: u64,
pub data_links: Vec<u64>,
pub flags: u8,
pub reserved: [u8; 3],
pub count: u32,
pub equal_length: Option<u64>,
pub offsets: Vec<u64>,
pub time_values: Vec<i64>,
pub angle_values: Vec<f64>,
pub distance_values: Vec<f64>,
}
impl ParseBlock for DlBlock {
fn parse(data: &[u8], offset: u64) -> Result<Self> {
let header = BlockHeader::parse(data, offset)?;
header.validate_type(b"##DL", offset)?;
let links_start = BLOCK_HEADER_SIZE;
let dl_next = read_link(data, links_start)?;
let data_link_count = header.link_count.saturating_sub(1) as usize;
let mut data_links = Vec::with_capacity(data_link_count);
for i in 0..data_link_count {
data_links.push(read_link(data, links_start + 8 + i * 8)?);
}
let data_start = header.data_offset();
let data_section = data
.get(data_start..)
.ok_or_else(|| Mf4Error::truncated(offset, data_start, data.len()))?;
let mut cursor = Cursor::new(data_section);
let flags = cursor.read_u8()?;
let mut reserved = [0u8; 3];
cursor.read_exact(&mut reserved)?;
let count = cursor.read_u32::<LittleEndian>()?;
let equal_length = if (flags & 0x01) != 0 {
Some(cursor.read_u64::<LittleEndian>()?)
} else {
None
};
let sections = u32::from(equal_length.is_none()) + (flags & 0x0E).count_ones();
let needed = u64::from(count) * 8 * u64::from(sections);
let remaining = data_section.len() - cursor.position() as usize;
if needed > remaining as u64 {
return Err(Mf4Error::truncated(
offset + data_start as u64 + cursor.position(),
usize::try_from(needed).unwrap_or(usize::MAX),
remaining,
));
}
let offsets = if equal_length.is_none() {
let mut offs = Vec::with_capacity(count as usize);
for _ in 0..count {
offs.push(cursor.read_u64::<LittleEndian>()?);
}
offs
} else {
Vec::new()
};
let time_values = if (flags & 0x02) != 0 {
let mut times = Vec::with_capacity(count as usize);
for _ in 0..count {
times.push(cursor.read_i64::<LittleEndian>()?);
}
times
} else {
Vec::new()
};
let angle_values = if (flags & 0x04) != 0 {
let mut angles = Vec::with_capacity(count as usize);
for _ in 0..count {
angles.push(cursor.read_f64::<LittleEndian>()?);
}
angles
} else {
Vec::new()
};
let distance_values = if (flags & 0x08) != 0 {
let mut distances = Vec::with_capacity(count as usize);
for _ in 0..count {
distances.push(cursor.read_f64::<LittleEndian>()?);
}
distances
} else {
Vec::new()
};
Ok(DlBlock {
header,
dl_next,
data_links,
flags,
reserved,
count,
equal_length,
offsets,
time_values,
angle_values,
distance_values,
})
}
}
#[derive(Debug, Clone)]
pub struct LdBlock {
pub header: BlockHeader,
pub ld_next: u64,
pub data_links: Vec<u64>,
pub invalidation_links: Vec<u64>,
pub flags: u32,
pub count: u32,
pub equal_length: Option<u64>,
pub offsets: Vec<u64>,
pub time_values: Vec<i64>,
pub angle_values: Vec<f64>,
pub distance_values: Vec<f64>,
}
impl LdBlock {
pub const FLAG_EQUAL_LENGTH: u32 = 0x0000_0001;
pub const FLAG_TIME_VALUES: u32 = 0x0000_0002;
pub const FLAG_ANGLE_VALUES: u32 = 0x0000_0004;
pub const FLAG_DISTANCE_VALUES: u32 = 0x0000_0008;
pub const FLAG_INVALIDATION_PRESENT: u32 = 0x8000_0000;
}
impl ParseBlock for LdBlock {
fn parse(data: &[u8], offset: u64) -> Result<Self> {
let header = BlockHeader::parse(data, offset)?;
header.validate_type(b"##LD", offset)?;
let links_start = BLOCK_HEADER_SIZE;
let ld_next = read_link(data, links_start)?;
let data_start = header.data_offset();
let data_section = data
.get(data_start..)
.ok_or_else(|| Mf4Error::truncated(offset, data_start, data.len()))?;
let mut cursor = Cursor::new(data_section);
let flags = cursor.read_u32::<LittleEndian>()?;
let count = cursor.read_u32::<LittleEndian>()?;
let has_invalidation = (flags & Self::FLAG_INVALIDATION_PRESENT) != 0;
let actual_links = header.link_count as usize;
let data_links_count = (count as usize).min(actual_links.saturating_sub(1));
let mut data_links = Vec::with_capacity(data_links_count);
for i in 0..data_links_count {
data_links.push(read_link(data, links_start + 8 + i * 8)?);
}
let mut invalidation_links = Vec::new();
if has_invalidation {
let inval_start_index = 1 + count as usize;
let inval_links_count =
(count as usize).min(actual_links.saturating_sub(inval_start_index));
invalidation_links.reserve(inval_links_count);
for i in 0..inval_links_count {
invalidation_links
.push(read_link(data, links_start + (inval_start_index + i) * 8)?);
}
}
let equal_length = if (flags & Self::FLAG_EQUAL_LENGTH) != 0 {
Some(cursor.read_u64::<LittleEndian>()?)
} else {
None
};
let sections = u32::from(equal_length.is_none()) + (flags & 0x0E).count_ones();
let needed = u64::from(count) * 8 * u64::from(sections);
let remaining = data_section.len() - cursor.position() as usize;
if needed > remaining as u64 {
return Err(Mf4Error::truncated(
offset + data_start as u64 + cursor.position(),
usize::try_from(needed).unwrap_or(usize::MAX),
remaining,
));
}
let offsets = if equal_length.is_none() {
let mut offs = Vec::with_capacity(count as usize);
for _ in 0..count {
offs.push(cursor.read_u64::<LittleEndian>()?);
}
offs
} else {
Vec::new()
};
let time_values = if (flags & Self::FLAG_TIME_VALUES) != 0 {
let mut times = Vec::with_capacity(count as usize);
for _ in 0..count {
times.push(cursor.read_i64::<LittleEndian>()?);
}
times
} else {
Vec::new()
};
let angle_values = if (flags & Self::FLAG_ANGLE_VALUES) != 0 {
let mut angles = Vec::with_capacity(count as usize);
for _ in 0..count {
angles.push(cursor.read_f64::<LittleEndian>()?);
}
angles
} else {
Vec::new()
};
let distance_values = if (flags & Self::FLAG_DISTANCE_VALUES) != 0 {
let mut distances = Vec::with_capacity(count as usize);
for _ in 0..count {
distances.push(cursor.read_f64::<LittleEndian>()?);
}
distances
} else {
Vec::new()
};
Ok(LdBlock {
header,
ld_next,
data_links,
invalidation_links,
flags,
count,
equal_length,
offsets,
time_values,
angle_values,
distance_values,
})
}
}
#[derive(Debug, Clone)]
pub struct HlBlock {
pub header: BlockHeader,
pub dl_first: u64,
pub flags: u16,
pub zip_type: CompressionType,
pub reserved: [u8; 5],
}
impl ParseBlock for HlBlock {
fn parse(data: &[u8], offset: u64) -> Result<Self> {
let header = BlockHeader::parse(data, offset)?;
header.validate_type(b"##HL", offset)?;
let links_start = BLOCK_HEADER_SIZE;
let dl_first = read_link(data, links_start)?;
let data_start = header.data_offset();
let data_section = data
.get(data_start..)
.ok_or_else(|| Mf4Error::truncated(offset, data_start, data.len()))?;
let mut cursor = Cursor::new(data_section);
let flags = cursor.read_u16::<LittleEndian>()?;
let zip_type_raw = cursor.read_u8()?;
let zip_type = CompressionType::from_u8(zip_type_raw);
let mut reserved = [0u8; 5];
cursor.read_exact(&mut reserved)?;
Ok(HlBlock {
header,
dl_first,
flags,
zip_type,
reserved,
})
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum DataBlock {
Data(DtBlock),
Compressed(DzBlock),
DataList(DlBlock),
ListData(LdBlock),
HeaderList(HlBlock),
}
impl DataBlock {
pub fn parse(data: &[u8], offset: u64) -> Result<Self> {
if data.len() < 4 {
return Err(Mf4Error::truncated(offset, 4, data.len()));
}
let block_id = &data[0..4];
match block_id {
b"##DT" | b"##SD" | b"##RD" | b"##DV" | b"##DI" => {
Ok(DataBlock::Data(DtBlock::parse(data, offset)?))
}
b"##DZ" => Ok(DataBlock::Compressed(DzBlock::parse(data, offset)?)),
b"##DL" => Ok(DataBlock::DataList(DlBlock::parse(data, offset)?)),
b"##LD" => Ok(DataBlock::ListData(LdBlock::parse(data, offset)?)),
b"##HL" => Ok(DataBlock::HeaderList(HlBlock::parse(data, offset)?)),
_ => Err(Mf4Error::invalid_block_id(
offset,
"##DT/DV/DI/DZ/DL/LD/HL",
String::from_utf8_lossy(block_id).to_string(),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_dt_block(data_len: usize) -> Vec<u8> {
let total_len = BLOCK_HEADER_SIZE + data_len;
let mut data = vec![0u8; total_len];
data[0..4].copy_from_slice(b"##DT");
data[8..16].copy_from_slice(&(total_len as u64).to_le_bytes());
data[16..24].copy_from_slice(&0u64.to_le_bytes());
for (i, byte) in data[BLOCK_HEADER_SIZE..].iter_mut().enumerate() {
*byte = (i & 0xFF) as u8;
}
data
}
#[test]
fn test_dt_block_parse() {
let data = create_test_dt_block(100);
let dt = DtBlock::parse(&data, 1000).unwrap();
assert_eq!(dt.header.block_type, *b"##DT");
assert_eq!(dt.data_length, 100);
}
#[test]
fn test_data_block_auto_detect() {
let dt_data = create_test_dt_block(50);
let block = DataBlock::parse(&dt_data, 0).unwrap();
assert!(matches!(block, DataBlock::Data(_)));
}
#[test]
fn test_compression_type() {
assert_eq!(CompressionType::from_u8(0), CompressionType::Deflate);
assert_eq!(
CompressionType::from_u8(1),
CompressionType::TransposedDeflate
);
assert_eq!(CompressionType::from_u8(2), CompressionType::Zstd);
assert_eq!(CompressionType::from_u8(3), CompressionType::TransposedZstd);
assert_eq!(CompressionType::from_u8(4), CompressionType::Lz4);
assert_eq!(CompressionType::from_u8(5), CompressionType::TransposedLz4);
assert!(matches!(
CompressionType::from_u8(99),
CompressionType::Unknown(99)
));
}
fn create_test_dl_block(count: u32, entries: usize) -> Vec<u8> {
let section_len = 8 + entries * 8; let total_len = BLOCK_HEADER_SIZE + 8 + section_len;
let mut data = vec![0u8; total_len];
data[0..4].copy_from_slice(b"##DL");
data[8..16].copy_from_slice(&(total_len as u64).to_le_bytes());
data[16..24].copy_from_slice(&1u64.to_le_bytes());
let section = BLOCK_HEADER_SIZE + 8;
data[section] = 0; data[section + 4..section + 8].copy_from_slice(&count.to_le_bytes());
data
}
#[test]
fn a_dl_count_beyond_the_data_section_is_an_error_not_an_allocation() {
let data = create_test_dl_block(u32::MAX, 0);
assert!(DlBlock::parse(&data, 0).is_err());
}
#[test]
fn a_dl_count_that_fits_the_data_section_parses() {
let mut data = create_test_dl_block(1, 1);
let entry = BLOCK_HEADER_SIZE + 8 + 8;
data[entry..entry + 8].copy_from_slice(&42u64.to_le_bytes());
let dl = DlBlock::parse(&data, 0).unwrap();
assert_eq!(dl.count, 1);
assert_eq!(dl.offsets, vec![42]);
}
}