use crate::blocks::common::{read_link, BlockHeader, ParseBlock, BLOCK_HEADER_SIZE};
use crate::error::{Mf4Error, Result};
const AT_DATA_SIZE: usize = 2 + 2 + 4 + 16 + 8 + 8;
use byteorder::{LittleEndian, ReadBytesExt};
use std::io::Cursor;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AtFlags {
pub embedded: bool,
pub compressed: bool,
pub md5_valid: bool,
}
impl AtFlags {
fn from_u16(value: u16) -> Self {
AtFlags {
embedded: (value & 0x0001) != 0,
compressed: (value & 0x0002) != 0,
md5_valid: (value & 0x0004) != 0,
}
}
}
#[derive(Debug, Clone)]
pub struct AtBlock {
pub header: BlockHeader,
pub at_next: u64,
pub tx_file_name: u64,
pub tx_file_path: u64,
pub md_comment: u64,
pub flags: AtFlags,
pub creator_index: u16,
pub md5_checksum: [u8; 16],
pub original_size: u64,
pub embedded_size: u64,
}
impl AtBlock {
pub const MIN_SIZE: u64 = BLOCK_HEADER_SIZE as u64 + 4 * 8 + AT_DATA_SIZE as u64;
pub fn is_embedded(&self) -> bool {
self.embedded_size > 0
}
pub fn embedded_data_offset(&self) -> u64 {
if self.is_embedded() {
self.header.offset + self.header.data_offset() as u64 + AT_DATA_SIZE as u64
} else {
0
}
}
}
impl ParseBlock for AtBlock {
fn parse(data: &[u8], offset: u64) -> Result<Self> {
let header = BlockHeader::parse(data, offset)?;
header.validate_type(b"##AT", offset)?;
if header.length < Self::MIN_SIZE {
return Err(Mf4Error::invalid_block_size(
"AT",
header.length,
Self::MIN_SIZE,
));
}
let links_start = BLOCK_HEADER_SIZE;
let at_next = read_link(data, links_start)?;
let tx_file_name = read_link(data, links_start + 8)?;
let tx_file_path = read_link(data, links_start + 16)?;
let md_comment = read_link(data, links_start + 24)?;
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() < AT_DATA_SIZE {
return Err(Mf4Error::truncated(
offset,
AT_DATA_SIZE,
data_section.len(),
));
}
let mut cursor = Cursor::new(data_section);
let flags_raw = cursor.read_u16::<LittleEndian>()?;
let flags = AtFlags::from_u16(flags_raw);
let creator_index = cursor.read_u16::<LittleEndian>()?;
let _reserved = cursor.read_u32::<LittleEndian>()?;
let mut md5_checksum = [0u8; 16];
std::io::Read::read_exact(&mut cursor, &mut md5_checksum)?;
let original_size = cursor.read_u64::<LittleEndian>()?;
let embedded_size = cursor.read_u64::<LittleEndian>()?;
Ok(AtBlock {
header,
at_next,
tx_file_name,
tx_file_path,
md_comment,
flags,
creator_index,
md5_checksum,
original_size,
embedded_size,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_at_block(embedded_size: u64, flags: u16) -> Vec<u8> {
let links = 4usize;
let total_len = BLOCK_HEADER_SIZE + links * 8 + AT_DATA_SIZE;
let mut data = vec![0u8; total_len];
data[0..4].copy_from_slice(b"##AT");
data[8..16].copy_from_slice(&(total_len as u64).to_le_bytes());
data[16..24].copy_from_slice(&(links as u64).to_le_bytes());
data[24..32].copy_from_slice(&100u64.to_le_bytes()); data[32..40].copy_from_slice(&200u64.to_le_bytes()); data[40..48].copy_from_slice(&300u64.to_le_bytes()); data[48..56].copy_from_slice(&400u64.to_le_bytes());
let d = BLOCK_HEADER_SIZE + links * 8;
data[d..d + 2].copy_from_slice(&flags.to_le_bytes());
data[d + 2..d + 4].copy_from_slice(&9u16.to_le_bytes()); for (k, byte) in data[d + 8..d + 24].iter_mut().enumerate() {
*byte = k as u8; }
data[d + 24..d + 32].copy_from_slice(&1024u64.to_le_bytes()); data[d + 32..d + 40].copy_from_slice(&embedded_size.to_le_bytes());
data
}
#[test]
fn test_at_block_parse_external() {
let data = create_test_at_block(0, 0);
let at = AtBlock::parse(&data, 5000).unwrap();
assert_eq!(at.header.block_type, *b"##AT");
assert_eq!(at.at_next, 100);
assert_eq!(at.tx_file_name, 200);
assert!(!at.is_embedded());
assert_eq!(at.original_size, 1024);
assert_eq!(at.embedded_data_offset(), 0);
}
#[test]
fn test_at_block_parse_embedded() {
let data = create_test_at_block(512, 0x0001);
let at = AtBlock::parse(&data, 5000).unwrap();
assert!(at.flags.embedded);
assert!(at.is_embedded());
assert_eq!(at.embedded_size, 512);
assert_eq!(
at.embedded_data_offset(),
5000 + (BLOCK_HEADER_SIZE + 4 * 8 + 40) as u64
);
}
#[test]
fn flag_bits_sit_where_the_standard_puts_them() {
let only_embedded = AtBlock::parse(&create_test_at_block(512, 0x0001), 0).unwrap();
assert!(only_embedded.flags.embedded);
assert!(!only_embedded.flags.compressed);
assert!(!only_embedded.flags.md5_valid);
let compressed = AtBlock::parse(&create_test_at_block(512, 0x0002), 0).unwrap();
assert!(!compressed.flags.embedded);
assert!(
compressed.flags.compressed,
"bit 1 is compressed embedded data"
);
assert!(!compressed.flags.md5_valid);
let md5 = AtBlock::parse(&create_test_at_block(512, 0x0004), 0).unwrap();
assert!(!md5.flags.embedded);
assert!(!md5.flags.compressed);
assert!(md5.flags.md5_valid, "bit 2 marks the checksum as valid");
let all = AtBlock::parse(&create_test_at_block(512, 0x0007), 0).unwrap();
assert!(all.flags.embedded && all.flags.compressed && all.flags.md5_valid);
}
#[test]
fn test_at_block_invalid_type() {
let mut data = create_test_at_block(0, 0);
data[0..4].copy_from_slice(b"##XX");
let result = AtBlock::parse(&data, 0);
assert!(matches!(result, Err(Mf4Error::InvalidBlockId { .. })));
}
}