use crate::core::error::{XmpError, XmpResult};
use crate::core::metadata::XmpMeta;
use crate::files::handler::{FileHandler, XmpOptions};
use std::io::{Read, Seek, SeekFrom, Write};
const ID3_TAG_HEADER_SIZE: usize = 10;
const ID3V22_FRAME_HEADER_SIZE: usize = 6;
const ID3V23_FRAME_HEADER_SIZE: usize = 10;
const XMP_V23_ID: &[u8] = b"PRIV";
const XMP_V22_ID: &[u8] = b"PRV\0";
const XMP_PREFIX: &[u8] = b"XMP\0";
#[derive(Debug, Clone, Copy)]
pub struct Mp3Handler;
impl FileHandler for Mp3Handler {
fn can_handle<R: Read + Seek>(&self, reader: &mut R) -> XmpResult<bool> {
let pos = reader.stream_position()?;
let file_len = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(pos))?;
if file_len < 10 {
return Ok(false);
}
let mut header = [0u8; 10];
if reader.read_exact(&mut header).is_err() {
reader.seek(SeekFrom::Start(pos))?;
return Ok(false);
}
reader.seek(SeekFrom::Start(pos))?;
if &header[0..3] != b"ID3" {
return Ok(false);
}
let major = header[3];
let minor = header[4];
if !(2..=4).contains(&major) || minor == 0xFF {
return Ok(false);
}
let flags = header[5];
if (flags & 0x80) != 0 {
return Ok(false);
}
if (flags & 0x0F) != 0 {
return Ok(false);
}
let size_bytes = &header[6..10];
if (size_bytes[0] | size_bytes[1] | size_bytes[2] | size_bytes[3]) & 0x80 != 0 {
return Ok(false);
}
Ok(true)
}
fn read_xmp<R: Read + Seek>(
&self,
reader: &mut R,
_options: &XmpOptions,
) -> XmpResult<Option<XmpMeta>> {
Self::read_xmp(reader)
}
fn write_xmp<R: Read + Seek, W: Write + Seek>(
&self,
reader: &mut R,
writer: &mut W,
meta: &XmpMeta,
) -> XmpResult<()> {
Self::write_xmp(reader, writer, meta)
}
fn format_name(&self) -> &'static str {
"MP3"
}
fn extensions(&self) -> &'static [&'static str] {
&["mp3"]
}
}
impl Mp3Handler {
pub fn read_xmp<R: Read + Seek>(mut reader: R) -> XmpResult<Option<XmpMeta>> {
let mut header = [0u8; ID3_TAG_HEADER_SIZE];
reader.read_exact(&mut header)?;
if &header[0..3] != b"ID3" {
return Ok(None); }
let major_version = header[3];
let minor_version = header[4];
let flags = header[5];
if !(2..=4).contains(&major_version) || minor_version == 0xFF {
return Err(XmpError::BadValue(format!(
"Unsupported ID3v2 version: {}.{}",
major_version, minor_version
)));
}
if (flags & 0x10) != 0 {
return Err(XmpError::NotSupported(
"ID3v2 footer not supported".to_string(),
));
}
if (flags & 0x80) != 0 {
return Err(XmpError::NotSupported(
"Unsynchronized ID3v2 tags not supported".to_string(),
));
}
let tag_size = Self::read_synchsafe_u32(&header[6..10])?;
if (flags & 0x40) != 0 {
let ext_header_size = Self::read_synchsafe_u32_from_reader(&mut reader)?;
let skip_size = if major_version < 4 {
ext_header_size - 4 } else {
ext_header_size
};
reader.seek(SeekFrom::Current(skip_size as i64 - 4))?;
}
let frame_header_size = if major_version == 2 {
ID3V22_FRAME_HEADER_SIZE
} else {
ID3V23_FRAME_HEADER_SIZE
};
let xmp_frame_id = if major_version == 2 {
XMP_V22_ID
} else {
XMP_V23_ID
};
let tag_start = reader.stream_position()?;
let tag_end = tag_start + tag_size as u64;
while reader.stream_position()? < tag_end {
let current_pos = reader.stream_position()?;
if tag_end - current_pos < frame_header_size as u64 {
break; }
let mut frame_header = vec![0u8; frame_header_size];
reader.read_exact(&mut frame_header)?;
if frame_header.iter().all(|&b| b == 0) {
break;
}
let (frame_id, frame_size) = Self::parse_frame_header(&frame_header, major_version)?;
if frame_id == xmp_frame_id {
if let Some(meta) = Self::read_xmp_frame_content(&mut reader, frame_size)? {
return Ok(Some(meta));
}
} else {
reader.seek(SeekFrom::Current(frame_size as i64))?;
}
}
Ok(None)
}
pub fn write_xmp<R: Read + Seek, W: Write + Seek>(
mut reader: R,
writer: &mut W,
meta: &XmpMeta,
) -> XmpResult<()> {
let xmp_packet = meta.serialize_packet()?;
let xmp_bytes = xmp_packet.as_bytes();
let mut frame_content = Vec::with_capacity(4 + xmp_bytes.len());
frame_content.extend_from_slice(XMP_PREFIX);
frame_content.extend_from_slice(xmp_bytes);
let mut header = [0u8; ID3_TAG_HEADER_SIZE];
reader.read_exact(&mut header)?;
if &header[0..3] != b"ID3" {
return Self::write_new_id3v2_tag(writer, &frame_content);
}
let major_version = header[3];
let flags = header[5];
let tag_size = Self::read_synchsafe_u32(&header[6..10])?;
let frame_header_size = if major_version == 2 {
ID3V22_FRAME_HEADER_SIZE
} else {
ID3V23_FRAME_HEADER_SIZE
};
let xmp_frame_id = if major_version == 2 {
XMP_V22_ID
} else {
XMP_V23_ID
};
let header_pos = writer.stream_position()?;
writer.write_all(&header)?;
if (flags & 0x40) != 0 {
let ext_header_size = Self::read_synchsafe_u32_from_reader(&mut reader)?;
let skip_size = if major_version < 4 {
ext_header_size - 4
} else {
ext_header_size
};
let mut ext_header = vec![0u8; skip_size as usize - 4];
reader.read_exact(&mut ext_header)?;
writer.write_all(&ext_header)?;
}
let tag_start = reader.stream_position()?;
let tag_end = tag_start + tag_size as u64;
let mut other_frames = Vec::new();
while reader.stream_position()? < tag_end {
let current_pos = reader.stream_position()?;
if tag_end - current_pos < frame_header_size as u64 {
break;
}
let mut frame_header = vec![0u8; frame_header_size];
reader.read_exact(&mut frame_header)?;
if frame_header.iter().all(|&b| b == 0) {
break;
}
let (frame_id, frame_size) = Self::parse_frame_header(&frame_header, major_version)?;
if frame_id == xmp_frame_id {
reader.seek(SeekFrom::Current(frame_size as i64))?;
} else {
let mut frame_content = vec![0u8; frame_size as usize];
reader.read_exact(&mut frame_content)?;
other_frames.push((frame_header, frame_content));
}
}
let mut new_tag_size = 0u32;
for (frame_header, frame_content) in &other_frames {
new_tag_size += frame_header.len() as u32 + frame_content.len() as u32;
}
let xmp_frame_size = frame_header_size as u32 + frame_content.len() as u32;
new_tag_size += xmp_frame_size;
for (frame_header, frame_content) in &other_frames {
writer.write_all(frame_header)?;
writer.write_all(frame_content)?;
}
Self::write_xmp_frame(writer, major_version, &frame_content)?;
let current_pos = writer.stream_position()?;
writer.seek(SeekFrom::Start(header_pos))?;
writer.write_all(&header[0..6])?; Self::write_synchsafe_u32(&mut header[6..10], new_tag_size)?;
writer.write_all(&header[6..10])?; writer.seek(SeekFrom::Start(current_pos))?;
reader.seek(SeekFrom::Start(tag_start + tag_size as u64))?;
std::io::copy(&mut reader, writer)?;
Ok(())
}
fn write_new_id3v2_tag<W: Write + Seek>(writer: &mut W, frame_content: &[u8]) -> XmpResult<()> {
let mut header = [0u8; ID3_TAG_HEADER_SIZE];
header[0..3].copy_from_slice(b"ID3");
header[3] = 3; header[4] = 0; header[5] = 0;
let frame_size = frame_content.len() as u32;
let tag_size = ID3V23_FRAME_HEADER_SIZE as u32 + frame_size;
Self::write_synchsafe_u32(&mut header[6..10], tag_size)?;
writer.write_all(&header)?;
Self::write_xmp_frame(writer, 3, frame_content)?;
Ok(())
}
fn write_xmp_frame<W: Write + Seek>(
writer: &mut W,
major_version: u8,
frame_content: &[u8],
) -> XmpResult<()> {
let frame_header_size = if major_version == 2 {
ID3V22_FRAME_HEADER_SIZE
} else {
ID3V23_FRAME_HEADER_SIZE
};
let xmp_frame_id = if major_version == 2 {
XMP_V22_ID
} else {
XMP_V23_ID
};
let mut frame_header = vec![0u8; frame_header_size];
frame_header[0..xmp_frame_id.len()].copy_from_slice(xmp_frame_id);
let frame_size = frame_content.len() as u32;
if major_version == 2 {
frame_header[3] = ((frame_size >> 16) & 0xFF) as u8;
frame_header[4] = ((frame_size >> 8) & 0xFF) as u8;
frame_header[5] = (frame_size & 0xFF) as u8;
} else if major_version == 4 {
Self::write_synchsafe_u32(&mut frame_header[4..8], frame_size)?;
} else {
frame_header[4] = ((frame_size >> 24) & 0xFF) as u8;
frame_header[5] = ((frame_size >> 16) & 0xFF) as u8;
frame_header[6] = ((frame_size >> 8) & 0xFF) as u8;
frame_header[7] = (frame_size & 0xFF) as u8;
}
writer.write_all(&frame_header)?;
writer.write_all(frame_content)?;
Ok(())
}
fn read_synchsafe_u32(bytes: &[u8]) -> XmpResult<u32> {
if bytes.len() < 4 {
return Err(XmpError::BadValue(
"Not enough bytes for synchsafe integer".to_string(),
));
}
let raw = u32::from(bytes[0]) << 24
| u32::from(bytes[1]) << 16
| u32::from(bytes[2]) << 8
| u32::from(bytes[3]);
if (raw & 0x80808080) != 0 {
return Err(XmpError::BadValue("Invalid synchsafe integer".to_string()));
}
Ok((raw & 0x7F)
| ((raw >> 1) & 0x3F80)
| ((raw >> 2) & 0x1FC000)
| ((raw >> 3) & 0x0FE00000))
}
fn read_synchsafe_u32_from_reader<R: Read>(reader: &mut R) -> XmpResult<u32> {
let mut bytes = [0u8; 4];
reader.read_exact(&mut bytes)?;
Self::read_synchsafe_u32(&bytes)
}
fn parse_frame_header(frame_header: &[u8], major_version: u8) -> XmpResult<(&[u8], u32)> {
let frame_id = if major_version == 2 {
&frame_header[0..3]
} else {
&frame_header[0..4]
};
let frame_size = if major_version == 2 {
u32::from(frame_header[3]) << 16
| u32::from(frame_header[4]) << 8
| u32::from(frame_header[5])
} else if major_version == 4 {
Self::read_synchsafe_u32(&frame_header[4..8])?
} else {
u32::from(frame_header[4]) << 24
| u32::from(frame_header[5]) << 16
| u32::from(frame_header[6]) << 8
| u32::from(frame_header[7])
};
Ok((frame_id, frame_size))
}
fn read_xmp_frame_content<R: Read + Seek>(
reader: &mut R,
frame_size: u32,
) -> XmpResult<Option<XmpMeta>> {
let mut frame_content = vec![0u8; frame_size as usize];
reader.read_exact(&mut frame_content)?;
if frame_content.len() < 4 || &frame_content[0..4] != b"XMP\0" {
return Ok(None);
}
let xmp_packet = &frame_content[4..];
let xmp_str = String::from_utf8(xmp_packet.to_vec())
.map_err(|e| XmpError::ParseError(format!("Invalid UTF-8 in XMP: {}", e)))?;
Ok(Some(XmpMeta::parse(&xmp_str)?))
}
fn write_synchsafe_u32(bytes: &mut [u8], value: u32) -> XmpResult<()> {
if bytes.len() < 4 {
return Err(XmpError::BadValue(
"Not enough bytes for synchsafe integer".to_string(),
));
}
if value > 0x0FFFFFFF {
return Err(XmpError::BadValue(
"Value too large for synchsafe integer".to_string(),
));
}
let encoded = (value & 0x7F)
| ((value & 0x3F80) << 1)
| ((value & 0x1FC000) << 2)
| ((value & 0x0FE00000) << 3);
bytes[0] = ((encoded >> 24) & 0xFF) as u8;
bytes[1] = ((encoded >> 16) & 0xFF) as u8;
bytes[2] = ((encoded >> 8) & 0xFF) as u8;
bytes[3] = (encoded & 0xFF) as u8;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::metadata::XmpMeta;
use crate::core::namespace::ns;
use crate::types::value::XmpValue;
use std::io::Cursor;
fn create_minimal_mp3() -> Vec<u8> {
let mut mp3 = Vec::new();
mp3.extend_from_slice(b"ID3");
mp3.extend_from_slice(&[0x03, 0x00]); mp3.push(0x00); mp3.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); mp3
}
#[test]
fn test_read_xmp_no_xmp() {
let mp3_data = create_minimal_mp3();
let reader = Cursor::new(mp3_data);
let result = Mp3Handler::read_xmp(reader).unwrap();
assert!(result.is_none());
}
#[test]
fn test_invalid_mp3() {
let invalid_data = vec![0x00, 0x01, 0x02, 0x03];
let reader = Cursor::new(invalid_data);
let result = Mp3Handler::read_xmp(reader);
assert!(result.is_err());
let no_id3_data = vec![0x00; 10];
let reader2 = Cursor::new(no_id3_data);
let result2 = Mp3Handler::read_xmp(reader2);
assert!(result2.is_ok());
assert!(result2.unwrap().is_none());
}
#[test]
fn test_write_xmp() {
let mp3_data = create_minimal_mp3();
let reader = Cursor::new(mp3_data.clone());
let mut writer = Cursor::new(Vec::new());
let mut meta = XmpMeta::new();
meta.set_property(ns::DC, "title", XmpValue::String("Test Audio".to_string()))
.unwrap();
Mp3Handler::write_xmp(reader, &mut writer, &meta).unwrap();
writer.set_position(0);
let result = Mp3Handler::read_xmp(writer).unwrap();
assert!(result.is_some(), "XMP should be readable after write");
let read_meta = result.unwrap();
let title_value = read_meta.get_property(ns::DC, "title");
assert!(title_value.is_some());
if let Some(XmpValue::String(title)) = title_value {
assert_eq!(title, "Test Audio");
} else {
panic!("Expected string value");
}
}
#[test]
fn test_synchsafe_u32() {
let test_values = vec![0, 1, 255, 256, 1000, 1000000];
for value in test_values {
let mut bytes = [0u8; 4];
Mp3Handler::write_synchsafe_u32(&mut bytes, value).unwrap();
let decoded = Mp3Handler::read_synchsafe_u32(&bytes).unwrap();
assert_eq!(value, decoded);
}
}
}