use crate::{AudexError, Result};
use std::io::{Read, Seek, SeekFrom, Write};
const CONTAINERS: &[&[u8; 4]] = &[
b"moov", b"udta", b"trak", b"mdia", b"meta", b"ilst", b"stbl", b"minf", b"moof", b"traf",
];
fn is_container_atom(name: &[u8; 4], in_ilst_children: bool) -> bool {
if in_ilst_children {
return false;
}
CONTAINERS.contains(&name)
}
const SKIP_SIZE: &[(&[u8; 4], usize)] = &[(b"meta", 4)];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AtomType {
Unknown([u8; 4]),
Ftyp,
Moov,
Mvhd,
Trak,
Mdat,
Free,
Skip,
Udta,
Meta,
Hdlr,
Ilst,
Tkhd,
Mdia,
Mdhd,
Minf,
Stbl,
Stsd,
Stts,
Stsc,
Stsz,
Stco,
Co64,
Mp4a,
Alac,
Ac3,
Esds,
Dac3,
Data,
Moof,
Mfhd,
Traf,
Tfhd,
Trun,
}
#[derive(Debug, Clone)]
pub struct AtomError {
pub message: String,
}
impl std::fmt::Display for AtomError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Atom error: {}", self.message)
}
}
impl std::error::Error for AtomError {}
#[derive(Debug, Clone)]
pub struct MP4Atom {
pub atom_type: AtomType,
pub name: [u8; 4],
pub length: u64,
pub offset: u64,
pub data_offset: u64,
pub data_length: u64,
pub children: Option<Vec<MP4Atom>>,
}
impl AtomType {
pub fn from_bytes(name: &[u8; 4]) -> Self {
match name {
b"ftyp" => AtomType::Ftyp,
b"moov" => AtomType::Moov,
b"mvhd" => AtomType::Mvhd,
b"trak" => AtomType::Trak,
b"mdat" => AtomType::Mdat,
b"free" => AtomType::Free,
b"skip" => AtomType::Skip,
b"udta" => AtomType::Udta,
b"meta" => AtomType::Meta,
b"hdlr" => AtomType::Hdlr,
b"ilst" => AtomType::Ilst,
b"tkhd" => AtomType::Tkhd,
b"mdia" => AtomType::Mdia,
b"mdhd" => AtomType::Mdhd,
b"minf" => AtomType::Minf,
b"stbl" => AtomType::Stbl,
b"stsd" => AtomType::Stsd,
b"stts" => AtomType::Stts,
b"stsc" => AtomType::Stsc,
b"stsz" => AtomType::Stsz,
b"stco" => AtomType::Stco,
b"co64" => AtomType::Co64,
b"mp4a" => AtomType::Mp4a,
b"alac" => AtomType::Alac,
b"ac-3" => AtomType::Ac3,
b"esds" => AtomType::Esds,
b"dac3" => AtomType::Dac3,
b"data" => AtomType::Data,
b"moof" => AtomType::Moof,
b"mfhd" => AtomType::Mfhd,
b"traf" => AtomType::Traf,
b"tfhd" => AtomType::Tfhd,
b"trun" => AtomType::Trun,
_ => AtomType::Unknown(*name),
}
}
pub fn to_bytes(self) -> [u8; 4] {
match self {
AtomType::Ftyp => *b"ftyp",
AtomType::Moov => *b"moov",
AtomType::Mvhd => *b"mvhd",
AtomType::Trak => *b"trak",
AtomType::Mdat => *b"mdat",
AtomType::Free => *b"free",
AtomType::Skip => *b"skip",
AtomType::Udta => *b"udta",
AtomType::Meta => *b"meta",
AtomType::Hdlr => *b"hdlr",
AtomType::Ilst => *b"ilst",
AtomType::Tkhd => *b"tkhd",
AtomType::Mdia => *b"mdia",
AtomType::Mdhd => *b"mdhd",
AtomType::Minf => *b"minf",
AtomType::Stbl => *b"stbl",
AtomType::Stsd => *b"stsd",
AtomType::Stts => *b"stts",
AtomType::Stsc => *b"stsc",
AtomType::Stsz => *b"stsz",
AtomType::Stco => *b"stco",
AtomType::Co64 => *b"co64",
AtomType::Mp4a => *b"mp4a",
AtomType::Alac => *b"alac",
AtomType::Ac3 => *b"ac-3",
AtomType::Esds => *b"esds",
AtomType::Dac3 => *b"dac3",
AtomType::Data => *b"data",
AtomType::Moof => *b"moof",
AtomType::Mfhd => *b"mfhd",
AtomType::Traf => *b"traf",
AtomType::Tfhd => *b"tfhd",
AtomType::Trun => *b"trun",
AtomType::Unknown(name) => name,
}
}
pub fn is_container(self) -> bool {
let name = self.to_bytes();
CONTAINERS.contains(&(&name))
}
pub fn skip_size(self) -> usize {
let name = self.to_bytes();
SKIP_SIZE
.iter()
.find(|(atom_name, _)| *atom_name == &name)
.map(|(_, size)| *size)
.unwrap_or(0)
}
}
impl MP4Atom {
pub fn new(atom_type: AtomType, data: Vec<u8>) -> Result<Self> {
let name = atom_type.to_bytes();
let data_length = data.len() as u64;
let length = data_length.checked_add(8).ok_or_else(|| {
AudexError::InvalidData(
"overflow computing atom length: data.len() + 8 exceeds u64::MAX".to_string(),
)
})?;
Ok(Self {
atom_type,
name,
length,
offset: 0,
data_offset: 8,
data_length,
children: if atom_type.is_container() {
Some(Vec::new())
} else {
None
},
})
}
pub fn parse<R: Read + Seek>(reader: &mut R, level: usize) -> Result<Self> {
Self::parse_with_preservation(reader, level, true)
}
pub fn parse_with_preservation<R: Read + Seek>(
reader: &mut R,
level: usize,
preserve_failed: bool,
) -> Result<Self> {
Self::parse_with_context(reader, level, preserve_failed, false)
}
fn parse_with_context<R: Read + Seek>(
reader: &mut R,
level: usize,
preserve_failed: bool,
in_ilst_children: bool,
) -> Result<Self> {
let offset = reader
.stream_position()
.map_err(|e| AudexError::ParseError(format!("Failed to get stream position: {}", e)))?;
let mut header = [0u8; 8];
reader
.read_exact(&mut header)
.map_err(|e| AudexError::ParseError(format!("Failed to read atom header: {}", e)))?;
let mut length = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as u64;
let name = [header[4], header[5], header[6], header[7]];
let mut data_offset = offset + 8;
if length == 1 {
let mut size_bytes = [0u8; 8];
reader.read_exact(&mut size_bytes).map_err(|e| {
AudexError::ParseError(format!("Failed to read 64-bit atom size: {}", e))
})?;
length = u64::from_be_bytes(size_bytes);
data_offset += 8;
if length < 16 {
return Err(AudexError::ParseError(
"64 bit atom length can only be 16 and higher".to_string(),
));
}
} else if length == 0 {
if level != 0 {
return Err(AudexError::ParseError(
"only a top-level atom can have zero length".to_string(),
));
}
let current_pos = reader.stream_position().map_err(|e| {
AudexError::ParseError(format!("Failed to get current position: {}", e))
})?;
let end_pos = reader
.seek(SeekFrom::End(0))
.map_err(|e| AudexError::ParseError(format!("Failed to seek to end: {}", e)))?;
length = end_pos.checked_sub(offset).ok_or_else(|| {
AudexError::ParseError(
"zero-length atom: end of stream is before atom offset".to_string(),
)
})?;
reader
.seek(SeekFrom::Start(current_pos))
.map_err(|e| AudexError::ParseError(format!("Failed to seek back: {}", e)))?;
} else if length < 8 {
return Err(AudexError::ParseError(
"atom length can only be 0, 1 or 8 and higher".to_string(),
));
}
let atom_type = AtomType::from_bytes(&name);
let header_size = data_offset.checked_sub(offset).ok_or_else(|| {
AudexError::ParseError("atom data offset is before atom start".to_string())
})?;
let data_length = length.checked_sub(header_size).ok_or_else(|| {
AudexError::ParseError("atom length is smaller than its header size".to_string())
})?;
let mut atom = MP4Atom {
atom_type,
name,
length,
offset,
data_offset,
data_length,
children: None,
};
if is_container_atom(&name, in_ilst_children) {
let mut children = Vec::new();
let skip_bytes = atom_type.skip_size();
let child_in_ilst = name == *b"ilst";
if skip_bytes > 0 {
let mut skip_buffer = vec![0u8; skip_bytes];
reader
.read_exact(&mut skip_buffer)
.map_err(|e| AudexError::ParseError(format!("Failed to skip bytes: {}", e)))?;
}
let children_end = offset.checked_add(length).ok_or_else(|| {
AudexError::ParseError("Atom offset + length overflows u64".to_string())
})?;
while reader.stream_position().map_err(|e| {
AudexError::ParseError(format!("Failed to get stream position: {}", e))
})? < children_end
{
let current_pos = reader.stream_position().map_err(|e| {
AudexError::ParseError(format!("Failed to get position: {}", e))
})?;
if current_pos >= children_end {
break;
}
if level + 1 > Self::MAX_ATOM_DEPTH {
return Err(AudexError::DepthLimitExceeded {
max_depth: Self::MAX_ATOM_DEPTH as u32,
});
}
if children.len() >= Self::MAX_CHILDREN_PER_ATOM {
return Err(AudexError::InvalidData(format!(
"Container atom has more than {} children",
Self::MAX_CHILDREN_PER_ATOM,
)));
}
match Self::parse_with_context(reader, level + 1, preserve_failed, child_in_ilst) {
Ok(child) => children.push(child),
Err(parse_error) => {
if matches!(parse_error, AudexError::DepthLimitExceeded { .. }) {
return Err(parse_error);
}
if preserve_failed {
if let Ok(preserved) =
Self::preserve_failed_atom(reader, &parse_error, current_pos)
{
let atom_end = preserved.offset.saturating_add(preserved.length);
if reader.seek(SeekFrom::Start(atom_end)).is_err() {
children.push(preserved);
break;
}
children.push(preserved);
continue; }
}
break; }
}
}
atom.children = Some(children);
} else {
let end_pos = offset.checked_add(length).ok_or_else(|| {
AudexError::ParseError("Atom offset + length overflows u64".to_string())
})?;
reader.seek(SeekFrom::Start(end_pos)).map_err(|e| {
AudexError::ParseError(format!("Failed to seek to end of atom: {}", e))
})?;
}
Ok(atom)
}
const MAX_ATOM_DEPTH: usize = 128;
const MAX_CHILDREN_PER_ATOM: usize = 100_000;
pub fn read_data<R: Read + Seek>(&self, reader: &mut R) -> Result<Vec<u8>> {
crate::limits::ParseLimits::default().check_tag_size(self.data_length, "MP4 atom")?;
let alloc_size = usize::try_from(self.data_length).map_err(|_| {
AudexError::ParseError(format!(
"Atom data length {} exceeds addressable memory",
self.data_length
))
})?;
reader
.seek(SeekFrom::Start(self.data_offset))
.map_err(|e| AudexError::ParseError(format!("Failed to seek to atom data: {}", e)))?;
let mut data = vec![0u8; alloc_size];
reader
.read_exact(&mut data)
.map_err(|e| AudexError::ParseError(format!("Failed to read atom data: {}", e)))?;
Ok(data)
}
pub fn findall(&self, name: &[u8; 4], recursive: bool) -> Vec<&MP4Atom> {
let mut result = Vec::new();
if let Some(children) = &self.children {
for child in children {
if &child.name == name {
result.push(child);
}
if recursive {
result.extend(child.findall(name, true));
}
}
}
result
}
pub fn get_child(&self, path: &[&str]) -> Option<&MP4Atom> {
if path.is_empty() {
return Some(self);
}
let target_name = path[0].as_bytes();
if target_name.len() != 4 {
return None;
}
let target = [
target_name[0],
target_name[1],
target_name[2],
target_name[3],
];
if let Some(children) = &self.children {
for child in children {
if child.name == target {
return child.get_child(&path[1..]);
}
}
}
None
}
pub fn datalength(&self) -> u64 {
self.data_length
}
pub fn read<R: Read + Seek>(&self, reader: &mut R) -> Result<(bool, Vec<u8>)> {
let data = self.read_data(reader)?;
let success = data.len() as u64 == self.data_length;
Ok((success, data))
}
pub fn findall_iter(
&self,
name: &[u8; 4],
recursive: bool,
) -> impl Iterator<Item = &MP4Atom> + '_ {
FindallIterator::new(self, name, recursive)
}
pub fn get_by_path(&self, path: &str) -> Option<&MP4Atom> {
let parts: Vec<&str> = path.split('.').collect();
self.get_by_path_parts(&parts)
}
fn get_by_path_parts(&self, remaining: &[&str]) -> Option<&MP4Atom> {
if remaining.is_empty() {
return Some(self);
}
let part_bytes = remaining[0].as_bytes();
if part_bytes.len() != 4 {
return None;
}
let target = [part_bytes[0], part_bytes[1], part_bytes[2], part_bytes[3]];
if let Some(children) = &self.children {
for child in children {
if child.name == target {
return child.get_by_path_parts(&remaining[1..]);
}
}
}
None
}
pub fn render(name: &[u8; 4], data: &[u8]) -> Result<Vec<u8>> {
let data_len = data.len() as u64;
let size = data_len.checked_add(8).ok_or_else(|| {
AudexError::InvalidData("Atom data too large: size overflows u64".to_string())
})?;
let mut result = Vec::new();
if size <= 0xFFFF_FFFF {
result.extend_from_slice(&(size as u32).to_be_bytes());
result.extend_from_slice(name);
} else {
let ext_size = size.checked_add(8).ok_or_else(|| {
AudexError::InvalidData(
"Atom data too large: extended size overflows u64".to_string(),
)
})?;
result.extend_from_slice(&1u32.to_be_bytes()); result.extend_from_slice(name);
result.extend_from_slice(&ext_size.to_be_bytes());
}
result.extend_from_slice(data);
Ok(result)
}
pub fn update_size(&mut self, new_data_size: u64) -> Result<()> {
let header_size = self.data_offset.checked_sub(self.offset).ok_or_else(|| {
AudexError::InvalidData("atom data_offset is smaller than offset".to_string())
})?;
self.data_length = new_data_size;
self.length = header_size.checked_add(new_data_size).ok_or_else(|| {
AudexError::InvalidData("atom header size + data size overflows u64".to_string())
})?;
Ok(())
}
pub fn calculate_children_size(&self) -> Result<u64> {
match self.children.as_ref() {
Some(children) => children.iter().try_fold(0u64, |acc, child| {
acc.checked_add(child.length).ok_or_else(|| {
AudexError::InvalidData("children atom sizes overflow u64".to_string())
})
}),
None => Ok(0),
}
}
pub fn update_offset_tables<R: Read + Seek, W: Write + Seek>(
&self,
reader: &mut R,
writer: &mut W,
size_delta: i64,
moov_offset: u64,
) -> Result<()> {
if size_delta == 0 {
return Ok(());
}
let stco_atoms = self.findall(b"stco", true);
let co64_atoms = self.findall(b"co64", true);
for stco in stco_atoms {
Self::update_stco_offsets(stco, reader, writer, size_delta, moov_offset)?;
}
for co64 in co64_atoms {
Self::update_co64_offsets(co64, reader, writer, size_delta, moov_offset)?;
}
Ok(())
}
fn update_stco_offsets<R: Read + Seek, W: Write + Seek>(
stco: &MP4Atom,
reader: &mut R,
writer: &mut W,
size_delta: i64,
moov_offset: u64,
) -> Result<()> {
let data = stco.read_data(reader)?;
if data.len() < 8 {
return Err(AudexError::ParseError("stco atom too short".to_string()));
}
let entry_count = u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize;
let required_size = entry_count
.checked_mul(4)
.and_then(|n| n.checked_add(8))
.ok_or_else(|| AudexError::ParseError("stco entry count overflow".to_string()))?;
if data.len() < required_size {
return Err(AudexError::ParseError("stco atom truncated".to_string()));
}
let mut new_data = data.clone();
for i in 0..entry_count {
let offset_pos = 8 + i * 4;
let old_offset = u32::from_be_bytes([
data[offset_pos],
data[offset_pos + 1],
data[offset_pos + 2],
data[offset_pos + 3],
]) as u64;
let new_offset = if old_offset > moov_offset {
if size_delta >= 0 {
old_offset.checked_add(size_delta as u64).ok_or_else(|| {
AudexError::ParseError(
"stco offset overflow: new offset exceeds u64 range".to_string(),
)
})?
} else {
let abs_delta = size_delta.unsigned_abs();
if old_offset >= abs_delta {
old_offset - abs_delta
} else {
return Err(AudexError::ParseError(
"offset would become negative after metadata change".to_string(),
));
}
}
} else {
old_offset };
if new_offset > u32::MAX as u64 {
return Err(AudexError::ParseError(
"offset too large for stco, file needs co64 atom conversion".to_string(),
));
}
let new_offset_bytes = (new_offset as u32).to_be_bytes();
new_data[offset_pos..offset_pos + 4].copy_from_slice(&new_offset_bytes);
}
writer.seek(SeekFrom::Start(stco.data_offset))?;
writer.write_all(&new_data)?;
Ok(())
}
fn update_co64_offsets<R: Read + Seek, W: Write + Seek>(
co64: &MP4Atom,
reader: &mut R,
writer: &mut W,
size_delta: i64,
moov_offset: u64,
) -> Result<()> {
let data = co64.read_data(reader)?;
if data.len() < 8 {
return Err(AudexError::ParseError("co64 atom too short".to_string()));
}
let entry_count = u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize;
let required_size = entry_count
.checked_mul(8)
.and_then(|n| n.checked_add(8))
.ok_or_else(|| AudexError::ParseError("co64 entry count overflow".to_string()))?;
if data.len() < required_size {
return Err(AudexError::ParseError("co64 atom truncated".to_string()));
}
let mut new_data = data.clone();
for i in 0..entry_count {
let offset_pos = 8 + i * 8;
let old_offset = u64::from_be_bytes([
data[offset_pos],
data[offset_pos + 1],
data[offset_pos + 2],
data[offset_pos + 3],
data[offset_pos + 4],
data[offset_pos + 5],
data[offset_pos + 6],
data[offset_pos + 7],
]);
let new_offset = if old_offset > moov_offset {
if size_delta >= 0 {
old_offset.checked_add(size_delta as u64).ok_or_else(|| {
AudexError::ParseError(
"co64 offset overflow: new offset exceeds u64 range".to_string(),
)
})?
} else {
let abs_delta = size_delta.unsigned_abs();
if old_offset >= abs_delta {
old_offset - abs_delta
} else {
return Err(AudexError::ParseError(
"offset would become negative after metadata change".to_string(),
));
}
}
} else {
old_offset };
let new_offset_bytes = new_offset.to_be_bytes();
new_data[offset_pos..offset_pos + 8].copy_from_slice(&new_offset_bytes);
}
writer.seek(SeekFrom::Start(co64.data_offset))?;
writer.write_all(&new_data)?;
Ok(())
}
pub fn update_tfhd_offsets<R: Read + Seek, W: Write + Seek>(
&self,
reader: &mut R,
writer: &mut W,
size_delta: i64,
) -> Result<()> {
let tfhd_atoms = self.findall(b"tfhd", true);
for tfhd in tfhd_atoms {
Self::update_single_tfhd_offset(tfhd, reader, writer, size_delta)?;
}
Ok(())
}
fn update_single_tfhd_offset<R: Read + Seek, W: Write + Seek>(
tfhd: &MP4Atom,
reader: &mut R,
writer: &mut W,
size_delta: i64,
) -> Result<()> {
let data = tfhd.read_data(reader)?;
if data.len() < 8 {
return Err(AudexError::ParseError("tfhd atom too short".to_string()));
}
let flags = u32::from_be_bytes([0, data[1], data[2], data[3]]);
let base_data_offset_present = (flags & 0x000001) != 0;
if !base_data_offset_present {
return Ok(()); }
let offset_pos = 8; if data.len() < offset_pos + 8 {
return Err(AudexError::ParseError(
"tfhd atom too short for base data offset".to_string(),
));
}
let old_offset = u64::from_be_bytes([
data[offset_pos],
data[offset_pos + 1],
data[offset_pos + 2],
data[offset_pos + 3],
data[offset_pos + 4],
data[offset_pos + 5],
data[offset_pos + 6],
data[offset_pos + 7],
]);
let new_offset = if size_delta >= 0 {
old_offset.checked_add(size_delta as u64).ok_or_else(|| {
AudexError::ParseError(
"tfhd offset overflow: new offset exceeds u64 range".to_string(),
)
})?
} else {
let abs_delta = size_delta.unsigned_abs();
if old_offset >= abs_delta {
old_offset - abs_delta
} else {
return Err(AudexError::ParseError(
"tfhd offset would become negative".to_string(),
));
}
};
let mut new_data = data.clone();
let new_offset_bytes = new_offset.to_be_bytes();
new_data[offset_pos..offset_pos + 8].copy_from_slice(&new_offset_bytes);
writer.seek(SeekFrom::Start(tfhd.data_offset))?;
writer.write_all(&new_data)?;
Ok(())
}
pub fn needs_offset_update(&self) -> bool {
matches!(&self.name, b"stco" | b"co64" | b"tfhd")
}
fn preserve_failed_atom<R: Read + Seek>(
reader: &mut R,
error: &crate::AudexError,
pre_parse_offset: u64,
) -> Result<Self> {
reader.seek(SeekFrom::Start(pre_parse_offset))?;
let offset = pre_parse_offset;
let mut header = [0u8; 8];
match reader.read_exact(&mut header) {
Ok(()) => {
let raw_length =
u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as u64;
let name = [header[4], header[5], header[6], header[7]];
let (length, data_offset) = if raw_length == 1 {
let mut size_bytes = [0u8; 8];
match reader.read_exact(&mut size_bytes) {
Ok(()) => {
let ext_len = u64::from_be_bytes(size_bytes);
let actual = if ext_len >= 16 { ext_len } else { 16 };
(actual, offset + 16)
}
Err(_) => (8u64, offset + 8),
}
} else {
let actual = if raw_length >= 8 { raw_length } else { 8 };
(actual, offset + 8)
};
Ok(MP4Atom {
atom_type: AtomType::Unknown(name),
name,
length,
offset,
data_offset,
data_length: length.saturating_sub(data_offset - offset),
children: None,
})
}
Err(_) => Err(AudexError::ParseError(format!(
"Cannot preserve failed atom: {}",
error
))),
}
}
pub fn is_movie_fragment(&self) -> bool {
matches!(&self.name, b"moof" | b"mfhd" | b"traf" | b"tfhd" | b"trun")
}
pub fn get_movie_fragments(&self) -> Vec<&MP4Atom> {
let mut fragments = Vec::new();
if self.is_movie_fragment() {
fragments.push(self);
}
if let Some(children) = &self.children {
for child in children {
fragments.extend(child.get_movie_fragments());
}
}
fragments
}
pub fn update_fragment_offsets<R: Read + Seek, W: Write + Seek>(
&self,
reader: &mut R,
writer: &mut W,
size_delta: i64,
moov_offset: u64,
) -> Result<()> {
if size_delta == 0 {
return Ok(());
}
self.update_tfhd_offsets(reader, writer, size_delta)?;
let trun_atoms = self.findall(b"trun", true);
for trun in trun_atoms {
Self::update_trun_offsets(trun, reader, writer, size_delta, moov_offset)?;
}
Ok(())
}
fn update_trun_offsets<R: Read + Seek, W: Write + Seek>(
trun: &MP4Atom,
reader: &mut R,
writer: &mut W,
size_delta: i64,
moov_offset: u64,
) -> Result<()> {
let data = trun.read_data(reader)?;
if data.len() < 8 {
return Err(AudexError::ParseError("trun atom too short".to_string()));
}
let flags = u32::from_be_bytes([0, data[1], data[2], data[3]]);
let data_offset_present = (flags & 0x000001) != 0;
if !data_offset_present {
return Ok(()); }
let _sample_count = u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize;
let offset_pos = 8;
if offset_pos + 4 > data.len() {
return Err(AudexError::ParseError("trun atom truncated".to_string()));
}
let old_offset = i32::from_be_bytes([
data[offset_pos],
data[offset_pos + 1],
data[offset_pos + 2],
data[offset_pos + 3],
]);
let new_offset = if moov_offset > i64::MAX as u64 {
old_offset
} else if (old_offset as i64) > (moov_offset as i64) {
let delta_i32 = i32::try_from(size_delta).map_err(|_| {
AudexError::ParseError(format!(
"trun offset delta {} exceeds i32 range",
size_delta
))
})?;
old_offset.checked_add(delta_i32).ok_or_else(|| {
AudexError::ParseError(format!(
"trun offset overflow: {} + {}",
old_offset, delta_i32
))
})?
} else {
old_offset
};
let mut new_data = data.clone();
let new_offset_bytes = new_offset.to_be_bytes();
new_data[offset_pos..offset_pos + 4].copy_from_slice(&new_offset_bytes);
writer.seek(SeekFrom::Start(trun.data_offset))?;
writer.write_all(&new_data)?;
Ok(())
}
}
#[derive(Debug)]
pub struct Atoms {
pub atoms: Vec<MP4Atom>,
}
impl Atoms {
pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
let mut atoms = Vec::new();
let end = reader
.seek(SeekFrom::End(0))
.map_err(|e| AudexError::ParseError(format!("Failed to seek to end: {}", e)))?;
reader
.seek(SeekFrom::Start(0))
.map_err(|e| AudexError::ParseError(format!("Failed to seek to start: {}", e)))?;
while reader.stream_position().unwrap_or(0) + 8 <= end {
match MP4Atom::parse_with_preservation(reader, 0, true) {
Ok(atom) => atoms.push(atom),
Err(_) => break, }
}
Ok(Atoms { atoms })
}
pub fn path(&self, path_str: &str) -> Option<Vec<&MP4Atom>> {
let parts: Vec<&str> = path_str.split('.').collect();
let mut result = Vec::new();
let mut current_atoms = &self.atoms;
for (i, part) in parts.iter().enumerate() {
let part_bytes = part.as_bytes();
if part_bytes.len() != 4 {
return None;
}
let target = [part_bytes[0], part_bytes[1], part_bytes[2], part_bytes[3]];
let found = current_atoms.iter().find(|atom| atom.name == target)?;
result.push(found);
if i + 1 < parts.len() {
current_atoms = found.children.as_ref()?;
}
}
Some(result)
}
pub fn contains(&self, path_str: &str) -> bool {
self.path(path_str).is_some()
}
pub fn get(&self, path_str: &str) -> Option<&MP4Atom> {
self.path(path_str).and_then(|path| path.last().copied())
}
pub fn get_by_names(&self, names: &[&str]) -> Option<&MP4Atom> {
let mut current_atoms = &self.atoms;
let mut found_atom: Option<&MP4Atom> = None;
for name in names {
let name_bytes = name.as_bytes();
if name_bytes.len() != 4 {
return None;
}
let target = [name_bytes[0], name_bytes[1], name_bytes[2], name_bytes[3]];
found_atom = current_atoms.iter().find(|atom| atom.name == target);
if let Some(atom) = found_atom {
if let Some(children) = &atom.children {
current_atoms = children;
} else if name
!= names
.last()
.expect("names is non-empty; we are iterating over it")
{
return None;
}
} else {
return None;
}
}
found_atom
}
pub fn get_by_binary_path(&self, path: &[u8]) -> Option<&MP4Atom> {
let path_str = std::str::from_utf8(path).ok()?;
self.get(path_str)
}
pub fn path_names(&self, names: &[&str]) -> Option<Vec<&MP4Atom>> {
let mut result = Vec::new();
let mut current_atoms = &self.atoms;
for name in names {
let name_bytes = name.as_bytes();
if name_bytes.len() != 4 {
return None;
}
let target = [name_bytes[0], name_bytes[1], name_bytes[2], name_bytes[3]];
let found = current_atoms.iter().find(|atom| atom.name == target)?;
result.push(found);
if let Some(children) = &found.children {
current_atoms = children;
}
}
Some(result)
}
}
struct FindallIterator<'a> {
stack: Vec<(&'a MP4Atom, bool)>, target_name: [u8; 4],
recursive: bool,
}
impl<'a> FindallIterator<'a> {
fn new(root: &'a MP4Atom, name: &[u8; 4], recursive: bool) -> Self {
let mut stack = Vec::new();
if let Some(children) = &root.children {
for child in children.iter().rev() {
stack.push((child, false));
}
}
Self {
stack,
target_name: *name,
recursive,
}
}
}
impl<'a> Iterator for FindallIterator<'a> {
type Item = &'a MP4Atom;
fn next(&mut self) -> Option<Self::Item> {
while let Some((atom, has_yielded)) = self.stack.pop() {
if !has_yielded && atom.name == self.target_name {
if self.recursive {
self.stack.push((atom, true));
}
return Some(atom);
}
if self.recursive && has_yielded {
if let Some(children) = &atom.children {
for child in children.iter().rev() {
self.stack.push((child, false));
}
}
} else if self.recursive && !has_yielded {
if let Some(children) = &atom.children {
for child in children.iter().rev() {
self.stack.push((child, false));
}
}
}
}
None
}
}
#[cfg(feature = "async")]
mod async_parse {
use super::*;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
impl MP4Atom {
pub fn parse_async(
reader: &mut tokio::fs::File,
level: usize,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self>> + Send + '_>>
{
Box::pin(Self::parse_with_preservation_async(reader, level, true))
}
pub fn parse_with_preservation_async(
reader: &mut tokio::fs::File,
level: usize,
preserve_failed: bool,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self>> + Send + '_>>
{
Self::parse_with_preservation_async_context(reader, level, preserve_failed, false)
}
fn parse_with_preservation_async_context(
reader: &mut tokio::fs::File,
level: usize,
preserve_failed: bool,
in_ilst_children: bool,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self>> + Send + '_>>
{
Box::pin(async move {
let offset = reader.stream_position().await.map_err(|e| {
AudexError::ParseError(format!("Failed to get stream position: {}", e))
})?;
let mut header = [0u8; 8];
reader.read_exact(&mut header).await.map_err(|e| {
AudexError::ParseError(format!("Failed to read atom header: {}", e))
})?;
let mut length =
u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as u64;
let name = [header[4], header[5], header[6], header[7]];
let mut data_offset = offset + 8;
if length == 1 {
let mut size_bytes = [0u8; 8];
reader.read_exact(&mut size_bytes).await.map_err(|e| {
AudexError::ParseError(format!("Failed to read 64-bit atom size: {}", e))
})?;
length = u64::from_be_bytes(size_bytes);
data_offset += 8;
if length < 16 {
return Err(AudexError::ParseError(
"64 bit atom length can only be 16 and higher".to_string(),
));
}
} else if length == 0 {
if level != 0 {
return Err(AudexError::ParseError(
"only a top-level atom can have zero length".to_string(),
));
}
let current_pos = reader.stream_position().await.map_err(|e| {
AudexError::ParseError(format!("Failed to get current position: {}", e))
})?;
let end_pos = reader.seek(SeekFrom::End(0)).await.map_err(|e| {
AudexError::ParseError(format!("Failed to seek to end: {}", e))
})?;
length = end_pos.checked_sub(offset).ok_or_else(|| {
AudexError::ParseError(
"zero-length atom: end of stream is before atom offset".to_string(),
)
})?;
reader
.seek(SeekFrom::Start(current_pos))
.await
.map_err(|e| {
AudexError::ParseError(format!("Failed to seek back: {}", e))
})?;
} else if length < 8 {
return Err(AudexError::ParseError(
"atom length can only be 0, 1 or 8 and higher".to_string(),
));
}
let atom_type = AtomType::from_bytes(&name);
let header_size = data_offset.checked_sub(offset).ok_or_else(|| {
AudexError::ParseError("atom data offset is before atom start".to_string())
})?;
let data_length = length.checked_sub(header_size).ok_or_else(|| {
AudexError::ParseError(
"atom length is smaller than its header size".to_string(),
)
})?;
let mut atom = MP4Atom {
atom_type,
name,
length,
offset,
data_offset,
data_length,
children: None,
};
if is_container_atom(&name, in_ilst_children) {
let mut children = Vec::new();
let skip_bytes = atom_type.skip_size();
let child_in_ilst = name == *b"ilst";
if skip_bytes > 0 {
let mut skip_buffer = vec![0u8; skip_bytes];
reader.read_exact(&mut skip_buffer).await.map_err(|e| {
AudexError::ParseError(format!("Failed to skip bytes: {}", e))
})?;
}
let children_end = offset.checked_add(length).ok_or_else(|| {
AudexError::ParseError("Atom offset + length overflows u64".to_string())
})?;
while reader.stream_position().await.unwrap_or(0) < children_end {
let current_pos = reader.stream_position().await.map_err(|e| {
AudexError::ParseError(format!("Failed to get position: {}", e))
})?;
if current_pos >= children_end {
break;
}
if level + 1 > Self::MAX_ATOM_DEPTH {
return Err(AudexError::DepthLimitExceeded {
max_depth: Self::MAX_ATOM_DEPTH as u32,
});
}
if children.len() >= Self::MAX_CHILDREN_PER_ATOM {
return Err(AudexError::InvalidData(format!(
"Container atom has more than {} children",
Self::MAX_CHILDREN_PER_ATOM,
)));
}
match Self::parse_with_preservation_async_context(
reader,
level + 1,
preserve_failed,
child_in_ilst,
)
.await
{
Ok(child) => children.push(child),
Err(parse_error) => {
if matches!(parse_error, AudexError::DepthLimitExceeded { .. }) {
return Err(parse_error);
}
if preserve_failed {
if let Ok(preserved) = Self::preserve_failed_atom_async(
reader,
&parse_error,
current_pos,
)
.await
{
let atom_end =
preserved.offset.saturating_add(preserved.length);
if reader.seek(SeekFrom::Start(atom_end)).await.is_err() {
children.push(preserved);
break;
}
children.push(preserved);
continue;
}
}
break;
}
}
}
atom.children = Some(children);
} else {
let end_pos = offset.checked_add(length).ok_or_else(|| {
AudexError::ParseError("Atom offset + length overflows u64".to_string())
})?;
reader.seek(SeekFrom::Start(end_pos)).await.map_err(|e| {
AudexError::ParseError(format!("Failed to seek to end of atom: {}", e))
})?;
}
Ok(atom)
})
}
pub async fn read_data_async(&self, reader: &mut tokio::fs::File) -> Result<Vec<u8>> {
crate::limits::ParseLimits::default()
.check_tag_size(self.data_length, "MP4 atom async")?;
let alloc_size = usize::try_from(self.data_length).map_err(|_| {
AudexError::ParseError(format!(
"Atom data length {} exceeds addressable memory",
self.data_length
))
})?;
reader
.seek(SeekFrom::Start(self.data_offset))
.await
.map_err(|e| {
AudexError::ParseError(format!("Failed to seek to atom data: {}", e))
})?;
let mut data = vec![0u8; alloc_size];
reader
.read_exact(&mut data)
.await
.map_err(|e| AudexError::ParseError(format!("Failed to read atom data: {}", e)))?;
Ok(data)
}
async fn preserve_failed_atom_async(
reader: &mut tokio::fs::File,
error: &crate::AudexError,
pre_parse_offset: u64,
) -> Result<Self> {
reader.seek(SeekFrom::Start(pre_parse_offset)).await?;
let offset = pre_parse_offset;
let mut header = [0u8; 8];
match reader.read_exact(&mut header).await {
Ok(_) => {
let raw_length =
u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as u64;
let name = [header[4], header[5], header[6], header[7]];
let (length, data_offset) = if raw_length == 1 {
let mut size_bytes = [0u8; 8];
match reader.read_exact(&mut size_bytes).await {
Ok(_) => {
let ext_len = u64::from_be_bytes(size_bytes);
let actual = if ext_len >= 16 { ext_len } else { 16 };
(actual, offset + 16)
}
Err(_) => (8u64, offset + 8),
}
} else {
let actual = if raw_length >= 8 { raw_length } else { 8 };
(actual, offset + 8)
};
Ok(MP4Atom {
atom_type: AtomType::Unknown(name),
name,
length,
offset,
data_offset,
data_length: length.saturating_sub(data_offset - offset),
children: None,
})
}
Err(_) => Err(AudexError::ParseError(format!(
"Cannot preserve failed atom: {}",
error
))),
}
}
}
impl Atoms {
pub async fn parse_async(reader: &mut tokio::fs::File) -> Result<Self> {
let mut atoms = Vec::new();
let end = reader
.seek(SeekFrom::End(0))
.await
.map_err(|e| AudexError::ParseError(format!("Failed to seek to end: {}", e)))?;
reader
.seek(SeekFrom::Start(0))
.await
.map_err(|e| AudexError::ParseError(format!("Failed to seek to start: {}", e)))?;
while reader.stream_position().await.unwrap_or(0) + 8 <= end {
match MP4Atom::parse_async(reader, 0).await {
Ok(atom) => atoms.push(atom),
Err(_) => break,
}
}
Ok(Atoms { atoms })
}
}
}