use crate::mp4::as_entry::AudioSampleEntry;
use crate::mp4::atom::{Atoms, MP4Atom};
use crate::mp4::util::{name2key, parse_full_atom};
use crate::tags::{Metadata, PaddingInfo, Tags};
use crate::util::{insert_bytes, resize_bytes};
use crate::{AudexError, FileType, Result, StreamInfo};
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{BufReader, Cursor, Read, Seek, SeekFrom, Write};
use std::path::Path;
use std::time::Duration;
const MAX_TOTAL_ILST_DATA_BYTES: u64 = 128 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u32)]
pub enum AtomDataType {
Implicit = 0,
Utf8 = 1,
Utf16 = 2,
Sjis = 3,
Html = 6,
Xml = 7,
Uuid = 8,
Isrc = 9,
Mi3p = 10,
Gif = 12,
Jpeg = 13,
Png = 14,
Url = 15,
Duration = 16,
DateTime = 17,
Genres = 18,
Integer = 21,
RiaaPA = 24,
Upc = 25,
Bmp = 27,
}
impl AtomDataType {
pub fn from_u32(value: u32) -> Option<Self> {
match value {
0 => Some(AtomDataType::Implicit),
1 => Some(AtomDataType::Utf8),
2 => Some(AtomDataType::Utf16),
3 => Some(AtomDataType::Sjis),
6 => Some(AtomDataType::Html),
7 => Some(AtomDataType::Xml),
8 => Some(AtomDataType::Uuid),
9 => Some(AtomDataType::Isrc),
10 => Some(AtomDataType::Mi3p),
12 => Some(AtomDataType::Gif),
13 => Some(AtomDataType::Jpeg),
14 => Some(AtomDataType::Png),
15 => Some(AtomDataType::Url),
16 => Some(AtomDataType::Duration),
17 => Some(AtomDataType::DateTime),
18 => Some(AtomDataType::Genres),
21 => Some(AtomDataType::Integer),
24 => Some(AtomDataType::RiaaPA),
25 => Some(AtomDataType::Upc),
27 => Some(AtomDataType::Bmp),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MP4Cover {
#[cfg_attr(
feature = "serde",
serde(with = "crate::serde_helpers::bytes_as_base64")
)]
pub data: Vec<u8>,
pub imageformat: AtomDataType,
}
impl MP4Cover {
pub const FORMAT_JPEG: AtomDataType = AtomDataType::Jpeg;
pub const FORMAT_PNG: AtomDataType = AtomDataType::Png;
pub fn new(data: Vec<u8>, imageformat: AtomDataType) -> Self {
Self { data, imageformat }
}
pub fn new_jpeg(data: Vec<u8>) -> Self {
Self::new(data, Self::FORMAT_JPEG)
}
pub fn new_png(data: Vec<u8>) -> Self {
Self::new(data, Self::FORMAT_PNG)
}
pub fn detect_format(data: &[u8]) -> Option<AtomDataType> {
if data.len() < 4 {
return None;
}
if data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
return Some(AtomDataType::Jpeg);
}
if data.len() >= 8
&& data[0] == 0x89
&& data[1] == 0x50
&& data[2] == 0x4E
&& data[3] == 0x47
&& data[4] == 0x0D
&& data[5] == 0x0A
&& data[6] == 0x1A
&& data[7] == 0x0A
{
return Some(AtomDataType::Png);
}
if data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x38 {
return Some(AtomDataType::Gif);
}
if data[0] == 0x42 && data[1] == 0x4D {
return Some(AtomDataType::Bmp);
}
None
}
pub fn new_auto_detect(data: Vec<u8>) -> Self {
let imageformat = Self::detect_format(&data).unwrap_or(Self::FORMAT_JPEG);
Self::new(data, imageformat)
}
}
impl AsRef<[u8]> for MP4Cover {
fn as_ref(&self) -> &[u8] {
&self.data
}
}
impl std::ops::Deref for MP4Cover {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.data
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MP4FreeForm {
#[cfg_attr(
feature = "serde",
serde(with = "crate::serde_helpers::bytes_as_base64")
)]
pub data: Vec<u8>,
pub dataformat: AtomDataType,
pub version: u8,
}
impl MP4FreeForm {
pub const FORMAT_DATA: AtomDataType = AtomDataType::Implicit;
pub const FORMAT_TEXT: AtomDataType = AtomDataType::Utf8;
pub fn new(data: Vec<u8>, dataformat: AtomDataType, version: u8) -> Self {
Self {
data,
dataformat,
version,
}
}
pub fn new_text(data: Vec<u8>) -> Self {
Self::new(data, AtomDataType::Utf8, 0)
}
pub fn new_data(data: Vec<u8>) -> Self {
Self::new(data, AtomDataType::Implicit, 0)
}
}
impl AsRef<[u8]> for MP4FreeForm {
fn as_ref(&self) -> &[u8] {
&self.data
}
}
impl std::ops::Deref for MP4FreeForm {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.data
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Chapter {
pub start: f64,
pub title: String,
}
impl Chapter {
pub fn new(start: f64, title: String) -> Self {
Self { start, title }
}
}
#[derive(Debug, Clone, Default)]
pub struct MP4Chapters {
pub chapters: Vec<Chapter>,
pub timescale: Option<u32>,
pub duration: Option<u64>,
}
impl MP4Chapters {
pub fn new() -> Self {
Self::default()
}
pub fn load<R: Read + Seek>(atoms: &Atoms, reader: &mut R) -> Result<Option<Self>> {
if !Self::can_load(atoms) {
return Ok(None);
}
let mut chapters = MP4Chapters::new();
if let Some(mvhd) = atoms.get("moov.mvhd") {
chapters.parse_mvhd(mvhd, reader)?;
}
if chapters.timescale.is_none() {
return Err(AudexError::ParseError(
"Unable to get timescale".to_string(),
));
}
if let Some(chpl) = atoms.get("moov.udta.chpl") {
chapters.parse_chpl(chpl, reader)?;
}
Ok(Some(chapters))
}
pub fn can_load(atoms: &Atoms) -> bool {
atoms.contains("moov.udta.chpl") && atoms.contains("moov.mvhd")
}
fn parse_mvhd<R: Read + Seek>(&mut self, mvhd: &MP4Atom, reader: &mut R) -> Result<()> {
let data = mvhd.read_data(reader)?;
self.parse_mvhd_data(&data)
}
fn parse_mvhd_data(&mut self, data: &[u8]) -> Result<()> {
if data.is_empty() {
return Err(AudexError::ParseError("Invalid mvhd".to_string()));
}
let version = data[0];
let mut pos = 4;
match version {
0 => {
if data.len() < pos + 16 {
return Err(AudexError::ParseError("mvhd too short".to_string()));
}
pos += 8;
self.timescale = Some(u32::from_be_bytes([
data[pos],
data[pos + 1],
data[pos + 2],
data[pos + 3],
]));
pos += 4;
self.duration = Some(u32::from_be_bytes([
data[pos],
data[pos + 1],
data[pos + 2],
data[pos + 3],
]) as u64);
}
1 => {
if data.len() < pos + 24 {
return Err(AudexError::ParseError("mvhd too short".to_string()));
}
pos += 16;
self.timescale = Some(u32::from_be_bytes([
data[pos],
data[pos + 1],
data[pos + 2],
data[pos + 3],
]));
pos += 4;
self.duration = Some(u64::from_be_bytes([
data[pos],
data[pos + 1],
data[pos + 2],
data[pos + 3],
data[pos + 4],
data[pos + 5],
data[pos + 6],
data[pos + 7],
]));
}
_ => {
return Err(AudexError::ParseError(format!(
"Unknown mvhd version {}",
version
)));
}
}
Ok(())
}
fn parse_chpl<R: Read + Seek>(&mut self, chpl: &MP4Atom, reader: &mut R) -> Result<()> {
let data = chpl.read_data(reader)?;
self.parse_chpl_data(&data)
}
pub fn parse_chpl_data(&mut self, data: &[u8]) -> Result<()> {
if data.len() < 9 {
return Err(AudexError::ParseError("Invalid chpl atom".to_string()));
}
let version = data[0];
let (chapters_count, mut pos) = if version >= 1 {
if data.len() < 13 {
return Err(AudexError::ParseError(
"chpl v1 atom too short for 4-byte chapter count".to_string(),
));
}
let count = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize;
let remaining = data.len().saturating_sub(12);
if count > remaining / 9 {
return Err(AudexError::ParseError(
"chpl chapter count exceeds available data".to_string(),
));
}
(count, 12)
} else {
let count = data[8] as usize;
let remaining = data.len().saturating_sub(9);
if count > remaining / 9 {
return Err(AudexError::ParseError(
"chpl chapter count exceeds available data".to_string(),
));
}
(count, 9)
};
for i in 0..chapters_count {
if pos + 8 > data.len() {
return Err(AudexError::ParseError("chpl atom truncated".to_string()));
}
let raw_time = u64::from_be_bytes([
data[pos],
data[pos + 1],
data[pos + 2],
data[pos + 3],
data[pos + 4],
data[pos + 5],
data[pos + 6],
data[pos + 7],
]);
pos += 8;
if pos >= data.len() {
return Err(AudexError::ParseError("chpl atom truncated".to_string()));
}
let title_len = data[pos] as usize;
pos += 1;
if pos + title_len > data.len() {
return Err(AudexError::ParseError("chpl atom truncated".to_string()));
}
let title_bytes = &data[pos..pos + title_len];
let title = String::from_utf8(title_bytes.to_vec())
.map_err(|e| AudexError::ParseError(format!("chapter {} title: {}", i, e)))?;
pos += title_len;
let start_seconds = raw_time as f64 / 10_000_000.0;
self.chapters.push(Chapter::new(start_seconds, title));
}
Ok(())
}
pub fn chapters(&self) -> &[Chapter] {
&self.chapters
}
pub fn chapters_mut(&mut self) -> &mut Vec<Chapter> {
&mut self.chapters
}
pub fn add_chapter(&mut self, chapter: Chapter) {
self.chapters.push(chapter);
}
pub fn clear(&mut self) {
self.chapters.clear();
}
pub fn len(&self) -> usize {
self.chapters.len()
}
pub fn is_empty(&self) -> bool {
self.chapters.is_empty()
}
pub fn iter(&self) -> std::slice::Iter<'_, Chapter> {
self.chapters.iter()
}
pub fn get(&self, index: usize) -> Option<&Chapter> {
self.chapters.get(index)
}
pub fn pprint(&self) -> String {
let mut result = String::new();
for chapter in &self.chapters {
let Ok(duration) = Duration::try_from_secs_f64(chapter.start.max(0.0)) else {
continue;
};
let hours = duration.as_secs() / 3600;
let minutes = (duration.as_secs() % 3600) / 60;
let seconds = duration.as_secs() % 60;
let millis = duration.subsec_millis();
if !result.is_empty() {
result.push('\n');
}
result.push_str(&format!(
"{}:{:02}:{:02}.{:03} {}",
hours, minutes, seconds, millis, chapter.title
));
}
if result.is_empty() {
"chapters=".to_string()
} else {
format!("chapters=\n {}", result.replace('\n', "\n "))
}
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MP4Tags {
pub tags: HashMap<String, Vec<String>>,
pub covers: Vec<MP4Cover>,
pub freeforms: HashMap<String, Vec<MP4FreeForm>>,
pub failed_atoms: HashMap<String, Vec<Vec<u8>>>,
padding: usize,
}
impl MP4Tags {
pub fn new() -> Self {
Self::default()
}
pub fn can_load(atoms: &Atoms) -> bool {
atoms.contains("moov.udta.meta.ilst")
}
pub fn load<R: Read + Seek>(atoms: &Atoms, reader: &mut R) -> Result<Option<Self>> {
if !Self::can_load(atoms) {
return Ok(None);
}
let ilst_path = atoms
.path("moov.udta.meta.ilst")
.ok_or_else(|| AudexError::ParseError("Failed to get ilst path".to_string()))?;
let ilst = ilst_path
.last()
.ok_or_else(|| AudexError::ParseError("Empty ilst path".to_string()))?;
let mut tags = MP4Tags::new();
if let Some(meta_path) = atoms.path("moov.udta.meta") {
if let Some(meta) = meta_path.last() {
if let Some(children) = &meta.children {
for (i, child) in children.iter().enumerate() {
if child.name == *b"ilst" {
if i > 0 && children[i - 1].name == *b"free" {
tags.padding = usize::try_from(children[i - 1].data_length)
.unwrap_or(usize::MAX);
} else if i + 1 < children.len() && children[i + 1].name == *b"free" {
tags.padding = usize::try_from(children[i + 1].data_length)
.unwrap_or(usize::MAX);
}
break;
}
}
}
}
}
if let Some(children) = &ilst.children {
let mut total_ilst_bytes = 0u64;
for child in children {
total_ilst_bytes =
total_ilst_bytes
.checked_add(child.data_length)
.ok_or_else(|| {
AudexError::InvalidData("MP4 metadata size overflow".to_string())
})?;
if total_ilst_bytes > MAX_TOTAL_ILST_DATA_BYTES {
return Err(AudexError::InvalidData(format!(
"MP4 metadata exceeds cumulative {} byte limit",
MAX_TOTAL_ILST_DATA_BYTES
)));
}
let data = child.read_data(reader)?;
if tags.parse_metadata_atom_data(child, &data).is_err() {
let key = crate::mp4::util::name2key(&child.name);
tags.failed_atoms.entry(key).or_default().push(data);
}
}
}
Ok(Some(tags))
}
fn parse_metadata_atom_data(&mut self, atom: &MP4Atom, data: &[u8]) -> Result<()> {
match &atom.name {
b"covr" => self.parse_cover_atom(data)?,
b"----" => self.parse_freeform_atom(data)?,
b"trkn" | b"disk" => self.parse_pair_atom(&atom.name, data)?,
b"cpil" | b"pgap" | b"pcst" => self.parse_bool_atom(&atom.name, data)?,
b"gnre" => self.parse_genre_atom(data)?,
b"plID" => self.parse_integer_atom_bytes(&atom.name, data, 8)?,
b"cnID" | b"geID" | b"atID" | b"sfID" | b"cmID" | b"tvsn" | b"tves" => {
self.parse_integer_atom_bytes(&atom.name, data, 4)?
}
b"tmpo" | b"\xa9mvi" | b"\xa9mvc" => {
self.parse_integer_atom_bytes(&atom.name, data, 2)?
}
b"akID" | b"shwm" | b"stik" | b"hdvd" | b"rtng" => {
self.parse_integer_atom_bytes(&atom.name, data, 1)?
}
_ => {
let is_known_text = matches!(
&atom.name,
b"\xa9nam"
| b"\xa9alb"
| b"\xa9ART"
| b"aART"
| b"\xa9wrt"
| b"\xa9day"
| b"\xa9cmt"
| b"desc"
| b"purd"
| b"\xa9grp"
| b"\xa9gen"
| b"\xa9lyr"
| b"catg"
| b"keyw"
| b"\xa9too"
| b"\xa9pub"
| b"cprt"
| b"soal"
| b"soaa"
| b"soar"
| b"sonm"
| b"soco"
| b"sosn"
| b"tvsh"
| b"\xa9wrk"
| b"\xa9mvn"
| b"purl"
| b"egid"
);
if is_known_text {
self.parse_text_atom(&atom.name, data)?;
} else {
return Err(AudexError::ParseError(format!(
"Unknown atom type: {}",
crate::mp4::util::name2key(&atom.name)
)));
}
}
}
Ok(())
}
fn parse_cover_atom(&mut self, data: &[u8]) -> Result<()> {
const MAX_COVER_IMAGES: usize = 256;
let mut pos = 0;
while pos + 12 <= data.len() {
if self.covers.len() >= MAX_COVER_IMAGES {
break;
}
let length =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
as usize;
if length < 16 || pos.checked_add(length).is_none_or(|end| end > data.len()) {
return Err(AudexError::ParseError(
"invalid covr child atom length".to_string(),
));
}
let name = &data[pos + 4..pos + 8];
if name != b"data" {
if name == b"name" {
pos += length;
continue;
}
return Err(AudexError::ParseError(format!(
"unexpected atom {:?} inside covr",
name
)));
}
let format_raw =
u32::from_be_bytes([data[pos + 8], data[pos + 9], data[pos + 10], data[pos + 11]]);
let imageformat = match AtomDataType::from_u32(format_raw) {
Some(AtomDataType::Jpeg) => AtomDataType::Jpeg,
Some(AtomDataType::Png) => AtomDataType::Png,
Some(AtomDataType::Gif) => AtomDataType::Gif,
Some(AtomDataType::Bmp) => AtomDataType::Bmp,
_ => AtomDataType::Jpeg,
};
let cover_len = length - 16;
crate::limits::ParseLimits::default()
.check_image_size(cover_len as u64, "MP4 covr image")?;
let cover_data = data[pos + 16..pos + length].to_vec();
self.covers.push(MP4Cover::new(cover_data, imageformat));
pos += length;
}
Ok(())
}
fn parse_freeform_atom(&mut self, data: &[u8]) -> Result<()> {
if data.len() < 8 {
return Err(AudexError::ParseError(
"Freeform atom too short".to_string(),
));
}
let mean_length = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
if mean_length < 12 || mean_length > data.len() {
return Err(AudexError::ParseError("Invalid mean length".to_string()));
}
let mean = String::from_utf8(data[12..mean_length].to_vec()).map_err(|_| {
AudexError::ParseError("Freeform atom mean field contains invalid UTF-8".to_string())
})?;
let name_start = mean_length;
if name_start + 8 > data.len() {
return Err(AudexError::ParseError(
"Freeform atom truncated".to_string(),
));
}
let name_length = u32::from_be_bytes([
data[name_start],
data[name_start + 1],
data[name_start + 2],
data[name_start + 3],
]) as usize;
let name_end = name_start.checked_add(name_length).ok_or_else(|| {
AudexError::ParseError("freeform atom name length overflow".to_string())
})?;
if name_length < 12 || name_end > data.len() {
return Err(AudexError::ParseError("Invalid name length".to_string()));
}
let name = String::from_utf8(data[name_start + 12..name_end].to_vec()).map_err(|_| {
AudexError::ParseError("Freeform atom name field contains invalid UTF-8".to_string())
})?;
let mut pos = name_end;
let key = format!("----:{}:{}", mean, name);
let mut values = Vec::new();
while pos + 16 <= data.len() {
let length =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
as usize;
if length < 16 || pos.checked_add(length).is_none_or(|end| end > data.len()) {
return Err(AudexError::ParseError(
"invalid freeform data child atom length".to_string(),
));
}
let atom_name = &data[pos + 4..pos + 8];
if atom_name != b"data" {
return Err(AudexError::ParseError(
"unexpected freeform child atom name".to_string(),
));
}
let version = data[pos + 8];
let flags = u32::from_be_bytes([0, data[pos + 9], data[pos + 10], data[pos + 11]]);
let dataformat = AtomDataType::from_u32(flags).unwrap_or(AtomDataType::Implicit);
let value_data = data[pos + 16..pos + length].to_vec();
values.push(MP4FreeForm::new(value_data, dataformat, version));
pos += length;
}
if !values.is_empty() {
self.freeforms.entry(key).or_default().extend(values);
}
Ok(())
}
fn parse_pair_atom(&mut self, name: &[u8; 4], data: &[u8]) -> Result<()> {
let mut values = Vec::new();
let mut pos = 0;
while pos + 12 <= data.len() {
let length =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
as usize;
if length < 16 || pos.checked_add(length).is_none_or(|end| end > data.len()) {
break;
}
if &data[pos + 4..pos + 8] == b"data" && length >= 22 {
let track = u16::from_be_bytes([data[pos + 18], data[pos + 19]]);
let total = u16::from_be_bytes([data[pos + 20], data[pos + 21]]);
if total > 0 {
values.push(format!("{}/{}", track, total));
} else {
values.push(track.to_string());
}
}
pos += length;
}
if !values.is_empty() {
let key = name2key(name);
self.tags.entry(key).or_default().extend(values);
}
Ok(())
}
fn parse_bool_atom(&mut self, name: &[u8; 4], data: &[u8]) -> Result<()> {
let mut pos = 0;
while pos + 12 <= data.len() {
let length =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
as usize;
if length < 17 || pos.checked_add(length).is_none_or(|end| end > data.len()) {
return Err(AudexError::ParseError(
"invalid boolean child atom length".to_string(),
));
}
if &data[pos + 4..pos + 8] == b"data" {
let value = data[pos + 16] != 0;
let key = name2key(name);
self.tags.insert(key, vec![value.to_string()]);
break;
}
pos += length;
}
Ok(())
}
fn parse_text_atom(&mut self, name: &[u8; 4], data: &[u8]) -> Result<()> {
let mut values = Vec::new();
let mut pos = 0;
while pos + 12 <= data.len() {
let length =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
as usize;
if length < 16 || pos.checked_add(length).is_none_or(|end| end > data.len()) {
return Err(AudexError::ParseError(
"invalid text child atom length".to_string(),
));
}
if &data[pos + 4..pos + 8] == b"data" {
let data_type = u32::from_be_bytes([
data[pos + 8],
data[pos + 9],
data[pos + 10],
data[pos + 11],
]);
if data_type == 0 || data_type == 1 {
let text_data = &data[pos + 16..pos + length];
let text = String::from_utf8(text_data.to_vec())
.map_err(|e| {
AudexError::ParseError(format!("Non-UTF-8 text data in atom: {}", e))
})?
.trim_end_matches('\0')
.to_string();
if !text.is_empty() {
values.push(text);
}
} else {
return Err(AudexError::ParseError(format!(
"Invalid data type {} for text atom",
data_type
)));
}
}
pos += length;
}
if !values.is_empty() {
let key = name2key(name);
self.tags.entry(key).or_default().extend(values);
}
Ok(())
}
fn parse_genre_atom(&mut self, data: &[u8]) -> Result<()> {
let mut values = Vec::new();
let mut pos = 0;
while pos + 12 <= data.len() {
let length =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
as usize;
if length < 18 || pos.checked_add(length).is_none_or(|end| end > data.len()) {
break;
}
if &data[pos + 4..pos + 8] == b"data" && length >= 18 {
let genre_id = u16::from_be_bytes([data[pos + 16], data[pos + 17]]);
if genre_id > 0 && genre_id <= 255 {
let genre_name =
crate::constants::get_genre((genre_id - 1) as u8).ok_or_else(|| {
AudexError::ParseError(format!("unknown genre id: {}", genre_id))
})?;
values.push(genre_name.to_string());
} else {
return Err(AudexError::ParseError("invalid genre".to_string()));
}
}
pos += length;
}
if !values.is_empty() {
let key = crate::mp4::util::name2key(b"\xa9gen"); self.tags.insert(key, values);
}
Ok(())
}
fn parse_integer_atom_bytes(
&mut self,
name: &[u8; 4],
data: &[u8],
min_bytes: usize,
) -> Result<()> {
let mut values = Vec::new();
let mut pos = 0;
while pos + 12 <= data.len() {
let length =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
as usize;
if length < 17 || pos.checked_add(length).is_none_or(|end| end > data.len()) {
return Err(AudexError::ParseError(
"invalid integer child atom length".to_string(),
));
}
if &data[pos + 4..pos + 8] == b"data" {
let version = data[pos + 8];
let flags = u32::from_be_bytes([0, data[pos + 9], data[pos + 10], data[pos + 11]]);
if version != 0 {
return Err(AudexError::ParseError("unsupported version".to_string()));
}
if flags != AtomDataType::Implicit as u32 && flags != AtomDataType::Integer as u32 {
return Err(AudexError::ParseError("unsupported type".to_string()));
}
let data_len = length - 16;
if data_len < min_bytes {
return Err(AudexError::ParseError(format!(
"integer atom too small: got {} bytes, expected at least {}",
data_len, min_bytes
)));
}
let value = match data_len {
1 => (data[pos + 16] as i8) as i64,
2 => i16::from_be_bytes([data[pos + 16], data[pos + 17]]) as i64,
3 => {
let b0 = data[pos + 16];
let b1 = data[pos + 17];
let b2 = data[pos + 18];
let sign = if b0 & 0x80 != 0 { 0xFF } else { 0x00 };
i32::from_be_bytes([sign, b0, b1, b2]) as i64
}
4 => i32::from_be_bytes([
data[pos + 16],
data[pos + 17],
data[pos + 18],
data[pos + 19],
]) as i64,
8 => i64::from_be_bytes([
data[pos + 16],
data[pos + 17],
data[pos + 18],
data[pos + 19],
data[pos + 20],
data[pos + 21],
data[pos + 22],
data[pos + 23],
]),
_ => {
return Err(AudexError::ParseError(format!(
"invalid value size {}",
data_len
)));
}
};
values.push(value.to_string());
}
pos += length;
}
if !values.is_empty() {
let key = crate::mp4::util::name2key(name);
self.tags.insert(key, values);
}
Ok(())
}
pub fn get_first(&self, key: &str) -> Option<&String> {
self.tags.get(key)?.first()
}
pub fn set_single(&mut self, key: &str, value: String) {
self.tags.insert(key.to_string(), vec![value]);
}
pub fn add_value(&mut self, key: &str, value: String) {
self.tags.entry(key.to_string()).or_default().push(value);
}
pub fn padding(&self) -> usize {
self.padding
}
pub fn set_padding(&mut self, padding: usize) {
self.padding = padding;
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
debug_event!("saving MP4 tags");
let path = path.as_ref();
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(AudexError::Io)?;
let atoms = Atoms::parse(&mut file)
.map_err(|e| AudexError::ParseError(format!("Failed to parse MP4 atoms: {}", e)))?;
let new_ilst_data = self
.render_ilst()
.map_err(|e| AudexError::ParseError(format!("Failed to render metadata: {}", e)))?;
trace_event!(ilst_bytes = new_ilst_data.len(), "rendered MP4 ilst atom");
if let Some(ilst_path) = atoms.path("moov.udta.meta.ilst") {
self.save_existing(&mut file, &atoms, &ilst_path, &new_ilst_data)?;
} else {
self.save_new(&mut file, &atoms, &new_ilst_data)?;
}
Ok(())
}
pub fn save_to_writer(&self, writer: &mut dyn crate::ReadWriteSeek) -> Result<()> {
let result = self.render_to_vec(writer)?;
writer.seek(SeekFrom::Start(0))?;
writer.write_all(&result)?;
let written_end = writer.stream_position()?;
crate::util::truncate_writer_dyn(writer, written_end)?;
Ok(())
}
pub fn save_to<W: Read + Write + Seek + std::any::Any>(&self, writer: &mut W) -> Result<()> {
let result = self.render_to_vec(writer)?;
writer.seek(SeekFrom::Start(0))?;
writer.write_all(&result)?;
let written_end = writer.stream_position()?;
crate::util::truncate_writer(writer, written_end)?;
Ok(())
}
fn render_to_vec(&self, writer: &mut dyn crate::ReadWriteSeek) -> Result<Vec<u8>> {
let file_size = writer.seek(SeekFrom::End(0))?;
let max_read_size = crate::limits::MAX_IN_MEMORY_WRITER_FILE;
if file_size > max_read_size {
return Err(AudexError::InvalidData(format!(
"File size ({} bytes) exceeds maximum for in-memory MP4 save ({} bytes)",
file_size, max_read_size
)));
}
writer.seek(SeekFrom::Start(0))?;
let mut data = Vec::new();
writer.read_to_end(&mut data)?;
let mut cursor = Cursor::new(data);
let atoms = Atoms::parse(&mut cursor)
.map_err(|e| AudexError::ParseError(format!("Failed to parse MP4 atoms: {}", e)))?;
let new_ilst_data = self
.render_ilst()
.map_err(|e| AudexError::ParseError(format!("Failed to render metadata: {}", e)))?;
if let Some(ilst_path) = atoms.path("moov.udta.meta.ilst") {
self.save_existing(&mut cursor, &atoms, &ilst_path, &new_ilst_data)?;
} else {
self.save_new(&mut cursor, &atoms, &new_ilst_data)?;
}
Ok(cursor.into_inner())
}
fn save_existing<F: Read + Write + Seek + 'static>(
&self,
file: &mut F,
atoms: &Atoms,
ilst_path: &[&MP4Atom],
new_ilst_data: &[u8],
) -> Result<()> {
let ilst = ilst_path
.last()
.ok_or_else(|| AudexError::ParseError("Empty atom path for ilst".to_string()))?;
let mut offset = ilst.offset;
let mut length = ilst.length;
let free_atom = self.find_padding(ilst_path);
if let Some(free) = free_atom {
offset = std::cmp::min(offset, free.offset);
length = length.checked_add(free.length).ok_or_else(|| {
AudexError::InvalidData("ilst + free atom length overflows u64".to_string())
})?;
}
let padding_overhead = 8;
let file_size = file.seek(SeekFrom::End(0))?;
file.seek(SeekFrom::Start(0))?;
let safe_length = std::cmp::min(length, file_size);
let ilst_header_size: i64 = 8;
let file_size_i64 = i64::try_from(file_size).map_err(|_| {
crate::AudexError::InvalidData("file size exceeds maximum supported value".into())
})?;
let offset_i64 = i64::try_from(offset).map_err(|_| {
crate::AudexError::InvalidData("atom offset exceeds maximum supported value".into())
})?;
let safe_length_i64 = i64::try_from(safe_length).map_err(|_| {
crate::AudexError::InvalidData("atom length exceeds maximum supported value".into())
})?;
let content_size = (file_size_i64 - (offset_i64 + safe_length_i64)).max(0);
let padding_size =
safe_length_i64 - (new_ilst_data.len() as i64 + ilst_header_size + padding_overhead);
let info = PaddingInfo::new(padding_size, content_size);
const MAX_PADDING: usize = 10 * 1024 * 1024;
let new_padding = usize::try_from(info.get_default_padding().max(0))
.unwrap_or(0)
.min(MAX_PADDING);
let ilst_atom = MP4Atom::render(b"ilst", new_ilst_data)?;
let mut final_data = ilst_atom;
if new_padding > 0 {
let free_data = vec![0u8; new_padding];
let free_atom = MP4Atom::render(b"free", &free_data)?;
final_data.extend_from_slice(&free_atom);
}
resize_bytes(file, safe_length, final_data.len() as u64, offset)?;
let delta = i64::try_from(final_data.len()).map_err(|_| {
crate::AudexError::InvalidData(
"output data size exceeds maximum supported value".into(),
)
})? - safe_length_i64;
file.seek(SeekFrom::Start(offset))?;
file.write_all(&final_data)?;
self.update_parents(file, &ilst_path[..ilst_path.len() - 1], delta, offset)?;
if delta == 0 && safe_length != final_data.len() as u64 {
return Err(AudexError::InvalidOperation(
"MP4 save encountered an unexpected resize state".to_string(),
));
}
self.update_offsets(file, atoms, delta, offset)?;
Ok(())
}
fn save_new<F: Read + Write + Seek + 'static>(
&self,
file: &mut F,
atoms: &Atoms,
new_ilst_data: &[u8],
) -> Result<()> {
let hdlr = MP4Atom::render(
b"hdlr",
&[
0, 0, 0, 0, 0, 0, 0, 0, b'm', b'd', b'i', b'r', b'a', b'p', b'p', b'l', 0, 0, 0, 0, 0, 0, 0, 0, 0, ],
)?;
if new_ilst_data.is_empty() {
return Err(AudexError::InvalidData(
"Cannot save empty metadata to MP4".to_string(),
));
}
let ilst_atom = MP4Atom::render(b"ilst", new_ilst_data)?;
let meta_data = [&[0u8; 4], &hdlr[..], &ilst_atom[..]].concat();
let path = if let Some(udta_path) = atoms.path("moov.udta") {
udta_path
} else {
atoms
.path("moov")
.ok_or_else(|| AudexError::ParseError("No moov atom found".to_string()))?
};
let last_atom = path
.last()
.ok_or_else(|| AudexError::ParseError("Empty atom path for moov".to_string()))?;
let header_size = last_atom
.data_offset
.checked_sub(last_atom.offset)
.ok_or_else(|| {
AudexError::ParseError("atom data_offset is before atom start".to_string())
})?;
let offset = last_atom.offset.checked_add(header_size).ok_or_else(|| {
AudexError::ParseError("moov atom offset too large to add header size".to_string())
})?;
let file_size = file.seek(SeekFrom::End(0)).map_err(AudexError::Io)?;
let content_size = file_size.checked_sub(offset).ok_or_else(|| {
AudexError::ParseError(
"moov offset exceeds file size, file may be truncated".to_string(),
)
})?;
let content_size_i64 = i64::try_from(content_size).map_err(|_| {
AudexError::InvalidData(
"content size exceeds maximum supported value for padding calculation".to_string(),
)
})?;
let meta_data_len_i64 = i64::try_from(meta_data.len()).map_err(|_| {
AudexError::InvalidData("metadata size exceeds maximum supported value".to_string())
})?;
let padding_size = -meta_data_len_i64;
let info = PaddingInfo::new(padding_size, content_size_i64);
const MAX_PADDING: usize = 10 * 1024 * 1024;
let new_padding = usize::try_from(info.get_default_padding().max(0))
.unwrap_or(0)
.min(MAX_PADDING);
let free_atom = if new_padding > 0 {
MP4Atom::render(b"free", &vec![0u8; new_padding])?
} else {
Vec::new()
};
let meta = MP4Atom::render(b"meta", &[&meta_data[..], &free_atom[..]].concat())?;
let data = if last_atom.name != *b"udta" {
MP4Atom::render(b"udta", &meta)?
} else {
meta
};
insert_bytes(file, data.len() as u64, offset, None)?;
file.seek(SeekFrom::Start(offset))?;
file.write_all(&data)?;
let data_len_i64 = i64::try_from(data.len()).map_err(|_| {
AudexError::InvalidData(
"inserted data size exceeds maximum supported value".to_string(),
)
})?;
self.update_parents(file, &path, data_len_i64, offset)?;
self.update_offsets(file, atoms, data_len_i64, offset)?;
Ok(())
}
fn find_padding<'a>(&self, ilst_path: &[&'a MP4Atom]) -> Option<&'a MP4Atom> {
if ilst_path.len() < 2 {
return None;
}
let meta = ilst_path[ilst_path.len() - 2]; if let Some(children) = &meta.children {
for (i, child) in children.iter().enumerate() {
if child.name == *b"ilst" {
if i > 0 && children[i - 1].name == *b"free" {
return Some(&children[i - 1]);
}
if i + 1 < children.len() && children[i + 1].name == *b"free" {
return Some(&children[i + 1]);
}
break;
}
}
}
None
}
fn update_parents<F: Read + Write + Seek>(
&self,
file: &mut F,
path: &[&MP4Atom],
delta: i64,
resize_offset: u64,
) -> Result<()> {
if delta == 0 {
return Ok(());
}
for atom in path {
let mut actual_offset = atom.offset;
if actual_offset > resize_offset {
actual_offset = (actual_offset as i64)
.checked_add(delta)
.filter(|&v| v >= 0)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Parent atom offset underflow: {} + {}",
atom.offset, delta
))
})? as u64;
}
file.seek(SeekFrom::Start(actual_offset))?;
let mut size_bytes = [0u8; 4];
file.read_exact(&mut size_bytes)?;
let size = u32::from_be_bytes(size_bytes);
if size == 1 {
file.seek(SeekFrom::Start(actual_offset + 8))?;
let mut size_bytes = [0u8; 8];
file.read_exact(&mut size_bytes)?;
let size = u64::from_be_bytes(size_bytes);
let size_i64 = i64::try_from(size).map_err(|_| {
AudexError::ParseError(format!(
"64-bit atom size {} exceeds maximum representable value",
size
))
})?;
let new_size =
size_i64
.checked_add(delta)
.filter(|&s| s >= 8)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Atom size overflow: {} + {} produces invalid size",
size, delta
))
})?;
file.seek(SeekFrom::Start(actual_offset + 8))?;
file.write_all(&new_size.to_be_bytes())?;
} else {
let new_size = (size as i64)
.checked_add(delta)
.filter(|&s| s >= 8 && s <= u32::MAX as i64)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Atom size overflow: {} + {} produces invalid 32-bit size",
size, delta
))
})? as u32;
file.seek(SeekFrom::Start(actual_offset))?;
file.write_all(&new_size.to_be_bytes())?;
}
}
Ok(())
}
fn update_offsets<F: Read + Write + Seek>(
&self,
file: &mut F,
atoms: &Atoms,
delta: i64,
offset: u64,
) -> Result<()> {
if delta == 0 {
return Ok(());
}
if let Some(moov) = atoms.atoms.iter().find(|a| a.name == *b"moov") {
for atom in moov.findall(b"stco", true) {
self.update_offset_table(file, atom, delta, offset, false)?;
}
for atom in moov.findall(b"co64", true) {
self.update_offset_table(file, atom, delta, offset, true)?;
}
}
if let Some(moof) = atoms.atoms.iter().find(|a| a.name == *b"moof") {
for atom in moof.findall(b"tfhd", true) {
self.update_tfhd(file, atom, delta, offset)?;
}
}
Ok(())
}
fn update_offset_table<F: Read + Write + Seek>(
&self,
file: &mut F,
atom: &crate::mp4::atom::MP4Atom,
delta: i64,
offset: u64,
is_64bit: bool,
) -> Result<()> {
let mut atom_offset = atom.offset;
if atom_offset > offset {
atom_offset = (atom_offset as i64)
.checked_add(delta)
.filter(|&v| v >= 0)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Offset table atom position underflow: {} + {}",
atom_offset, delta
))
})? as u64;
}
file.seek(std::io::SeekFrom::Start(atom_offset + 12))
.map_err(|e| AudexError::ParseError(format!("Seek failed: {}", e)))?;
let mut count_buf = [0u8; 4];
file.read_exact(&mut count_buf)
.map_err(|e| AudexError::ParseError(format!("Read failed: {}", e)))?;
let count = u32::from_be_bytes(count_buf) as usize;
let entry_size = if is_64bit { 8 } else { 4 };
let header_overhead: u64 = 16; let max_data_bytes = atom.length.saturating_sub(header_overhead);
let max_entries = max_data_bytes / entry_size as u64;
if (count as u64) > max_entries {
return Err(AudexError::ParseError(format!(
"Offset table entry count ({}) exceeds atom capacity ({} entries fit in {} bytes)",
count, max_entries, max_data_bytes
)));
}
let alloc_size = count.checked_mul(entry_size).ok_or_else(|| {
AudexError::ParseError(format!(
"Offset table allocation overflow: {} entries * {} bytes",
count, entry_size
))
})?;
let mut data = vec![0u8; alloc_size];
file.read_exact(&mut data)
.map_err(|e| AudexError::ParseError(format!("Read offsets failed: {}", e)))?;
for i in 0..count {
let pos = i * entry_size;
if is_64bit {
let o = u64::from_be_bytes([
data[pos],
data[pos + 1],
data[pos + 2],
data[pos + 3],
data[pos + 4],
data[pos + 5],
data[pos + 6],
data[pos + 7],
]);
if o > offset {
let o_i64 = i64::try_from(o).map_err(|_| {
AudexError::ParseError(format!(
"64-bit chunk offset {} exceeds maximum representable value",
o
))
})?;
let new_o = o_i64
.checked_add(delta)
.filter(|&v| v >= 0)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Chunk offset underflow: {} + {} is negative",
o, delta
))
})? as u64;
data[pos..pos + 8].copy_from_slice(&new_o.to_be_bytes());
}
} else {
let o =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
if (o as u64) > offset {
let new_o = (o as i64)
.checked_add(delta)
.filter(|&v| v >= 0 && v <= u32::MAX as i64)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Chunk offset underflow: {} + {} is invalid for 32-bit",
o, delta
))
})? as u32;
data[pos..pos + 4].copy_from_slice(&new_o.to_be_bytes());
}
}
}
file.seek(std::io::SeekFrom::Start(atom_offset + 16))
.map_err(|e| AudexError::ParseError(format!("Seek failed: {}", e)))?;
file.write_all(&data)
.map_err(|e| AudexError::ParseError(format!("Write offsets failed: {}", e)))?;
Ok(())
}
fn update_tfhd<F: Read + Write + Seek>(
&self,
file: &mut F,
atom: &crate::mp4::atom::MP4Atom,
delta: i64,
offset: u64,
) -> Result<()> {
let mut atom_offset = atom.offset;
if atom_offset > offset {
atom_offset = (atom_offset as i64)
.checked_add(delta)
.filter(|&v| v >= 0)
.ok_or_else(|| {
AudexError::ParseError(format!(
"tfhd atom offset underflow: {} + {}",
atom_offset, delta
))
})? as u64;
}
file.seek(std::io::SeekFrom::Start(atom_offset + 9))
.map_err(|e| AudexError::ParseError(format!("Seek failed: {}", e)))?;
let atom_len = usize::try_from(atom.length).map_err(|_| {
AudexError::ParseError(format!(
"Atom length {} exceeds addressable range",
atom.length
))
})?;
if atom_len < 12 {
return Err(AudexError::ParseError(format!(
"tfhd atom too short for version and flags: {} bytes",
atom.length
)));
}
let alloc_size = atom_len - 9;
let limits = crate::limits::ParseLimits::default();
if alloc_size as u64 > limits.max_tag_size {
return Err(AudexError::ParseError(format!(
"tfhd atom data size ({} bytes) exceeds maximum allowed ({})",
alloc_size, limits.max_tag_size
)));
}
let mut data = vec![0u8; alloc_size];
file.read_exact(&mut data)
.map_err(|e| AudexError::ParseError(format!("Read tfhd failed: {}", e)))?;
let flags = u32::from_be_bytes([0, data[0], data[1], data[2]]);
if flags & 1 != 0 {
if data.len() >= 15 {
let o = u64::from_be_bytes([
data[7], data[8], data[9], data[10], data[11], data[12], data[13], data[14],
]);
if o > offset {
let o_i64 = i64::try_from(o).map_err(|_| {
AudexError::ParseError(format!(
"64-bit tfhd base_data_offset {} exceeds maximum representable value",
o
))
})?;
let new_o = o_i64
.checked_add(delta)
.filter(|&v| v >= 0)
.ok_or_else(|| {
AudexError::ParseError(format!(
"tfhd base_data_offset underflow: {} + {}",
o, delta
))
})? as u64;
file.seek(std::io::SeekFrom::Start(atom_offset + 16))
.map_err(|e| AudexError::ParseError(format!("Seek failed: {}", e)))?;
file.write_all(&new_o.to_be_bytes())
.map_err(|e| AudexError::ParseError(format!("Write tfhd failed: {}", e)))?;
}
}
}
Ok(())
}
pub fn render_ilst(&self) -> Result<Vec<u8>> {
let mut ilst_data = Vec::new();
let mut tags_with_vendor = self.tags.clone();
let has_content =
!tags_with_vendor.is_empty() || !self.freeforms.is_empty() || !self.covers.is_empty();
if has_content && !tags_with_vendor.contains_key("©too") {
tags_with_vendor.insert(
"©too".to_string(),
vec![format!("Audex {}", crate::VERSION_STRING)],
);
}
let itunes_order = [
"©nam", "©ART", "aART", "©wrt", "©alb", "©gen", "gnre", "©day", "trkn", "disk", "©cmt",
"©lyr", "©grp", "©too", "cpil", "pgap", "pcst", "tmpo", "stik", "hdvd", "rtng",
"covr", "catg", "keyw", "purd", "purl", "egid", "soal", "soaa", "soar", "sonm", "soco", "sosn",
"tvsh", "©mvn", "©wrk", "plID", "cnID", "geID", "atID", "sfID", "cmID", "akID", "tvsn", "tves", "©mvi", "©mvc",
"shwm",
];
let mut skipped_keys: Vec<String> = Vec::new();
for ordered_key in &itunes_order {
if *ordered_key == "covr" {
if !self.covers.is_empty() {
let cover_data = self.render_cover_atom()?;
ilst_data.extend_from_slice(&cover_data);
}
} else if let Some(values) = tags_with_vendor.get(*ordered_key) {
match self.render_metadata_atom(ordered_key, values) {
Ok(atom_data) => ilst_data.extend_from_slice(&atom_data),
Err(_e) => {
warn_event!("Failed to render metadata key '{}': {}", ordered_key, _e);
skipped_keys.push(ordered_key.to_string());
}
}
}
}
let mut remaining_keys: Vec<&String> = tags_with_vendor
.keys()
.filter(|k| !itunes_order.contains(&k.as_str()))
.collect();
remaining_keys.sort();
for key in remaining_keys {
let values = &tags_with_vendor[key];
match self.render_metadata_atom(key, values) {
Ok(atom_data) => ilst_data.extend_from_slice(&atom_data),
Err(_e) => {
warn_event!("Failed to render metadata key '{}': {}", key, _e);
skipped_keys.push(key.clone());
}
}
}
let mut freeform_keys: Vec<_> = self.freeforms.keys().collect();
freeform_keys.sort();
for key in freeform_keys {
if let Some(values) = self.freeforms.get(key) {
let freeform_data = self.render_freeform_atom(key, values)?;
ilst_data.extend_from_slice(&freeform_data);
}
}
let mut failed_keys: Vec<_> = self.failed_atoms.keys().collect();
failed_keys.sort();
for key in failed_keys {
let data_list = &self.failed_atoms[key];
let name_bytes = crate::mp4::util::key2name(key)?;
if name_bytes != b"----" && self.tags.contains_key(key) {
continue;
}
for data in data_list {
if name_bytes.len() == 4 {
let name = [name_bytes[0], name_bytes[1], name_bytes[2], name_bytes[3]];
let atom_data = MP4Atom::render(&name, data)?;
ilst_data.extend_from_slice(&atom_data);
}
}
}
if !skipped_keys.is_empty() {
return Err(AudexError::InvalidData(format!(
"failed to render {} metadata tag(s): {}",
skipped_keys.len(),
skipped_keys.join(", ")
)));
}
Ok(ilst_data)
}
fn render_metadata_atom(&self, key: &str, values: &[String]) -> Result<Vec<u8>> {
let name_bytes = crate::mp4::util::key2name(key)?;
if name_bytes.len() != 4 {
return Err(AudexError::ParseError(format!(
"Invalid key length: {}",
key
)));
}
let name = [name_bytes[0], name_bytes[1], name_bytes[2], name_bytes[3]];
let mut atom_data = Vec::new();
for value in values {
let data_atom = match key {
"trkn" | "disk" => self.render_pair_data(&name, value)?,
"cpil" | "pgap" | "pcst" => self.render_bool_data(&name, value)?,
"gnre" => self.render_genre_data(value)?,
key if self.is_integer_atom(key) => self.render_integer_data(&name, value)?,
_ => self.render_text_data(&name, value)?,
};
atom_data.extend_from_slice(&data_atom);
}
MP4Atom::render(&name, &atom_data)
}
fn is_integer_atom(&self, key: &str) -> bool {
matches!(
key,
"plID"
| "cnID"
| "geID"
| "atID"
| "sfID"
| "cmID"
| "tvsn"
| "tves"
| "tmpo"
| "©mvi"
| "©mvc"
| "akID"
| "shwm"
| "stik"
| "hdvd"
| "rtng"
)
}
fn render_text_data(&self, _name: &[u8; 4], value: &str) -> Result<Vec<u8>> {
let text_bytes = value.as_bytes();
let mut data = Vec::new();
let atom_size = u32::try_from(text_bytes.len() + 16)
.map_err(|_| AudexError::InvalidData("Text data too large for MP4 atom".to_string()))?;
data.extend_from_slice(&atom_size.to_be_bytes());
data.extend_from_slice(b"data");
data.push(0); data.extend_from_slice(&(AtomDataType::Utf8 as u32).to_be_bytes()[1..4]); data.extend_from_slice(&[0u8; 4]); data.extend_from_slice(text_bytes);
Ok(data)
}
fn render_integer_data(&self, name: &[u8; 4], value: &str) -> Result<Vec<u8>> {
let int_value: i64 = value
.parse()
.map_err(|_| AudexError::ParseError(format!("Invalid integer: {}", value)))?;
let min_bytes: u8 = match name {
b"plID" => 8,
b"cnID" | b"geID" | b"atID" | b"sfID" | b"cmID" | b"tvsn" | b"tves" => 4,
b"tmpo" | b"\xa9mvi" | b"\xa9mvc" => 2,
_ => 1, };
let bytes = if (-128..=127).contains(&int_value) && min_bytes <= 1 {
vec![int_value as u8]
} else if (-32768..=32767).contains(&int_value) && min_bytes <= 2 {
(int_value as i16).to_be_bytes().to_vec()
} else if (-2147483648..=2147483647).contains(&int_value) && min_bytes <= 4 {
(int_value as i32).to_be_bytes().to_vec()
} else if min_bytes <= 8 {
int_value.to_be_bytes().to_vec()
} else {
return Err(AudexError::ParseError(format!(
"Integer value {} out of range for atom {:?}",
int_value,
std::str::from_utf8(name).unwrap_or("????")
)));
};
let mut data = Vec::new();
let atom_size = u32::try_from(bytes.len() + 16).map_err(|_| {
AudexError::InvalidData("Integer data too large for MP4 atom".to_string())
})?;
data.extend_from_slice(&atom_size.to_be_bytes());
data.extend_from_slice(b"data");
data.push(0); data.extend_from_slice(&(AtomDataType::Integer as u32).to_be_bytes()[1..4]); data.extend_from_slice(&[0u8; 4]); data.extend_from_slice(&bytes);
Ok(data)
}
fn render_pair_data(&self, name: &[u8; 4], value: &str) -> Result<Vec<u8>> {
let parts: Vec<&str> = value.split('/').collect();
let track: u16 = parts[0]
.parse()
.map_err(|_| AudexError::ParseError(format!("Invalid track number: {}", parts[0])))?;
let total: u16 = if parts.len() > 1 {
parts[1]
.parse()
.map_err(|_| AudexError::ParseError(format!("Invalid total: {}", parts[1])))?
} else {
0
};
let has_trailing_pad = name == b"trkn";
let payload_size: u32 = if has_trailing_pad { 8 } else { 6 };
let mut data = Vec::new();
data.extend_from_slice(&(16 + payload_size).to_be_bytes());
data.extend_from_slice(b"data");
data.push(0); data.extend_from_slice(&(AtomDataType::Implicit as u32).to_be_bytes()[1..4]); data.extend_from_slice(&[0u8; 4]); data.extend_from_slice(&[0u8; 2]); data.extend_from_slice(&track.to_be_bytes());
data.extend_from_slice(&total.to_be_bytes());
if has_trailing_pad {
data.extend_from_slice(&[0u8; 2]);
}
Ok(data)
}
fn render_bool_data(&self, _name: &[u8; 4], value: &str) -> Result<Vec<u8>> {
let bool_value = match value.to_lowercase().as_str() {
"true" | "1" | "yes" => 1u8,
"false" | "0" | "no" => 0u8,
_ => {
return Err(AudexError::ParseError(format!(
"Invalid boolean: {}",
value
)));
}
};
let mut data = Vec::new();
data.extend_from_slice(&17u32.to_be_bytes()); data.extend_from_slice(b"data");
data.push(0); data.extend_from_slice(&(AtomDataType::Integer as u32).to_be_bytes()[1..4]); data.extend_from_slice(&[0u8; 4]); data.push(bool_value);
Ok(data)
}
fn render_genre_data(&self, value: &str) -> Result<Vec<u8>> {
self.render_text_data(b"\xa9gen", value)
}
fn render_cover_atom(&self) -> Result<Vec<u8>> {
let mut covr_data = Vec::new();
for cover in &self.covers {
let mut data = Vec::new();
let atom_size = u32::try_from(cover.data.len() + 16).map_err(|_| {
AudexError::InvalidData("Cover data too large for MP4 atom".to_string())
})?;
data.extend_from_slice(&atom_size.to_be_bytes());
data.extend_from_slice(b"data");
data.push(0); data.extend_from_slice(&(cover.imageformat as u32).to_be_bytes()[1..4]); data.extend_from_slice(&[0u8; 4]); data.extend_from_slice(&cover.data);
covr_data.extend_from_slice(&data);
}
MP4Atom::render(b"covr", &covr_data)
}
fn render_freeform_atom(&self, key: &str, values: &[MP4FreeForm]) -> Result<Vec<u8>> {
let parts: Vec<&str> = key.split(':').collect();
if parts.len() != 3 || parts[0] != "----" {
return Err(AudexError::ParseError(format!(
"Invalid freeform key: {}",
key
)));
}
let mean = parts[1];
let name = parts[2];
let mut freeform_data = Vec::new();
let mean_bytes = mean.as_bytes();
let mean_atom_size = u32::try_from(mean_bytes.len() + 12).map_err(|_| {
AudexError::InvalidData("Freeform mean field too large for MP4 atom".to_string())
})?;
freeform_data.extend_from_slice(&mean_atom_size.to_be_bytes());
freeform_data.extend_from_slice(b"mean");
freeform_data.extend_from_slice(&[0u8; 4]); freeform_data.extend_from_slice(mean_bytes);
let name_bytes = name.as_bytes();
let name_atom_size = u32::try_from(name_bytes.len() + 12).map_err(|_| {
AudexError::InvalidData("Freeform name field too large for MP4 atom".to_string())
})?;
freeform_data.extend_from_slice(&name_atom_size.to_be_bytes());
freeform_data.extend_from_slice(b"name");
freeform_data.extend_from_slice(&[0u8; 4]); freeform_data.extend_from_slice(name_bytes);
for value in values {
let mut data = Vec::new();
let atom_size = u32::try_from(value.data.len() + 16).map_err(|_| {
AudexError::InvalidData("Freeform data too large for MP4 atom".to_string())
})?;
data.extend_from_slice(&atom_size.to_be_bytes());
data.extend_from_slice(b"data");
data.push(value.version); data.extend_from_slice(&(value.dataformat as u32).to_be_bytes()[1..4]); data.extend_from_slice(&[0u8; 4]); data.extend_from_slice(&value.data);
freeform_data.extend_from_slice(&data);
}
MP4Atom::render(b"----", &freeform_data)
}
}
#[cfg(feature = "async")]
impl MP4Tags {
pub async fn save_async<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path = path.as_ref();
let mut file = tokio::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.await
.map_err(AudexError::Io)?;
let atoms = Atoms::parse_async(&mut file)
.await
.map_err(|e| AudexError::ParseError(format!("Failed to parse MP4 atoms: {}", e)))?;
let new_ilst_data = self
.render_ilst()
.map_err(|e| AudexError::ParseError(format!("Failed to render metadata: {}", e)))?;
if let Some(ilst_path) = atoms.path("moov.udta.meta.ilst") {
self.save_existing_async(&mut file, &atoms, &ilst_path, &new_ilst_data)
.await?;
} else {
self.save_new_async(&mut file, &atoms, &new_ilst_data)
.await?;
}
use tokio::io::AsyncWriteExt;
file.flush().await.map_err(AudexError::Io)?;
Ok(())
}
async fn save_existing_async(
&self,
file: &mut tokio::fs::File,
atoms: &Atoms,
ilst_path: &[&MP4Atom],
new_ilst_data: &[u8],
) -> Result<()> {
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
let ilst = ilst_path
.last()
.ok_or_else(|| AudexError::ParseError("Empty atom path for ilst".to_string()))?;
let mut offset = ilst.offset;
let mut length = ilst.length;
let free_atom = self.find_padding(ilst_path);
if let Some(free) = free_atom {
offset = std::cmp::min(offset, free.offset);
length = length.checked_add(free.length).ok_or_else(|| {
AudexError::InvalidData("ilst + free atom length overflows u64".to_string())
})?;
}
let padding_overhead = 8;
let file_size = file.seek(SeekFrom::End(0)).await?;
file.seek(SeekFrom::Start(0)).await?;
let safe_length = std::cmp::min(length, file_size);
let ilst_header_size: i64 = 8;
let file_size_i64 = i64::try_from(file_size).map_err(|_| {
crate::AudexError::InvalidData("file size exceeds maximum supported value".into())
})?;
let offset_i64 = i64::try_from(offset).map_err(|_| {
crate::AudexError::InvalidData("atom offset exceeds maximum supported value".into())
})?;
let safe_length_i64 = i64::try_from(safe_length).map_err(|_| {
crate::AudexError::InvalidData("atom length exceeds maximum supported value".into())
})?;
let content_size = (file_size_i64 - (offset_i64 + safe_length_i64)).max(0);
let padding_size =
safe_length_i64 - (new_ilst_data.len() as i64 + ilst_header_size + padding_overhead);
let info = PaddingInfo::new(padding_size, content_size);
const MAX_PADDING: usize = 10 * 1024 * 1024;
let new_padding = usize::try_from(info.get_default_padding().max(0))
.unwrap_or(0)
.min(MAX_PADDING);
let ilst_atom = MP4Atom::render(b"ilst", new_ilst_data)?;
let mut final_data = ilst_atom;
if new_padding > 0 {
let free_data = vec![0u8; new_padding];
let free_atom = MP4Atom::render(b"free", &free_data)?;
final_data.extend_from_slice(&free_atom);
}
crate::util::resize_bytes_async(file, safe_length, final_data.len() as u64, offset).await?;
let delta = i64::try_from(final_data.len()).map_err(|_| {
crate::AudexError::InvalidData(
"output data size exceeds maximum supported value".into(),
)
})? - safe_length_i64;
file.seek(SeekFrom::Start(offset)).await?;
file.write_all(&final_data).await?;
self.update_parents_async(file, &ilst_path[..ilst_path.len() - 1], delta, offset)
.await?;
if delta == 0 && safe_length != final_data.len() as u64 {
return Err(AudexError::InvalidOperation(
"MP4 save encountered an unexpected resize state".to_string(),
));
}
self.update_offsets_async(file, atoms, delta, offset)
.await?;
Ok(())
}
async fn save_new_async(
&self,
file: &mut tokio::fs::File,
atoms: &Atoms,
new_ilst_data: &[u8],
) -> Result<()> {
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
if new_ilst_data.is_empty() {
return Err(AudexError::InvalidData(
"Cannot save empty metadata to MP4".to_string(),
));
}
let hdlr = MP4Atom::render(
b"hdlr",
&[
0, 0, 0, 0, 0, 0, 0, 0, b'm', b'd', b'i', b'r', b'a', b'p', b'p', b'l', 0, 0, 0, 0,
0, 0, 0, 0, 0,
],
)?;
let ilst_atom = MP4Atom::render(b"ilst", new_ilst_data)?;
let meta_data = [&[0u8; 4], &hdlr[..], &ilst_atom[..]].concat();
let path = if let Some(udta_path) = atoms.path("moov.udta") {
udta_path
} else {
atoms
.path("moov")
.ok_or_else(|| AudexError::ParseError("No moov atom found".to_string()))?
};
let last_atom = path
.last()
.ok_or_else(|| AudexError::ParseError("Empty atom path for moov".to_string()))?;
let header_size = last_atom
.data_offset
.checked_sub(last_atom.offset)
.ok_or_else(|| {
AudexError::ParseError("atom data_offset is before atom start".to_string())
})?;
let offset = last_atom.offset.checked_add(header_size).ok_or_else(|| {
AudexError::ParseError("moov atom offset too large to add header size".to_string())
})?;
let file_size = file.seek(SeekFrom::End(0)).await.map_err(AudexError::Io)?;
let content_size = file_size.checked_sub(offset).ok_or_else(|| {
AudexError::ParseError(
"moov offset exceeds file size, file may be truncated".to_string(),
)
})?;
let content_size_i64 = i64::try_from(content_size).map_err(|_| {
AudexError::InvalidData(
"content size exceeds maximum supported value for padding calculation".to_string(),
)
})?;
let meta_data_len_i64 = i64::try_from(meta_data.len()).map_err(|_| {
AudexError::InvalidData("metadata size exceeds maximum supported value".to_string())
})?;
let padding_size = -meta_data_len_i64;
let info = PaddingInfo::new(padding_size, content_size_i64);
const MAX_PADDING: usize = 10 * 1024 * 1024;
let new_padding = usize::try_from(info.get_default_padding().max(0))
.unwrap_or(0)
.min(MAX_PADDING);
let free_atom = if new_padding > 0 {
MP4Atom::render(b"free", &vec![0u8; new_padding])?
} else {
Vec::new()
};
let meta = MP4Atom::render(b"meta", &[&meta_data[..], &free_atom[..]].concat())?;
let data = if last_atom.name != *b"udta" {
MP4Atom::render(b"udta", &meta)?
} else {
meta
};
crate::util::insert_bytes_async(file, data.len() as u64, offset, None).await?;
file.seek(SeekFrom::Start(offset)).await?;
file.write_all(&data).await?;
let data_len_i64 = i64::try_from(data.len()).map_err(|_| {
AudexError::InvalidData(
"inserted data size exceeds maximum supported value".to_string(),
)
})?;
self.update_parents_async(file, &path, data_len_i64, offset)
.await?;
self.update_offsets_async(file, atoms, data_len_i64, offset)
.await?;
Ok(())
}
async fn update_parents_async(
&self,
file: &mut tokio::fs::File,
path: &[&MP4Atom],
delta: i64,
resize_offset: u64,
) -> Result<()> {
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
if delta == 0 {
return Ok(());
}
for atom in path {
let mut actual_offset = atom.offset;
if actual_offset > resize_offset {
actual_offset = (actual_offset as i64)
.checked_add(delta)
.filter(|&v| v >= 0)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Parent atom offset underflow: {} + {}",
atom.offset, delta
))
})? as u64;
}
file.seek(SeekFrom::Start(actual_offset)).await?;
let mut size_bytes = [0u8; 4];
file.read_exact(&mut size_bytes).await?;
let size = u32::from_be_bytes(size_bytes);
if size == 1 {
file.seek(SeekFrom::Start(actual_offset + 8)).await?;
let mut size_bytes = [0u8; 8];
file.read_exact(&mut size_bytes).await?;
let size = u64::from_be_bytes(size_bytes);
let size_i64 = i64::try_from(size).map_err(|_| {
AudexError::ParseError(format!(
"64-bit atom size {} exceeds maximum representable value",
size
))
})?;
let new_size =
size_i64
.checked_add(delta)
.filter(|&s| s >= 8)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Atom size overflow: {} + {} produces invalid size",
size, delta
))
})?;
file.seek(SeekFrom::Start(actual_offset + 8)).await?;
file.write_all(&new_size.to_be_bytes()).await?;
} else {
let new_size = (size as i64)
.checked_add(delta)
.filter(|&s| s >= 8 && s <= u32::MAX as i64)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Atom size overflow: {} + {} produces invalid 32-bit size",
size, delta
))
})? as u32;
file.seek(SeekFrom::Start(actual_offset)).await?;
file.write_all(&new_size.to_be_bytes()).await?;
}
}
Ok(())
}
async fn update_offsets_async(
&self,
file: &mut tokio::fs::File,
atoms: &Atoms,
delta: i64,
offset: u64,
) -> Result<()> {
if delta == 0 {
return Ok(());
}
if let Some(moov) = atoms.atoms.iter().find(|a| a.name == *b"moov") {
for atom in moov.findall(b"stco", true) {
self.update_offset_table_async(file, atom, delta, offset, false)
.await?;
}
for atom in moov.findall(b"co64", true) {
self.update_offset_table_async(file, atom, delta, offset, true)
.await?;
}
}
if let Some(moof) = atoms.atoms.iter().find(|a| a.name == *b"moof") {
for atom in moof.findall(b"tfhd", true) {
self.update_tfhd_async(file, atom, delta, offset).await?;
}
}
Ok(())
}
async fn update_offset_table_async(
&self,
file: &mut tokio::fs::File,
atom: &crate::mp4::atom::MP4Atom,
delta: i64,
offset: u64,
is_64bit: bool,
) -> Result<()> {
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
let mut atom_offset = atom.offset;
if atom_offset > offset {
atom_offset = (atom_offset as i64)
.checked_add(delta)
.filter(|&v| v >= 0)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Offset table atom position underflow: {} + {}",
atom_offset, delta
))
})? as u64;
}
file.seek(SeekFrom::Start(atom_offset + 12))
.await
.map_err(|e| AudexError::ParseError(format!("Seek failed: {}", e)))?;
let mut count_buf = [0u8; 4];
file.read_exact(&mut count_buf)
.await
.map_err(|e| AudexError::ParseError(format!("Read failed: {}", e)))?;
let count = u32::from_be_bytes(count_buf) as usize;
let entry_size = if is_64bit { 8 } else { 4 };
let header_overhead: u64 = 16; let max_data_bytes = atom.length.saturating_sub(header_overhead);
let max_entries = max_data_bytes / entry_size as u64;
if (count as u64) > max_entries {
return Err(AudexError::ParseError(format!(
"Offset table entry count ({}) exceeds atom capacity ({} entries fit in {} bytes)",
count, max_entries, max_data_bytes
)));
}
let alloc_size = count.checked_mul(entry_size).ok_or_else(|| {
AudexError::ParseError(format!(
"Offset table allocation overflow: {} entries * {} bytes",
count, entry_size
))
})?;
let mut data = vec![0u8; alloc_size];
file.read_exact(&mut data)
.await
.map_err(|e| AudexError::ParseError(format!("Read offsets failed: {}", e)))?;
for i in 0..count {
let pos = i * entry_size;
if is_64bit {
let o = u64::from_be_bytes([
data[pos],
data[pos + 1],
data[pos + 2],
data[pos + 3],
data[pos + 4],
data[pos + 5],
data[pos + 6],
data[pos + 7],
]);
if o > offset {
let o_i64 = i64::try_from(o).map_err(|_| {
AudexError::ParseError(format!(
"64-bit chunk offset {} exceeds maximum representable value",
o
))
})?;
let new_o = o_i64
.checked_add(delta)
.filter(|&v| v >= 0)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Chunk offset underflow: {} + {} is negative",
o, delta
))
})? as u64;
data[pos..pos + 8].copy_from_slice(&new_o.to_be_bytes());
}
} else {
let o =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
if (o as u64) > offset {
let new_o = (o as i64)
.checked_add(delta)
.filter(|&v| v >= 0 && v <= u32::MAX as i64)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Chunk offset underflow: {} + {} is invalid for 32-bit",
o, delta
))
})? as u32;
data[pos..pos + 4].copy_from_slice(&new_o.to_be_bytes());
}
}
}
file.seek(SeekFrom::Start(atom_offset + 16))
.await
.map_err(|e| AudexError::ParseError(format!("Seek failed: {}", e)))?;
file.write_all(&data)
.await
.map_err(|e| AudexError::ParseError(format!("Write offsets failed: {}", e)))?;
Ok(())
}
async fn update_tfhd_async(
&self,
file: &mut tokio::fs::File,
atom: &crate::mp4::atom::MP4Atom,
delta: i64,
offset: u64,
) -> Result<()> {
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
let mut atom_offset = atom.offset;
if atom_offset > offset {
atom_offset = (atom_offset as i64)
.checked_add(delta)
.filter(|&v| v >= 0)
.ok_or_else(|| {
AudexError::ParseError(format!(
"Atom offset overflow: {} + {}",
atom_offset, delta
))
})? as u64;
}
file.seek(SeekFrom::Start(atom_offset + 9))
.await
.map_err(|e| AudexError::ParseError(format!("Seek failed: {}", e)))?;
let atom_len = usize::try_from(atom.length).map_err(|_| {
AudexError::ParseError(format!(
"Atom length {} exceeds addressable range",
atom.length
))
})?;
if atom_len < 12 {
return Err(AudexError::ParseError(format!(
"tfhd atom too short for version and flags: {} bytes",
atom.length
)));
}
let alloc_size = atom_len - 9;
let limits = crate::limits::ParseLimits::default();
if alloc_size as u64 > limits.max_tag_size {
return Err(AudexError::ParseError(format!(
"tfhd atom data size ({} bytes) exceeds maximum allowed ({})",
alloc_size, limits.max_tag_size
)));
}
let mut data = vec![0u8; alloc_size];
file.read_exact(&mut data)
.await
.map_err(|e| AudexError::ParseError(format!("Read tfhd failed: {}", e)))?;
let flags = u32::from_be_bytes([0, data[0], data[1], data[2]]);
if flags & 1 != 0 && data.len() >= 15 {
let o = u64::from_be_bytes([
data[7], data[8], data[9], data[10], data[11], data[12], data[13], data[14],
]);
if o > offset {
let o_i64 = i64::try_from(o).map_err(|_| {
AudexError::ParseError(format!(
"64-bit tfhd base_data_offset {} exceeds maximum representable value",
o
))
})?;
let new_o = o_i64
.checked_add(delta)
.filter(|&v| v >= 0)
.ok_or_else(|| {
AudexError::ParseError(format!(
"tfhd base_data_offset underflow: {} + {}",
o, delta
))
})? as u64;
file.seek(SeekFrom::Start(atom_offset + 16))
.await
.map_err(|e| AudexError::ParseError(format!("Seek failed: {}", e)))?;
file.write_all(&new_o.to_be_bytes())
.await
.map_err(|e| AudexError::ParseError(format!("Write tfhd failed: {}", e)))?;
}
}
Ok(())
}
pub async fn load_async(atoms: &Atoms, file: &mut tokio::fs::File) -> Result<Option<Self>> {
if !Self::can_load(atoms) {
return Ok(None);
}
let ilst_path = atoms
.path("moov.udta.meta.ilst")
.ok_or_else(|| AudexError::ParseError("Failed to get ilst path".to_string()))?;
let ilst = ilst_path
.last()
.ok_or_else(|| AudexError::ParseError("Empty ilst path".to_string()))?;
let mut tags = MP4Tags::new();
if let Some(meta_path) = atoms.path("moov.udta.meta") {
if let Some(meta) = meta_path.last() {
if let Some(children) = &meta.children {
for (i, child) in children.iter().enumerate() {
if child.name == *b"ilst" {
if i > 0 && children[i - 1].name == *b"free" {
tags.padding = usize::try_from(children[i - 1].data_length)
.unwrap_or(usize::MAX);
} else if i + 1 < children.len() && children[i + 1].name == *b"free" {
tags.padding = usize::try_from(children[i + 1].data_length)
.unwrap_or(usize::MAX);
}
break;
}
}
}
}
}
if let Some(children) = &ilst.children {
let mut total_ilst_bytes = 0u64;
for child in children {
total_ilst_bytes =
total_ilst_bytes
.checked_add(child.data_length)
.ok_or_else(|| {
AudexError::InvalidData("MP4 metadata size overflow".to_string())
})?;
if total_ilst_bytes > MAX_TOTAL_ILST_DATA_BYTES {
return Err(AudexError::InvalidData(format!(
"MP4 metadata exceeds cumulative {} byte limit",
MAX_TOTAL_ILST_DATA_BYTES
)));
}
let data = child.read_data_async(file).await?;
if tags.parse_metadata_atom_data(child, &data).is_err() {
let key = crate::mp4::util::name2key(&child.name);
tags.failed_atoms.entry(key).or_default().push(data);
}
}
}
Ok(Some(tags))
}
}
impl Tags for MP4Tags {
fn get(&self, key: &str) -> Option<&[String]> {
self.tags.get(key).map(|v| v.as_slice())
}
fn set(&mut self, key: &str, values: Vec<String>) {
if values.is_empty() {
self.tags.remove(key);
self.freeforms.remove(key);
} else if key.starts_with("----:") {
let template = self
.freeforms
.get(key)
.and_then(|values| values.first())
.cloned();
let freeform_values: Vec<MP4FreeForm> = values
.into_iter()
.map(|v| {
if let Some(template) = &template {
MP4FreeForm::new(v.into_bytes(), template.dataformat, template.version)
} else {
MP4FreeForm::new_text(v.into_bytes())
}
})
.collect();
self.freeforms.insert(key.to_string(), freeform_values);
} else {
self.tags.insert(key.to_string(), values);
}
}
fn remove(&mut self, key: &str) {
self.tags.remove(key);
self.freeforms.remove(key);
if key == "covr" {
self.covers.clear();
}
}
fn keys(&self) -> Vec<String> {
let mut keys: Vec<String> = self.tags.keys().cloned().collect();
keys.extend(self.freeforms.keys().cloned());
if !self.covers.is_empty() {
keys.push("covr".to_string());
}
keys.sort();
keys.dedup();
keys
}
fn pprint(&self) -> String {
let mut result = String::new();
let mut keys: Vec<_> = self.keys();
keys.sort();
for key in keys {
if let Some(values) = self.get(&key) {
for value in values {
result.push_str(&format!("{}={}\n", key, value));
}
}
}
result
}
}
impl Metadata for MP4Tags {
type Error = crate::AudexError;
fn new() -> Self {
MP4Tags::default()
}
fn load_from_fileobj(filething: &mut crate::util::AnyFileThing) -> crate::Result<Self> {
if let Some(path) = filething.filename() {
let mp4 = crate::mp4::MP4::load(path)?;
Ok(mp4.tags.unwrap_or_else(MP4Tags::new))
} else {
Err(crate::AudexError::InvalidOperation(
"MP4Tags.load_from_fileobj requires a real file path".to_string(),
))
}
}
fn save_to_fileobj(&self, filething: &mut crate::util::AnyFileThing) -> crate::Result<()> {
let path = filething.filename().ok_or_else(|| {
crate::AudexError::InvalidOperation(
"MP4Tags.save_to_fileobj requires a real file path".to_string(),
)
})?;
self.save(path)
}
fn delete_from_fileobj(filething: &mut crate::util::AnyFileThing) -> crate::Result<()> {
let path = filething.filename().ok_or_else(|| {
crate::AudexError::InvalidOperation(
"MP4Tags.delete_from_fileobj requires a real file path".to_string(),
)
})?;
MP4Tags::new().save(path)
}
}
impl crate::tags::MetadataFields for MP4Tags {
fn artist(&self) -> Option<&String> {
self.get_first("©ART")
}
fn set_artist(&mut self, artist: String) {
self.set_single("©ART", artist);
}
fn album(&self) -> Option<&String> {
self.get_first("©alb")
}
fn set_album(&mut self, album: String) {
self.set_single("©alb", album);
}
fn title(&self) -> Option<&String> {
self.get_first("©nam")
}
fn set_title(&mut self, title: String) {
self.set_single("©nam", title);
}
fn track_number(&self) -> Option<u32> {
self.get_first("trkn")?.split('/').next()?.parse().ok()
}
fn set_track_number(&mut self, track: u32) {
self.set_single("trkn", track.to_string());
}
fn date(&self) -> Option<&String> {
self.get_first("©day")
}
fn set_date(&mut self, date: String) {
self.set_single("©day", date);
}
fn genre(&self) -> Option<&String> {
self.get_first("©gen")
}
fn set_genre(&mut self, genre: String) {
self.set_single("©gen", genre);
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MP4Info {
pub bitrate: Option<u32>,
#[cfg_attr(
feature = "serde",
serde(with = "crate::serde_helpers::duration_as_secs_f64")
)]
pub length: Option<Duration>,
pub channels: Option<u16>,
pub sample_rate: Option<u32>,
pub bits_per_sample: Option<u16>,
pub codec: String,
pub codec_description: String,
}
impl MP4Info {
pub fn load<R: Read + Seek>(atoms: &Atoms, reader: &mut R) -> Result<Self> {
let mut info = MP4Info::default();
let moov = atoms
.get("moov")
.ok_or_else(|| AudexError::ParseError("not a MP4 file - no moov atom".to_string()))?;
let audio_trak = Self::find_audio_track(moov, reader)?;
if let Some(mdhd) = audio_trak.get_child(&["mdia", "mdhd"]) {
info.parse_mdhd(mdhd, reader)?;
}
if let Some(stsd) = audio_trak.get_child(&["mdia", "minf", "stbl", "stsd"]) {
info.parse_stsd(stsd, reader)?;
}
Ok(info)
}
fn find_audio_track<'a, R: Read + Seek>(
moov: &'a MP4Atom,
reader: &mut R,
) -> Result<&'a MP4Atom> {
if let Some(children) = &moov.children {
for trak in children {
if trak.name == *b"trak" {
if let Some(hdlr) = trak.get_child(&["mdia", "hdlr"]) {
let data = hdlr.read_data(reader)?;
if Self::is_audio_handler(&data) {
return Ok(trak);
}
}
}
}
}
Err(AudexError::ParseError(
"track has no audio data".to_string(),
))
}
fn is_audio_handler(data: &[u8]) -> bool {
data.len() >= 12 && &data[8..12] == b"soun"
}
fn parse_mdhd<R: Read + Seek>(&mut self, mdhd: &MP4Atom, reader: &mut R) -> Result<()> {
let data = mdhd.read_data(reader)?;
self.parse_mdhd_data(&data)
}
fn parse_mdhd_data(&mut self, data: &[u8]) -> Result<()> {
let (version, _flags, payload) = parse_full_atom(data)?;
let (timescale, duration) = match version {
0 => {
if payload.len() < 16 {
return Err(AudexError::ParseError("mdhd payload too short".to_string()));
}
let timescale =
u32::from_be_bytes([payload[8], payload[9], payload[10], payload[11]]);
let duration =
u32::from_be_bytes([payload[12], payload[13], payload[14], payload[15]]) as u64;
(timescale, duration)
}
1 => {
if payload.len() < 28 {
return Err(AudexError::ParseError("mdhd payload too short".to_string()));
}
let timescale =
u32::from_be_bytes([payload[16], payload[17], payload[18], payload[19]]);
let duration = u64::from_be_bytes([
payload[20],
payload[21],
payload[22],
payload[23],
payload[24],
payload[25],
payload[26],
payload[27],
]);
(timescale, duration)
}
_ => {
return Err(AudexError::ParseError(format!(
"Unknown mdhd version {}",
version
)));
}
};
if timescale > 0 {
let seconds = (duration as f64 / timescale as f64).clamp(0.0, u64::MAX as f64 / 1e9);
self.length = Duration::try_from_secs_f64(seconds).ok();
}
Ok(())
}
fn parse_stsd<R: Read + Seek>(&mut self, stsd: &MP4Atom, reader: &mut R) -> Result<()> {
let data = stsd.read_data(reader)?;
self.parse_stsd_data(&data)
}
fn parse_stsd_data(&mut self, data: &[u8]) -> Result<()> {
let (version, _flags, payload) = parse_full_atom(data)?;
if version != 0 {
return Err(AudexError::ParseError(format!(
"Unsupported stsd version {}",
version
)));
}
if payload.len() < 4 {
return Err(AudexError::ParseError("stsd payload too short".to_string()));
}
let num_entries = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
if num_entries == 0 {
return Ok(());
}
if payload.len() < 4 + 36 {
return Err(AudexError::ParseError(
"stsd payload too short for a sample entry".to_string(),
));
}
let entry_data = &payload[4..];
let mut cursor = Cursor::new(entry_data);
match MP4Atom::parse(&mut cursor, 1) {
Ok(entry_atom) => {
let entry = AudioSampleEntry::parse(&entry_atom, &mut cursor)?;
self.channels = Some(entry.channels);
self.sample_rate = Some(entry.sample_rate);
self.bits_per_sample = Some(entry.sample_size);
self.bitrate = Some(entry.bitrate);
self.codec = entry.codec;
self.codec_description = entry.codec_description;
}
Err(e) => {
return Err(AudexError::ParseError(format!(
"Failed to parse sample entry: {}",
e
)));
}
}
Ok(())
}
}
impl StreamInfo for MP4Info {
fn length(&self) -> Option<Duration> {
self.length
}
fn bitrate(&self) -> Option<u32> {
self.bitrate
}
fn sample_rate(&self) -> Option<u32> {
self.sample_rate
}
fn channels(&self) -> Option<u16> {
self.channels
}
fn bits_per_sample(&self) -> Option<u16> {
self.bits_per_sample
}
}
#[derive(Debug, Default)]
pub struct MP4 {
pub info: MP4Info,
pub tags: Option<MP4Tags>,
pub chapters: Option<MP4Chapters>,
path: Option<std::path::PathBuf>,
}
impl FileType for MP4 {
type Tags = MP4Tags;
type Info = MP4Info;
fn format_id() -> &'static str {
"MP4"
}
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
debug_event!("parsing MP4 file");
let file = File::open(&path)?;
let mut reader = BufReader::new(file);
let atoms = Atoms::parse(&mut reader)?;
trace_event!("MP4 atom tree traversal complete");
let info = MP4Info::load(&atoms, &mut reader)?;
let tags = MP4Tags::load(&atoms, &mut reader)?;
debug_event!(tags_present = tags.is_some(), "MP4 tags parsed");
let chapters = MP4Chapters::load(&atoms, &mut reader)?;
Ok(MP4 {
info,
tags,
chapters,
path: Some(path.as_ref().to_path_buf()),
})
}
fn load_from_reader(reader: &mut dyn crate::ReadSeek) -> Result<Self> {
debug_event!("parsing MP4 file from reader");
let mut reader = reader;
let atoms = Atoms::parse(&mut reader)?;
trace_event!("MP4 atom tree traversal complete");
let info = MP4Info::load(&atoms, &mut reader)?;
let tags = MP4Tags::load(&atoms, &mut reader)?;
debug_event!(tags_present = tags.is_some(), "MP4 tags parsed");
let chapters = MP4Chapters::load(&atoms, &mut reader)?;
Ok(MP4 {
info,
tags,
chapters,
path: None,
})
}
fn save(&mut self) -> Result<()> {
debug_event!("saving MP4 file metadata");
let path = self.path.as_ref().ok_or_else(|| {
AudexError::ParseError("No file path available for saving".to_string())
})?;
if let Some(tags) = &self.tags {
tags.save(path)?;
} else {
let empty_tags = MP4Tags::new();
empty_tags.save(path)?;
}
Ok(())
}
fn clear(&mut self) -> Result<()> {
self.tags = None;
if let Some(path) = &self.path {
let empty_tags = MP4Tags::new();
empty_tags.save(path)?;
}
Ok(())
}
fn save_to_writer(&mut self, writer: &mut dyn crate::ReadWriteSeek) -> Result<()> {
if let Some(tags) = &self.tags {
tags.save_to_writer(writer)?;
} else {
let empty_tags = MP4Tags::new();
empty_tags.save_to_writer(writer)?;
}
Ok(())
}
fn clear_writer(&mut self, writer: &mut dyn crate::ReadWriteSeek) -> Result<()> {
self.tags = None;
let empty_tags = MP4Tags::new();
empty_tags.save_to_writer(writer)?;
Ok(())
}
fn save_to_path(&mut self, path: &Path) -> Result<()> {
if let Some(tags) = &self.tags {
tags.save(path)?;
} else {
let empty_tags = MP4Tags::new();
empty_tags.save(path)?;
}
Ok(())
}
fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
));
}
self.tags = Some(MP4Tags::new());
Ok(())
}
fn get(&self, key: &str) -> Option<Vec<String>> {
let registry = crate::easymp4::KeyRegistry::new();
let mp4_key = if let Some(mapping) = registry.get_mp4_key(key) {
&mapping.mp4_key
} else {
key
};
let tags = self.tags.as_ref()?;
if let Some(v) = tags.get(mp4_key) {
return Some(v.to_vec());
}
if let Some(freeforms) = tags.freeforms.get(mp4_key) {
let values: Vec<String> = freeforms
.iter()
.filter_map(|ff| String::from_utf8(ff.data.clone()).ok())
.collect();
if !values.is_empty() {
return Some(values);
}
}
None
}
fn set(&mut self, key: &str, values: Vec<String>) -> Result<()> {
let registry = crate::easymp4::KeyRegistry::new();
let mp4_key = if let Some(mapping) = registry.get_mp4_key(key) {
mapping.mp4_key.clone()
} else {
key.to_string()
};
if let Some(tags) = self.tags_mut() {
tags.set(&mp4_key, values);
Ok(())
} else {
Err(AudexError::Unsupported(
"This format does not support tags".to_string(),
))
}
}
fn remove(&mut self, key: &str) -> Result<()> {
let registry = crate::easymp4::KeyRegistry::new();
let mp4_key = if let Some(mapping) = registry.get_mp4_key(key) {
&mapping.mp4_key
} else {
key
};
if let Some(tags) = self.tags_mut() {
tags.remove(mp4_key);
Ok(())
} else {
Err(AudexError::Unsupported(
"This format does not support tags".to_string(),
))
}
}
fn tags(&self) -> Option<&Self::Tags> {
self.tags.as_ref()
}
fn tags_mut(&mut self) -> Option<&mut Self::Tags> {
self.tags.as_mut()
}
fn info(&self) -> &Self::Info {
&self.info
}
fn score(filename: &str, header: &[u8]) -> i32 {
let mut score = 0;
if header.len() >= 8 && &header[4..8] == b"ftyp" {
score += 10;
}
if header.len() >= 12 {
let ftyp_data = &header[8..std::cmp::min(header.len(), 20)];
if ftyp_data.starts_with(b"M4A ")
|| ftyp_data.starts_with(b"M4B ")
|| ftyp_data.starts_with(b"M4P ")
|| ftyp_data.starts_with(b"mp41")
|| ftyp_data.starts_with(b"mp42")
|| ftyp_data.starts_with(b"isom")
|| ftyp_data.starts_with(b"3g2a")
|| ftyp_data.starts_with(b"3g2b")
|| ftyp_data.starts_with(b"3g2c")
{
score += 5;
}
}
let lower = filename.to_lowercase();
if lower.ends_with(".m4a")
|| lower.ends_with(".mp4")
|| lower.ends_with(".m4b")
|| lower.ends_with(".m4p")
|| lower.ends_with(".3g2")
|| lower.ends_with(".3gp")
{
score += 3;
}
score
}
fn mime_types() -> &'static [&'static str] {
&["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"]
}
fn filename(&self) -> Option<&str> {
self.path.as_ref().and_then(|p| p.to_str())
}
}
impl MP4 {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::load(path)
}
pub fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
));
}
self.tags = Some(MP4Tags::new());
Ok(())
}
pub fn get_or_create_tags(&mut self) -> &mut MP4Tags {
self.tags.get_or_insert_with(MP4Tags::new)
}
pub fn padding(&self) -> usize {
self.tags.as_ref().map(|t| t.padding()).unwrap_or(0)
}
pub fn set_tag(&mut self, key: &str, value: &str) -> Result<()> {
self.get_or_create_tags().set_single(key, value.to_string());
Ok(())
}
pub fn get_tag(&self, key: &str) -> Option<&String> {
self.tags.as_ref()?.get_first(key)
}
pub fn remove_tag(&mut self, key: &str) {
if let Some(tags) = &mut self.tags {
tags.remove(key);
}
}
pub fn pprint(&self) -> String {
let mime_type = Self::mime_types().first().unwrap_or(&"audio/mp4");
let mut result = format!("{} ({})", self.info.pprint(), mime_type);
if let Some(tags) = &self.tags {
let tag_info = tags.pprint();
if !tag_info.trim().is_empty() {
result.push('\n');
result.push_str(&tag_info);
}
}
if let Some(chapters) = &self.chapters {
let chapter_info = chapters.pprint();
if !chapter_info.is_empty() && chapter_info != "chapters=" {
result.push('\n');
result.push_str(&chapter_info);
}
}
result
}
#[cfg(feature = "async")]
pub async fn load_async<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let mut file = tokio::fs::File::open(path).await?;
let atoms = Atoms::parse_async(&mut file).await?;
let info = MP4Info::load_async(&atoms, &mut file).await?;
let tags = MP4Tags::load_async(&atoms, &mut file).await?;
let chapters = MP4Chapters::load_async(&atoms, &mut file).await?;
Ok(MP4 {
info,
tags,
chapters,
path: Some(path.to_path_buf()),
})
}
#[cfg(feature = "async")]
pub async fn save_async(&mut self) -> Result<()> {
let path = self.path.as_ref().ok_or_else(|| {
AudexError::ParseError("No file path available for saving".to_string())
})?;
if let Some(tags) = &self.tags {
tags.save_async(path).await?;
} else {
let empty_tags = MP4Tags::new();
empty_tags.save_async(path).await?;
}
Ok(())
}
#[cfg(feature = "async")]
pub async fn clear_async(&mut self) -> Result<()> {
self.tags = None;
self.save_async().await
}
#[cfg(feature = "async")]
pub async fn delete_async(&mut self) -> Result<()> {
if let Some(filename) = self.filename() {
tokio::fs::remove_file(filename).await?;
}
Ok(())
}
}
impl MP4Info {
pub fn pprint(&self) -> String {
let codec_desc = if self.codec_description.is_empty() {
self.codec.clone()
} else {
self.codec_description.clone()
};
let length = self.length.map(|d| d.as_secs_f64()).unwrap_or(0.0);
let bitrate = self.bitrate.unwrap_or(0);
format!(
"MPEG-4 audio ({}), {:.2} seconds, {} bps",
codec_desc, length, bitrate
)
}
}
#[cfg(feature = "async")]
impl MP4Info {
pub async fn load_async(atoms: &Atoms, file: &mut tokio::fs::File) -> Result<Self> {
let mut info = MP4Info::default();
let moov = atoms
.get("moov")
.ok_or_else(|| AudexError::ParseError("not a MP4 file - no moov atom".to_string()))?;
let audio_trak = Self::find_audio_track_async(moov, file).await?;
if let Some(mdhd) = audio_trak.get_child(&["mdia", "mdhd"]) {
let data = mdhd.read_data_async(file).await?;
info.parse_mdhd_data(&data)?;
}
if let Some(stsd) = audio_trak.get_child(&["mdia", "minf", "stbl", "stsd"]) {
let data = stsd.read_data_async(file).await?;
info.parse_stsd_data(&data)?;
}
Ok(info)
}
async fn find_audio_track_async<'a>(
moov: &'a MP4Atom,
file: &mut tokio::fs::File,
) -> Result<&'a MP4Atom> {
if let Some(children) = &moov.children {
for trak in children {
if trak.name == *b"trak" {
if let Some(hdlr) = trak.get_child(&["mdia", "hdlr"]) {
let data = hdlr.read_data_async(file).await?;
if Self::is_audio_handler(&data) {
return Ok(trak);
}
}
}
}
}
Err(AudexError::ParseError(
"track has no audio data".to_string(),
))
}
}
#[cfg(feature = "async")]
impl MP4Chapters {
pub async fn load_async(atoms: &Atoms, file: &mut tokio::fs::File) -> Result<Option<Self>> {
if !Self::can_load(atoms) {
return Ok(None);
}
let mut chapters = MP4Chapters::new();
if let Some(mvhd) = atoms.get("moov.mvhd") {
let data = mvhd.read_data_async(file).await?;
chapters.parse_mvhd_data(&data)?;
}
if chapters.timescale.is_none() {
return Err(AudexError::ParseError(
"Unable to get timescale".to_string(),
));
}
if let Some(chpl) = atoms.get("moov.udta.chpl") {
let data = chpl.read_data_async(file).await?;
chapters.parse_chpl_data(&data)?;
}
Ok(Some(chapters))
}
}