use crate::blocks::common::{BlockHeader, ParseBlock, BLOCK_HEADER_SIZE};
use crate::error::{Mf4Error, Result};
#[derive(Debug, Clone)]
pub struct TxBlock {
pub header: BlockHeader,
pub text: String,
}
impl ParseBlock for TxBlock {
fn parse(data: &[u8], offset: u64) -> Result<Self> {
let header = BlockHeader::parse(data, offset)?;
header.validate_type(b"##TX", offset)?;
let data_start = BLOCK_HEADER_SIZE;
let data_len = (header.length as usize).saturating_sub(data_start);
if data.len() < data_start + data_len {
return Err(Mf4Error::truncated(
offset,
data_start + data_len,
data.len(),
));
}
let text_bytes = &data[data_start..data_start + data_len];
let end = text_bytes
.iter()
.position(|&b| b == 0)
.unwrap_or(text_bytes.len());
let text = String::from_utf8_lossy(&text_bytes[..end]).to_string();
Ok(TxBlock { header, text })
}
}
impl TxBlock {
pub fn text_trimmed(&self) -> &str {
self.text.trim()
}
}
#[derive(Debug, Clone)]
pub struct MdBlock {
pub header: BlockHeader,
pub xml: String,
}
impl MdBlock {
pub fn metadata(&self) -> crate::model::Metadata {
crate::model::Metadata::parse(&self.xml)
}
}
impl ParseBlock for MdBlock {
fn parse(data: &[u8], offset: u64) -> Result<Self> {
let header = BlockHeader::parse(data, offset)?;
header.validate_type(b"##MD", offset)?;
let data_start = BLOCK_HEADER_SIZE;
let data_len = (header.length as usize).saturating_sub(data_start);
if data.len() < data_start + data_len {
return Err(Mf4Error::truncated(
offset,
data_start + data_len,
data.len(),
));
}
let xml_bytes = &data[data_start..data_start + data_len];
let end = xml_bytes
.iter()
.position(|&b| b == 0)
.unwrap_or(xml_bytes.len());
let xml = String::from_utf8_lossy(&xml_bytes[..end]).to_string();
Ok(MdBlock { header, xml })
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum TextOrMetadata {
Text(TxBlock),
Metadata(MdBlock),
}
impl TextOrMetadata {
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"##TX" => Ok(TextOrMetadata::Text(TxBlock::parse(data, offset)?)),
b"##MD" => Ok(TextOrMetadata::Metadata(MdBlock::parse(data, offset)?)),
_ => Err(Mf4Error::invalid_block_id(
offset,
"##TX/##MD",
String::from_utf8_lossy(block_id).to_string(),
)),
}
}
pub fn as_str(&self) -> &str {
match self {
TextOrMetadata::Text(tx) => &tx.text,
TextOrMetadata::Metadata(md) => &md.xml,
}
}
pub fn comment(&self) -> String {
match self {
TextOrMetadata::Text(tx) => tx.text.trim().to_string(),
TextOrMetadata::Metadata(md) => md.metadata().text().to_string(),
}
}
pub fn as_str_trimmed(&self) -> &str {
self.as_str().trim()
}
}
pub fn read_text_at_link(data: &[u8], file_len: u64, link: u64) -> Result<String> {
if link == 0 {
return Ok(String::new());
}
if link >= file_len {
return Err(Mf4Error::InvalidLink {
offset: 0,
target: link,
});
}
let offset = link as usize;
let remaining = data.len().saturating_sub(offset);
if remaining < BLOCK_HEADER_SIZE {
return Err(Mf4Error::truncated(link, BLOCK_HEADER_SIZE, remaining));
}
let block_data = &data[offset..];
let text_or_md = TextOrMetadata::parse(block_data, link)?;
Ok(text_or_md.as_str_trimmed().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_tx_block(text: &str) -> Vec<u8> {
let text_bytes = text.as_bytes();
let total_len = BLOCK_HEADER_SIZE + text_bytes.len() + 1; let mut data = vec![0u8; total_len];
data[0..4].copy_from_slice(b"##TX");
data[8..16].copy_from_slice(&(total_len as u64).to_le_bytes());
data[16..24].copy_from_slice(&0u64.to_le_bytes());
data[BLOCK_HEADER_SIZE..BLOCK_HEADER_SIZE + text_bytes.len()].copy_from_slice(text_bytes);
data
}
#[test]
fn test_tx_block_parse() {
let data = create_test_tx_block("TestChannel");
let tx = TxBlock::parse(&data, 0).unwrap();
assert_eq!(tx.text, "TestChannel");
assert_eq!(tx.text_trimmed(), "TestChannel");
}
#[test]
fn test_tx_block_with_whitespace() {
let data = create_test_tx_block(" Test ");
let tx = TxBlock::parse(&data, 0).unwrap();
assert_eq!(tx.text, " Test ");
assert_eq!(tx.text_trimmed(), "Test");
}
fn create_test_md_block(xml: &str) -> Vec<u8> {
let xml_bytes = xml.as_bytes();
let total_len = BLOCK_HEADER_SIZE + xml_bytes.len() + 1;
let mut data = vec![0u8; total_len];
data[0..4].copy_from_slice(b"##MD");
data[8..16].copy_from_slice(&(total_len as u64).to_le_bytes());
data[16..24].copy_from_slice(&0u64.to_le_bytes());
data[BLOCK_HEADER_SIZE..BLOCK_HEADER_SIZE + xml_bytes.len()].copy_from_slice(xml_bytes);
data
}
#[test]
fn test_md_block_parse() {
let data = create_test_md_block("<root><item>value</item></root>");
let md = MdBlock::parse(&data, 0).unwrap();
assert!(md.xml.contains("<root>"));
}
#[test]
fn test_text_or_metadata_auto_detect() {
let tx_data = create_test_tx_block("Hello");
let result = TextOrMetadata::parse(&tx_data, 0).unwrap();
assert!(matches!(result, TextOrMetadata::Text(_)));
assert_eq!(result.as_str_trimmed(), "Hello");
let md_data = create_test_md_block("<xml/>");
let result = TextOrMetadata::parse(&md_data, 0).unwrap();
assert!(matches!(result, TextOrMetadata::Metadata(_)));
}
}