use crate::iff::{IffError, InvalidChunk, assert_valid_chunk_id, is_valid_chunk_id};
use byteorder::{LittleEndian, WriteBytesExt};
use std::cell::RefCell;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::rc::{Rc, Weak};
#[derive(Debug, Clone)]
pub struct RiffChunk {
pub id: String,
pub data_size: u32,
pub offset: u64,
pub data_offset: u64,
pub size: u32,
}
impl RiffChunk {
pub fn new(id: &str, data_size: u32, offset: u64) -> std::result::Result<Self, IffError> {
let data_offset = offset.checked_add(8).ok_or_else(|| {
IffError(format!(
"RIFF chunk data offset overflow at offset={}",
offset
))
})?;
let padding = data_size % 2;
let size = data_size.checked_add(8 + padding).ok_or_else(|| {
IffError(format!(
"RIFF chunk size overflow: data_size={} + header + padding exceeds u32",
data_size
))
})?;
Ok(Self {
id: id.to_string(),
data_size,
offset,
data_offset,
size,
})
}
pub fn try_new(id: &str, data_size: u32, offset: u64) -> crate::Result<Self> {
Self::new(id, data_size, offset).map_err(|e| crate::AudexError::InvalidData(e.to_string()))
}
pub fn id(&self) -> &str {
&self.id
}
pub fn data_size(&self) -> u32 {
self.data_size
}
pub fn offset(&self) -> u64 {
self.offset
}
pub fn data_offset(&self) -> u64 {
self.data_offset
}
pub fn size(&self) -> u32 {
self.size
}
pub fn parse_header(header: &[u8]) -> std::result::Result<(String, u32), IffError> {
if header.len() < 8 {
return Err(IffError("Header too short".to_string()));
}
let id_bytes = &header[0..4];
let size = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
let id = String::from_utf8(id_bytes.to_vec())
.map_err(|_| IffError("Invalid chunk ID encoding".to_string()))?;
Ok((id, size))
}
pub fn padding(&self) -> u32 {
self.data_size % 2
}
pub fn read(&self, file: &mut File) -> std::result::Result<Vec<u8>, IffError> {
let limits = crate::limits::ParseLimits::default();
if (self.data_size as u64) > limits.max_tag_size {
return Err(IffError(format!(
"RIFF chunk data size {} exceeds global limit {} bytes",
self.data_size, limits.max_tag_size
)));
}
file.seek(SeekFrom::Start(self.data_offset))?;
let mut data = vec![0u8; self.data_size as usize];
file.read_exact(&mut data)?;
Ok(data)
}
pub fn write(&self, file: &mut File, data: &[u8]) -> std::result::Result<(), IffError> {
if data.len() > self.data_size as usize {
return Err(IffError("Data too large for chunk".to_string()));
}
file.seek(SeekFrom::Start(self.data_offset))?;
file.write_all(data)?;
let padding = self.padding();
if padding > 0 {
file.seek(SeekFrom::Start(self.data_offset + self.data_size as u64))?;
file.write_all(&vec![0u8; padding as usize])?;
}
Ok(())
}
pub fn resize(
&mut self,
file: &mut File,
new_data_size: u32,
) -> std::result::Result<(i64, u32), IffError> {
let old_size = self.get_actual_data_size(file)?;
let padding = new_data_size % 2;
let new_total_size = new_data_size.checked_add(padding).ok_or_else(|| {
IffError(format!(
"RIFF chunk resize overflow: data_size={} + padding exceeds u32",
new_data_size
))
})?;
let size_change = new_total_size as i64 - old_size as i64;
crate::iff::resize_bytes(file, old_size, new_total_size, self.data_offset)?;
file.flush()?;
self.data_size = new_data_size;
let new_padding = self.padding();
self.size = 8u32
.checked_add(self.data_size)
.and_then(|s| s.checked_add(new_padding))
.ok_or_else(|| {
IffError(format!(
"RIFF chunk total size overflow: 8 + {} + {} exceeds u32",
self.data_size, new_padding
))
})?;
file.seek(SeekFrom::Start(self.offset + 4))?;
file.write_u32::<LittleEndian>(self.data_size)?;
Ok((size_change, self.size))
}
pub fn delete(&mut self, file: &mut File) -> std::result::Result<(), IffError> {
crate::iff::delete_bytes(file, self.size as u64, self.offset)?;
file.flush()?;
Ok(())
}
fn get_actual_data_size(&self, file: &mut File) -> std::result::Result<u32, IffError> {
file.seek(SeekFrom::End(0))?;
let file_size = file.stream_position()?;
let expected_size = self.data_size as u64 + self.padding() as u64;
let max_size_possible = file_size.saturating_sub(self.data_offset);
let actual = std::cmp::min(expected_size, max_size_possible);
Ok(u32::try_from(actual).unwrap_or(u32::MAX))
}
}
#[derive(Debug, Clone)]
pub struct RiffListChunk {
pub base: RiffChunk,
pub name: Option<String>,
pub name_size: usize,
subchunks: HashMap<String, RiffChunk>,
subchunks_loaded: bool,
}
impl RiffListChunk {
pub fn new(id: &str, data_size: u32, offset: u64) -> std::result::Result<Self, IffError> {
if id != "RIFF" && id != "LIST" {
return Err(InvalidChunk(format!("Expected RIFF or LIST chunk, got {}", id)).into());
}
let base = RiffChunk::new(id, data_size, offset)?;
Ok(Self {
base,
name: None,
name_size: 4,
subchunks: HashMap::new(),
subchunks_loaded: false,
})
}
pub fn init_container(
&mut self,
file: &mut File,
name_size: usize,
) -> std::result::Result<(), IffError> {
if self.base.data_size < name_size as u32 {
return Err(InvalidChunk(format!("Container chunk data size < {}", name_size)).into());
}
self.name_size = name_size;
if name_size > 0 {
file.seek(SeekFrom::Start(self.base.data_offset))?;
let mut name_bytes = vec![0u8; name_size];
file.read_exact(&mut name_bytes)?;
match String::from_utf8(name_bytes) {
Ok(name) => self.name = Some(name.trim_end().to_string()),
Err(_) => return Err(IffError("Invalid container name encoding".to_string())),
}
} else {
self.name = None;
}
Ok(())
}
pub fn load_subchunks(&mut self, file: &mut File) -> std::result::Result<(), IffError> {
if self.subchunks_loaded {
return Ok(());
}
self.subchunks.clear();
let mut next_offset = self.base.offset + 8 + self.name_size as u64;
let container_end = self
.base
.offset
.checked_add(8)
.and_then(|v| v.checked_add(self.base.data_size as u64))
.ok_or_else(|| IffError("RIFF container end offset overflow".into()))?;
while next_offset < container_end {
file.seek(SeekFrom::Start(next_offset))?;
let mut header = [0u8; 8];
if file.read_exact(&mut header).is_err() {
break; }
let (id, data_size) = RiffChunk::parse_header(&header)?;
if !is_valid_chunk_id(&id) {
break; }
let chunk = match RiffChunk::new(&id, data_size, next_offset) {
Ok(c) => c,
Err(_) => break, };
let chunk_end = next_offset + chunk.size as u64;
if chunk_end <= next_offset {
break;
}
if self.subchunks.contains_key(&id) {
warn_event!(chunk_id = %id, "Duplicate RIFF chunk ID; keeping first occurrence");
} else {
self.subchunks.insert(id.clone(), chunk);
}
next_offset = chunk_end;
if next_offset % 2 == 1 {
next_offset += 1; }
}
self.subchunks_loaded = true;
Ok(())
}
pub fn contains(&mut self, file: &mut File, id: &str) -> std::result::Result<bool, IffError> {
assert_valid_chunk_id(id)?;
self.load_subchunks(file)?;
Ok(self.subchunks.contains_key(id))
}
pub fn get_chunk(
&mut self,
file: &mut File,
id: &str,
) -> std::result::Result<&RiffChunk, IffError> {
assert_valid_chunk_id(id)?;
self.load_subchunks(file)?;
self.subchunks
.get(id)
.ok_or_else(|| IffError(format!("No '{}' chunk found", id)))
}
pub fn get_chunk_mut(
&mut self,
file: &mut File,
id: &str,
) -> std::result::Result<&mut RiffChunk, IffError> {
assert_valid_chunk_id(id)?;
self.load_subchunks(file)?;
self.subchunks
.get_mut(id)
.ok_or_else(|| IffError(format!("No '{}' chunk found", id)))
}
pub fn insert_chunk(
&mut self,
file: &mut File,
id: &str,
data: Option<&[u8]>,
) -> std::result::Result<(), IffError> {
if !is_valid_chunk_id(id) {
return Err(IffError("Invalid IFF chunk ID".to_string()));
}
let actual_data_size = self.get_actual_data_size(file)?;
let next_offset = self.base.data_offset + actual_data_size as u64;
let raw_len = data.map(|d| d.len()).unwrap_or(0);
let data_size = u32::try_from(raw_len).map_err(|_| {
IffError(format!(
"RIFF insert_chunk data length {} exceeds u32::MAX",
raw_len
))
})?;
let padding = data_size % 2;
let chunk_size = 8u32
.checked_add(data_size)
.and_then(|s| s.checked_add(padding))
.ok_or_else(|| {
IffError(format!(
"chunk size overflow: 8 + {} + {} exceeds u32::MAX",
data_size, padding
))
})?;
crate::iff::insert_bytes(file, chunk_size, next_offset)?;
file.seek(SeekFrom::Start(next_offset))?;
let id_bytes = id.as_bytes();
let mut padded_id = [b' '; 4];
let copy_len = std::cmp::min(id_bytes.len(), 4);
padded_id[..copy_len].copy_from_slice(&id_bytes[..copy_len]);
file.write_all(&padded_id)?;
file.write_u32::<LittleEndian>(data_size)?;
let chunk = RiffChunk::new(id, data_size, next_offset)?;
if let Some(data) = data {
chunk.write(file, data)?;
}
self.base.data_size = self
.base
.data_size
.checked_add(chunk_size)
.ok_or_else(|| IffError("Container size would overflow u32".to_string()))?;
self.base.size = 8u32.checked_add(self.base.data_size).ok_or_else(|| {
IffError("Container total size overflow: 8 + data_size exceeds u32".to_string())
})?;
file.seek(SeekFrom::Start(self.base.offset + 4))?;
file.write_u32::<LittleEndian>(self.base.data_size)?;
if self.subchunks_loaded {
self.subchunks.insert(id.to_string(), chunk);
}
file.flush()?;
Ok(())
}
pub fn delete_chunk(&mut self, file: &mut File, id: &str) -> std::result::Result<(), IffError> {
assert_valid_chunk_id(id)?;
self.load_subchunks(file)?;
let chunk = self
.subchunks
.get(id)
.ok_or_else(|| IffError(format!("No '{}' chunk found", id)))?;
let chunk_offset = chunk.offset;
let chunk_size = chunk.size;
crate::iff::delete_bytes(file, chunk_size as u64, chunk_offset)?;
file.flush()?;
self.subchunks.remove(id);
self.base.data_size = self
.base
.data_size
.checked_sub(chunk_size)
.ok_or_else(|| IffError("Chunk size exceeds container data size".to_string()))?;
self.base.size = 8u32.checked_add(self.base.data_size).ok_or_else(|| {
IffError("Container total size overflow: 8 + data_size exceeds u32".to_string())
})?;
file.seek(SeekFrom::Start(self.base.offset + 4))?;
file.write_u32::<LittleEndian>(self.base.data_size)?;
self.subchunks_loaded = false;
Ok(())
}
pub fn update_after_chunk_resize(
&mut self,
file: &mut File,
_chunk_id: &str,
size_change: i64,
) -> std::result::Result<(), IffError> {
self.subchunks_loaded = false;
if size_change != 0 {
let new_size = self.base.data_size as i64 + size_change;
if new_size < 0 || new_size > u32::MAX as i64 {
return Err(IffError(format!(
"Chunk resize would produce invalid container size: {}",
new_size
)));
}
self.base.data_size = new_size as u32;
self.base.size = 8u32.checked_add(self.base.data_size).ok_or_else(|| {
IffError("Container total size overflow: 8 + data_size exceeds u32".to_string())
})?;
file.seek(SeekFrom::Start(self.base.offset + 4))?;
file.write_u32::<LittleEndian>(self.base.data_size)?;
}
Ok(())
}
fn get_actual_data_size(&self, file: &mut File) -> std::result::Result<u32, IffError> {
file.seek(SeekFrom::End(0))?;
let file_size = file.stream_position()?;
let expected_size = self.base.data_size as u64 + self.base.padding() as u64;
let max_size_possible = file_size.saturating_sub(self.base.data_offset);
let actual = std::cmp::min(expected_size, max_size_possible);
Ok(u32::try_from(actual).unwrap_or(u32::MAX))
}
pub fn id(&self) -> &str {
&self.base.id
}
pub fn size(&self) -> u32 {
self.base.size
}
}
pub struct MutableChunk {
chunk: RiffChunk,
fileobj: Rc<RefCell<File>>,
parent_root: Weak<RefCell<RiffListChunk>>,
}
impl MutableChunk {
fn new(
chunk: RiffChunk,
fileobj: Rc<RefCell<File>>,
parent_root: Weak<RefCell<RiffListChunk>>,
) -> Self {
Self {
chunk,
fileobj,
parent_root,
}
}
pub fn id(&self) -> &str {
&self.chunk.id
}
pub fn data_size(&self) -> u32 {
self.chunk.data_size
}
pub fn offset(&self) -> u64 {
if let Some(parent_root) = self.parent_root.upgrade() {
if let Ok(mut file) = self.fileobj.try_borrow_mut() {
if let Ok(mut root) = parent_root.try_borrow_mut() {
if root.load_subchunks(&mut file).is_ok() {
if let Ok(chunk) = root.get_chunk(&mut file, &self.chunk.id) {
return chunk.offset;
}
}
}
}
}
self.chunk.offset
}
pub fn data_offset(&self) -> u64 {
if let Some(parent_root) = self.parent_root.upgrade() {
if let Ok(mut file) = self.fileobj.try_borrow_mut() {
if let Ok(mut root) = parent_root.try_borrow_mut() {
if root.load_subchunks(&mut file).is_ok() {
if let Ok(chunk) = root.get_chunk(&mut file, &self.chunk.id) {
return chunk.data_offset;
}
}
}
}
}
self.chunk.data_offset
}
pub fn size(&self) -> u32 {
self.chunk.size
}
pub fn read(&self) -> std::result::Result<Vec<u8>, IffError> {
let mut file = self.fileobj.borrow_mut();
self.chunk.read(&mut file)
}
pub fn write(&mut self, data: &[u8]) -> std::result::Result<(), IffError> {
let mut file = self.fileobj.borrow_mut();
self.chunk.write(&mut file, data)
}
pub fn resize(&mut self, new_data_size: u32) -> std::result::Result<(), IffError> {
let (size_change, new_chunk_size) = {
let mut file = self.fileobj.borrow_mut();
self.chunk.resize(&mut file, new_data_size)?
};
self.chunk.size = new_chunk_size;
self.chunk.data_size = new_data_size;
if let Some(parent_root) = self.parent_root.upgrade() {
let mut file = self.fileobj.borrow_mut();
parent_root.borrow_mut().update_after_chunk_resize(
&mut file,
&self.chunk.id,
size_change,
)?;
}
Ok(())
}
pub fn delete(&mut self) -> std::result::Result<(), IffError> {
let mut file = self.fileobj.borrow_mut();
self.chunk.delete(&mut file)?;
Ok(())
}
pub fn padding(&self) -> u32 {
self.chunk.padding()
}
}
pub struct RiffFile {
fileobj: Rc<RefCell<File>>,
pub file_type: Option<String>,
root: Rc<RefCell<RiffListChunk>>,
}
impl RiffFile {
pub fn new(fileobj: Rc<RefCell<File>>) -> std::result::Result<Self, IffError> {
let (id, data_size, file_type) = {
let mut file = fileobj.borrow_mut();
file.seek(SeekFrom::Start(0))?;
let mut header = [0u8; 8];
file.read_exact(&mut header)?;
let (id, data_size) = RiffChunk::parse_header(&header)?;
if id != "RIFF" {
return Err(
InvalidChunk(format!("Root chunk must be a RIFF chunk, got {}", id)).into(),
);
}
let mut type_bytes = [0u8; 4];
file.read_exact(&mut type_bytes)?;
let file_type = String::from_utf8(type_bytes.to_vec())
.map_err(|_| IffError("Invalid file type encoding".to_string()))?;
(id, data_size, Some(file_type))
};
let mut root = RiffListChunk::new(&id, data_size, 0)?;
root.name_size = 4; root.name = file_type.clone();
Ok(Self {
fileobj,
file_type,
root: Rc::new(RefCell::new(root)),
})
}
pub fn get_root(&self) -> std::result::Result<RiffListChunk, IffError> {
Ok(self.root.borrow().clone())
}
pub fn contains(&mut self, id: &str) -> std::result::Result<bool, IffError> {
let mut file = self.fileobj.borrow_mut();
self.root.borrow_mut().contains(&mut file, id)
}
pub fn get_chunk(&mut self, id: &str) -> std::result::Result<MutableChunk, IffError> {
let chunk = {
let mut file = self.fileobj.borrow_mut();
let mut root = self.root.borrow_mut();
root.load_subchunks(&mut file)?;
root.get_chunk(&mut file, id)?.clone()
};
Ok(MutableChunk::new(
chunk,
self.fileobj.clone(),
Rc::downgrade(&self.root),
))
}
pub fn delete_chunk(&mut self, id: &str) -> std::result::Result<(), IffError> {
let mut file = self.fileobj.borrow_mut();
self.root.borrow_mut().delete_chunk(&mut file, id)
}
pub fn insert_chunk(
&mut self,
id: &str,
data: Option<&[u8]>,
) -> std::result::Result<MutableChunk, IffError> {
{
let mut file = self.fileobj.borrow_mut();
self.root.borrow_mut().insert_chunk(&mut file, id, data)?;
}
self.get_chunk(id)
}
}