use super::AT_BLOCK_SIZE;
use crate::{
Result,
blocks::common::{BlockHeader, BlockParse, read_u16, read_u64, validate_buffer_size},
};
use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AttachmentFlags(u16);
impl AttachmentFlags {
pub const EMBEDDED: u16 = 0x0001;
pub const COMPRESSED: u16 = 0x0002;
pub const MD5_VALID: u16 = 0x0004;
pub fn from_u16(value: u16) -> Self {
Self(value)
}
pub fn as_u16(self) -> u16 {
self.0
}
pub fn is_embedded(self) -> bool {
self.0 & Self::EMBEDDED != 0
}
pub fn is_compressed(self) -> bool {
self.0 & Self::COMPRESSED != 0
}
pub fn is_md5_valid(self) -> bool {
self.0 & Self::MD5_VALID != 0
}
pub fn embedded() -> Self {
Self(Self::EMBEDDED)
}
pub fn embedded_compressed() -> Self {
Self(Self::EMBEDDED | Self::COMPRESSED)
}
pub fn external() -> Self {
Self(0)
}
}
#[derive(Debug, Clone)]
pub struct AttachmentBlock<'a> {
pub header: BlockHeader,
pub next_at_addr: u64,
pub filename_addr: u64,
pub mimetype_addr: u64,
pub comment_addr: u64,
pub flags: AttachmentFlags,
pub creator_index: u16,
pub md5_checksum: [u8; 16],
pub original_size: u64,
pub embedded_size: u64,
pub embedded_data: &'a [u8],
}
pub const AT_HEADER_SIZE: usize = 96;
impl<'a> BlockParse<'a> for AttachmentBlock<'a> {
const ID: &'static str = "##AT";
fn from_bytes(bytes: &'a [u8]) -> Result<Self> {
let header = Self::parse_header(bytes)?;
validate_buffer_size(bytes, AT_HEADER_SIZE)?;
let next_at_addr = read_u64(bytes, 24);
let filename_addr = read_u64(bytes, 32);
let mimetype_addr = read_u64(bytes, 40);
let comment_addr = read_u64(bytes, 48);
let flags = AttachmentFlags::from_u16(read_u16(bytes, 56));
let creator_index = read_u16(bytes, 58);
let mut md5_checksum = [0u8; 16];
md5_checksum.copy_from_slice(&bytes[64..80]);
let original_size = read_u64(bytes, 80);
let embedded_size = read_u64(bytes, 88);
let embedded_data = if flags.is_embedded() && embedded_size > 0 {
let data_end = AT_HEADER_SIZE + embedded_size as usize;
validate_buffer_size(bytes, data_end)?;
&bytes[AT_HEADER_SIZE..data_end]
} else {
&bytes[0..0] };
Ok(Self {
header,
next_at_addr,
filename_addr,
mimetype_addr,
comment_addr,
flags,
creator_index,
md5_checksum,
original_size,
embedded_size,
embedded_data,
})
}
}
impl AttachmentBlock<'_> {
pub fn external(original_size: u64) -> Self {
Self {
header: BlockHeader {
id: String::from("##AT"),
reserved: 0,
length: AT_BLOCK_SIZE as u64,
link_count: 4,
},
next_at_addr: 0,
filename_addr: 0,
mimetype_addr: 0,
comment_addr: 0,
flags: AttachmentFlags::external(),
creator_index: 0,
md5_checksum: [0u8; 16],
original_size,
embedded_size: 0,
embedded_data: &[],
}
}
pub fn embedded(data: &[u8]) -> AttachmentBlock<'_> {
AttachmentBlock {
header: BlockHeader {
id: String::from("##AT"),
reserved: 0,
length: (AT_HEADER_SIZE + data.len()) as u64,
link_count: 4,
},
next_at_addr: 0,
filename_addr: 0,
mimetype_addr: 0,
comment_addr: 0,
flags: AttachmentFlags::embedded(),
creator_index: 0,
md5_checksum: [0u8; 16],
original_size: data.len() as u64,
embedded_size: data.len() as u64,
embedded_data: data,
}
}
pub fn data(&self) -> Option<&[u8]> {
if self.flags.is_embedded() && !self.flags.is_compressed() {
Some(self.embedded_data)
} else {
None
}
}
#[cfg(feature = "compression")]
pub fn decompress(&self) -> Result<Option<Vec<u8>>> {
use crate::Error;
use miniz_oxide::inflate::decompress_to_vec_zlib;
if !self.flags.is_embedded() {
return Ok(None);
}
if self.flags.is_compressed() {
let decompressed = decompress_to_vec_zlib(self.embedded_data).map_err(|e| {
Error::BlockSerializationError(alloc::format!("AT decompression failed: {:?}", e))
})?;
if decompressed.len() != self.original_size as usize {
return Err(Error::BlockSerializationError(alloc::format!(
"AT decompressed size mismatch: expected {}, got {}",
self.original_size,
decompressed.len()
)));
}
Ok(Some(decompressed))
} else {
Ok(Some(self.embedded_data.to_vec()))
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let total_size = AT_HEADER_SIZE + self.embedded_data.len();
let mut buffer = Vec::with_capacity(total_size);
let mut header = self.header.clone();
header.length = total_size as u64;
buffer.extend_from_slice(&header.to_bytes()?);
buffer.extend_from_slice(&self.next_at_addr.to_le_bytes());
buffer.extend_from_slice(&self.filename_addr.to_le_bytes());
buffer.extend_from_slice(&self.mimetype_addr.to_le_bytes());
buffer.extend_from_slice(&self.comment_addr.to_le_bytes());
buffer.extend_from_slice(&self.flags.as_u16().to_le_bytes());
buffer.extend_from_slice(&self.creator_index.to_le_bytes());
buffer.extend_from_slice(&[0u8; 4]); buffer.extend_from_slice(&self.md5_checksum);
buffer.extend_from_slice(&self.original_size.to_le_bytes());
buffer.extend_from_slice(&self.embedded_size.to_le_bytes());
buffer.extend_from_slice(self.embedded_data);
Ok(buffer)
}
}
impl Default for AttachmentBlock<'_> {
fn default() -> Self {
Self::external(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_at_bytes(
flags: u16,
original_size: u64,
embedded_size: u64,
embedded_data: &[u8],
) -> Vec<u8> {
let total_len = AT_HEADER_SIZE as u64 + embedded_data.len() as u64;
let mut bytes = Vec::with_capacity(total_len as usize);
bytes.extend_from_slice(b"##AT");
bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&total_len.to_le_bytes()); bytes.extend_from_slice(&4u64.to_le_bytes());
bytes.extend_from_slice(&0u64.to_le_bytes()); bytes.extend_from_slice(&0x1000u64.to_le_bytes()); bytes.extend_from_slice(&0x2000u64.to_le_bytes()); bytes.extend_from_slice(&0u64.to_le_bytes());
bytes.extend_from_slice(&flags.to_le_bytes()); bytes.extend_from_slice(&0u16.to_le_bytes()); bytes.extend_from_slice(&[0u8; 4]); bytes.extend_from_slice(&[0u8; 16]); bytes.extend_from_slice(&original_size.to_le_bytes());
bytes.extend_from_slice(&embedded_size.to_le_bytes());
bytes.extend_from_slice(embedded_data);
bytes
}
#[test]
fn parse_external_reference() {
let bytes = create_at_bytes(0, 1024, 0, &[]);
let at = AttachmentBlock::from_bytes(&bytes).unwrap();
assert!(!at.flags.is_embedded());
assert!(!at.flags.is_compressed());
assert_eq!(at.original_size, 1024);
assert_eq!(at.embedded_size, 0);
assert!(at.embedded_data.is_empty());
assert_eq!(at.filename_addr, 0x1000);
assert_eq!(at.mimetype_addr, 0x2000);
}
#[test]
fn parse_embedded_uncompressed() {
let data = b"Hello, MDF4!";
let bytes = create_at_bytes(
AttachmentFlags::EMBEDDED,
data.len() as u64,
data.len() as u64,
data,
);
let at = AttachmentBlock::from_bytes(&bytes).unwrap();
assert!(at.flags.is_embedded());
assert!(!at.flags.is_compressed());
assert_eq!(at.original_size, data.len() as u64);
assert_eq!(at.embedded_size, data.len() as u64);
assert_eq!(at.embedded_data, data);
assert_eq!(at.data(), Some(data.as_slice()));
}
#[test]
fn flags_operations() {
let embedded = AttachmentFlags::embedded();
assert!(embedded.is_embedded());
assert!(!embedded.is_compressed());
let compressed = AttachmentFlags::embedded_compressed();
assert!(compressed.is_embedded());
assert!(compressed.is_compressed());
let external = AttachmentFlags::external();
assert!(!external.is_embedded());
let with_md5 =
AttachmentFlags::from_u16(AttachmentFlags::EMBEDDED | AttachmentFlags::MD5_VALID);
assert!(with_md5.is_embedded());
assert!(with_md5.is_md5_valid());
}
#[test]
fn roundtrip_external() {
let original = AttachmentBlock::external(2048);
let bytes = original.to_bytes().unwrap();
let parsed = AttachmentBlock::from_bytes(&bytes).unwrap();
assert!(!parsed.flags.is_embedded());
assert_eq!(parsed.original_size, 2048);
assert_eq!(parsed.embedded_size, 0);
}
#[test]
fn roundtrip_embedded() {
let data = b"Test attachment data for roundtrip";
let original = AttachmentBlock::embedded(data);
let bytes = original.to_bytes().unwrap();
let parsed = AttachmentBlock::from_bytes(&bytes).unwrap();
assert!(parsed.flags.is_embedded());
assert_eq!(parsed.original_size, data.len() as u64);
assert_eq!(parsed.embedded_data, data);
}
#[cfg(feature = "compression")]
mod compression_tests {
use super::*;
use miniz_oxide::deflate::compress_to_vec_zlib;
#[test]
fn decompress_embedded() {
let original_data =
b"This is test data that will be compressed for the attachment block.";
let compressed = compress_to_vec_zlib(original_data, 6);
let bytes = create_at_bytes(
AttachmentFlags::EMBEDDED | AttachmentFlags::COMPRESSED,
original_data.len() as u64,
compressed.len() as u64,
&compressed,
);
let at = AttachmentBlock::from_bytes(&bytes).unwrap();
assert!(at.flags.is_embedded());
assert!(at.flags.is_compressed());
let decompressed = at.decompress().unwrap().unwrap();
assert_eq!(decompressed.as_slice(), original_data);
}
#[test]
fn decompress_uncompressed_returns_copy() {
let data = b"Uncompressed data";
let bytes = create_at_bytes(
AttachmentFlags::EMBEDDED,
data.len() as u64,
data.len() as u64,
data,
);
let at = AttachmentBlock::from_bytes(&bytes).unwrap();
let result = at.decompress().unwrap().unwrap();
assert_eq!(result.as_slice(), data);
}
#[test]
fn decompress_external_returns_none() {
let bytes = create_at_bytes(0, 1024, 0, &[]);
let at = AttachmentBlock::from_bytes(&bytes).unwrap();
let result = at.decompress().unwrap();
assert!(result.is_none());
}
}
}