use std::io::Cursor;
use id3::Tag;
use crate::{
asset_handlers::id3_helper::{self, ID3V2Header},
asset_io::{
AssetIO, AssetPatch, C2paReader, C2paWriter, ObjectLocations, ReadSeek, ReadWriteSeek,
RemoteManifestUrl, WriteXmp,
},
error::{Error, Result},
};
static SUPPORTED_TYPES: [&str; 5] = [
"mp3",
"audio/mpeg",
"audio/mp3",
"audio/x-mp3",
"audio/mpeg3",
];
fn read_header(reader: &mut dyn ReadSeek) -> Result<Option<ID3V2Header>> {
let mut buf = [0u8; 10];
reader.read_exact(&mut buf)?;
match ID3V2Header::parse_from_bytes(&buf)? {
Some(h) => Ok(Some(h)),
None => {
if buf[0] == 0xff && (buf[1] & 0xe0 == 0xe0) {
Ok(None)
} else {
Err(Error::UnsupportedType)
}
}
}
}
fn add_required_frame(
asset_type: &str,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
) -> Result<()> {
let mp3io = Mp3IO::new(asset_type);
input_stream.rewind()?;
match mp3io.read_c2pa(input_stream) {
Ok(_) => {
input_stream.rewind()?;
output_stream.rewind()?;
std::io::copy(input_stream, output_stream)?;
Ok(())
}
Err(Error::JumbfNotFound) => {
input_stream.rewind()?;
mp3io.write_c2pa(input_stream, output_stream, &[1, 2, 3, 4])
}
Err(Error::TooManyManifestStores) => Ok(()),
Err(e) => Err(e),
}
}
pub struct Mp3IO {
_mp3_format: String,
}
impl C2paReader for Mp3IO {
fn read_c2pa(&self, input_stream: &mut dyn ReadSeek) -> Result<Vec<u8>> {
input_stream.rewind()?;
let mut manifest: Option<Vec<u8>> = None;
if let Ok(tag) = Tag::read_from2(input_stream) {
for eo in tag.encapsulated_objects() {
if id3_helper::is_c2pa_mime_type(&eo.mime_type) {
match manifest {
Some(_) => return Err(Error::TooManyManifestStores),
None => manifest = Some(eo.data.clone()),
}
}
}
}
manifest.ok_or(Error::JumbfNotFound)
}
fn read_xmp(&self, input_stream: &mut dyn ReadSeek) -> Option<String> {
id3_helper::read_xmp_from_id3(input_stream).ok()?
}
}
impl WriteXmp for Mp3IO {
fn write_xmp(
&self,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
xmp: &str,
) -> Result<()> {
input_stream.rewind()?;
let header = read_header(input_stream)?;
let id3_end = header.map_or(0, |h| h.get_size()) as u64;
id3_helper::write_xmp_to_id3_stream(input_stream, output_stream, xmp, id3_end)
}
}
impl AssetIO for Mp3IO {
fn new(mp3_format: &str) -> Self {
Mp3IO {
_mp3_format: mp3_format.to_string(),
}
}
fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> {
Box::new(Mp3IO::new(asset_type))
}
fn get_reader(&self) -> &dyn C2paReader {
self
}
fn get_writer(&self, asset_type: &str) -> Option<Box<dyn C2paWriter>> {
Some(Box::new(Mp3IO::new(asset_type)))
}
fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> {
Some(self)
}
fn remote_manifest_url_ref(&self) -> Option<&dyn RemoteManifestUrl> {
Some(self)
}
fn write_xmp_ref(&self) -> Option<&dyn WriteXmp> {
Some(self)
}
fn supported_types(&self) -> &[&str] {
&SUPPORTED_TYPES
}
}
impl C2paWriter for Mp3IO {
fn write_c2pa(
&self,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
store_bytes: &[u8],
) -> Result<()> {
input_stream.rewind()?;
let header = read_header(input_stream)?;
let id3_end = header.map_or(0, |h| h.get_size()) as u64;
id3_helper::write_cai_with_id3(input_stream, output_stream, store_bytes, id3_end)
}
fn get_object_locations(
&self,
input_stream: &mut dyn ReadSeek,
) -> Result<Vec<ObjectLocations>> {
let mut output_stream = Cursor::new(Vec::<u8>::new());
add_required_frame(&self._mp3_format, input_stream, &mut output_stream)?;
id3_helper::get_object_locations(&mut output_stream)
}
fn remove_c2pa(
&self,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
) -> Result<()> {
self.write_c2pa(input_stream, output_stream, &[])
}
}
impl AssetPatch for Mp3IO {
fn patch_c2pa(&self, stream: &mut dyn ReadWriteSeek, store_bytes: &[u8]) -> Result<()> {
id3_helper::patch_cai_in_id3_stream(stream, store_bytes)
}
}
#[cfg(test)]
pub mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
#![allow(clippy::unwrap_used)]
use std::io::Cursor;
use super::*;
use crate::{
asset_handlers::id3_helper::test_helpers,
error::Error,
utils::{io_utils::tempdirectory, test::fixture_path},
};
fn fixture() -> std::path::PathBuf {
fixture_path("sample1.mp3")
}
#[test]
fn test_write_mp3() {
let handler = Mp3IO::new("mp3");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "sample1-mp3.mp3");
test_helpers::run_write_read_roundtrip(&handler, &fixture(), &out);
}
#[test]
fn test_patch_write_mp3() {
let handler = Mp3IO::new("mp3");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "sample1-mp3.mp3");
test_helpers::run_patch_same_size(&handler, &fixture(), &out);
}
#[test]
fn test_patch_size_mismatch() {
let handler = Mp3IO::new("mp3");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "sample1-mp3.mp3");
test_helpers::run_patch_size_mismatch(&handler, &fixture(), &out);
}
#[test]
fn test_remove_c2pa() {
let handler = Mp3IO::new("mp3");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "sample1-mp3.mp3");
test_helpers::run_remove_manifest(&handler, &fixture(), &out);
}
#[test]
fn test_remote_ref() {
let handler = Mp3IO::new("mp3");
test_helpers::run_remote_ref_xmp(&handler, &handler, &fixture());
}
#[test]
fn test_get_object_locations_structure() {
let handler = Mp3IO::new("mp3");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "sample1-mp3.mp3");
test_helpers::run_get_object_locations_structure(&handler, &fixture(), &out);
}
#[test]
fn test_remove_from_stream() {
let handler = Mp3IO::new("mp3");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "sample1-mp3.mp3");
test_helpers::run_remove_from_stream(&handler, &fixture(), &out);
}
#[test]
fn test_write_cai_empty_removes() {
let handler = Mp3IO::new("mp3");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "sample1-mp3.mp3");
test_helpers::run_write_cai_empty_removes(&handler, &fixture(), &out);
}
#[test]
fn test_supported_types() {
let handler = Mp3IO::new("mp3");
test_helpers::run_supported_types(&handler, "mp3", "audio/mpeg");
}
#[test]
fn test_embed_reference_file_path() {
let handler = Mp3IO::new("mp3");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "embed_ref.mp3");
test_helpers::run_embed_reference_file_path(&handler, &handler, &fixture(), &out);
}
#[test]
fn test_read_cai_success_with_manifest() {
let handler = Mp3IO::new("mp3");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "with_manifest.mp3");
test_helpers::run_read_cai_success_with_manifest(&handler, &fixture(), &out);
}
#[test]
fn test_read_cai_too_many_manifest_stores() {
test_helpers::run_read_cai_too_many_manifest_stores(&Mp3IO::new("mp3"), &[]);
}
#[test]
fn test_get_handler_and_reader() {
let mp3_io = Mp3IO::new("mp3");
let handler = mp3_io.get_handler("audio/mpeg");
let reader = mp3_io.get_reader();
let mut f = std::fs::File::open(fixture()).unwrap();
match reader.read_c2pa(&mut f) {
Err(Error::JumbfNotFound) => {}
other => panic!(
"unexpected result for fixture without manifest: {:?}",
other
),
}
assert!(handler.supported_types().contains(&"audio/mpeg"));
}
#[test]
fn read_cai_store_malicious_id3v23_compression_does_not_crash() {
let mp3_io = Mp3IO::new("mp3");
let result = mp3_io.read_cai_store(&fixture_path("id3v23_compression_underflow.mp3"));
assert!(
matches!(result, Err(Error::JumbfNotFound)),
"expected JumbfNotFound (no crash), got {result:?}",
);
}
#[test]
fn test_read_cai_store_no_id3() {
let mp3_io = Mp3IO::new("mp3");
let mpeg_stream: Vec<u8> = std::iter::once(0xff_u8)
.chain(std::iter::once(0xe0_u8))
.chain(std::iter::repeat_n(0, 20))
.collect();
let mut cursor = Cursor::new(mpeg_stream);
match mp3_io.read_c2pa(&mut cursor) {
Err(Error::JumbfNotFound) => {}
other => panic!(
"expected JumbfNotFound for bare MPEG stream, got {:?}",
other
),
}
}
#[test]
fn test_write_cai_unsupported_type() {
let mp3_io = Mp3IO::new("mp3");
let mut input = Cursor::new(b"XXXX\x00\x00\x00\x00\x00\x00".to_vec());
let mut output = Cursor::new(Vec::new());
match mp3_io.write_c2pa(&mut input, &mut output, &[1, 2, 3]) {
Err(Error::UnsupportedType) => {}
other => panic!(
"expected UnsupportedType for unknown magic, got {:?}",
other
),
}
}
#[test]
fn test_write_cai_io_error_too_short() {
let mp3_io = Mp3IO::new("mp3");
let mut input = Cursor::new(b"abc".to_vec());
let mut output = Cursor::new(Vec::new());
match mp3_io.write_c2pa(&mut input, &mut output, &[1, 2, 3]) {
Err(Error::IoError(_)) => {}
other => panic!("expected IoError for short stream, got {:?}", other),
}
}
#[test]
fn test_write_cai_invalid_id3_version() {
let mp3_io = Mp3IO::new("mp3");
let mut input = Cursor::new(test_helpers::id3_header(1, 0).to_vec());
let mut output = Cursor::new(Vec::new());
match mp3_io.write_c2pa(&mut input, &mut output, &[1, 2, 3]) {
Err(Error::UnsupportedType) => {}
other => panic!("expected UnsupportedType for ID3v1 header, got {:?}", other),
}
}
}