use std::{
io::{Cursor, SeekFrom},
path::Path,
};
use id3::Tag;
use crate::{
asset_handlers::id3_helper::{self, ID3V2Header},
asset_io::{
AssetIO, AssetPatch, CAIRead, CAIReadWrapper, CAIReadWrite, CAIReader, CAIWriter,
HashObjectPositions, RemoteRefEmbed, RemoteRefEmbedType,
},
error::{Error, Result},
};
static SUPPORTED_TYPES: [&str; 2] = ["flac", "audio/flac"];
const FLAC_HEADER: &[u8; 4] = b"fLaC";
const ID3_HEADER: &[u8; 3] = b"ID3";
fn read_header(reader: &mut dyn CAIRead) -> Result<Option<ID3V2Header>> {
let mut buf = [0u8; 10];
reader.read_exact(&mut buf).map_err(Error::IoError)?;
if buf[0..3] == *ID3_HEADER {
return ID3V2Header::parse_from_bytes(&buf)
.map_err(|_| Error::FlacError(FlacError::InvalidId3Version));
}
if buf[0..4] == *FLAC_HEADER {
return Ok(None);
}
Err(Error::UnsupportedType)
}
fn validate_flac_stream(reader: &mut dyn CAIRead) -> Result<()> {
let mut marker = [0u8; 4];
reader.read_exact(&mut marker).map_err(Error::IoError)?;
if &marker != b"fLaC" {
return Err(Error::InvalidAsset(
"invalid FLAC stream: missing fLaC marker".to_string(),
));
}
Ok(())
}
fn add_required_frame(
input_stream: &mut dyn CAIRead,
output_stream: &mut dyn CAIReadWrite,
) -> Result<()> {
let flac_io = FlacIO::new("flac");
input_stream.rewind()?;
match flac_io.read_cai(input_stream) {
Ok(_) => {
input_stream.rewind()?;
output_stream.rewind()?;
std::io::copy(input_stream, output_stream)?;
Ok(())
}
Err(Error::JumbfNotFound) => {
input_stream.rewind()?;
flac_io.write_cai(input_stream, output_stream, &[1, 2, 3, 4])
}
Err(Error::TooManyManifestStores) => Ok(()),
Err(e) => Err(e),
}
}
#[derive(Debug, thiserror::Error)]
pub enum FlacError {
#[error("invalid ID3 version for FLAC")]
InvalidId3Version,
}
pub struct FlacIO {
_asset_type: String,
}
impl CAIReader for FlacIO {
fn read_cai(&self, input_stream: &mut dyn CAIRead) -> Result<Vec<u8>> {
input_stream.rewind()?;
let header = read_header(input_stream)?;
input_stream.rewind()?;
if let Some(h) = header {
let mut manifest: Option<Vec<u8>> = None;
let reader = CAIReadWrapper {
reader: input_stream,
};
if let Ok(tag) = Tag::read_from2(reader) {
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()),
}
}
}
}
input_stream.seek(SeekFrom::Start(h.get_size() as u64))?;
validate_flac_stream(input_stream)?;
if let Some(m) = manifest {
return Ok(m);
}
} else {
validate_flac_stream(input_stream)?;
}
Err(Error::JumbfNotFound)
}
fn read_xmp(&self, input_stream: &mut dyn CAIRead) -> Option<String> {
input_stream.rewind().ok()?;
let header = read_header(input_stream).ok()?;
header.as_ref()?;
id3_helper::read_xmp_from_id3(input_stream).ok()?
}
}
impl RemoteRefEmbed for FlacIO {
fn embed_reference(&self, asset_path: &Path, embed_ref: RemoteRefEmbedType) -> Result<()> {
id3_helper::embed_xmp_reference(self, asset_path, embed_ref)
}
fn embed_reference_to_stream(
&self,
source_stream: &mut dyn CAIRead,
output_stream: &mut dyn CAIReadWrite,
embed_ref: RemoteRefEmbedType,
) -> Result<()> {
match embed_ref {
RemoteRefEmbedType::Xmp(url) => {
source_stream.rewind()?;
let header = read_header(source_stream)?;
let id3_end = header.map_or(0, |h| h.get_size()) as u64;
let current_xmp = self.read_xmp(source_stream);
id3_helper::embed_xmp_to_id3_stream(
source_stream,
output_stream,
url,
id3_end,
current_xmp,
)
}
_ => Err(Error::UnsupportedType),
}
}
}
impl AssetIO for FlacIO {
fn new(asset_type: &str) -> Self {
FlacIO {
_asset_type: asset_type.to_string(),
}
}
fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> {
Box::new(FlacIO::new(asset_type))
}
fn get_reader(&self) -> &dyn CAIReader {
self
}
fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> {
Some(Box::new(FlacIO::new(asset_type)))
}
fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> {
Some(self)
}
fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> {
id3_helper::read_cai_store_from_path(self, asset_path)
}
fn save_cai_store(&self, asset_path: &Path, store_bytes: &[u8]) -> Result<()> {
id3_helper::save_cai_store_to_path(self, asset_path, store_bytes)
}
fn get_object_locations(&self, asset_path: &Path) -> Result<Vec<HashObjectPositions>> {
id3_helper::get_object_locations_from_path(self, asset_path)
}
fn remove_cai_store(&self, asset_path: &Path) -> Result<()> {
self.save_cai_store(asset_path, &[])
}
fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> {
Some(self)
}
fn supported_types(&self) -> &[&str] {
&SUPPORTED_TYPES
}
}
impl CAIWriter for FlacIO {
fn write_cai(
&self,
input_stream: &mut dyn CAIRead,
output_stream: &mut dyn CAIReadWrite,
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_from_stream(
&self,
input_stream: &mut dyn CAIRead,
) -> Result<Vec<HashObjectPositions>> {
let mut output_stream = Cursor::new(Vec::<u8>::new());
add_required_frame(input_stream, &mut output_stream)?;
id3_helper::get_object_locations(&mut output_stream)
}
fn remove_cai_store_from_stream(
&self,
input_stream: &mut dyn CAIRead,
output_stream: &mut dyn CAIReadWrite,
) -> Result<()> {
self.write_cai(input_stream, output_stream, &[])
}
}
impl AssetPatch for FlacIO {
fn patch_cai_store(&self, asset_path: &Path, store_bytes: &[u8]) -> Result<()> {
id3_helper::patch_cai_in_id3_asset(asset_path, store_bytes)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
#![allow(clippy::unwrap_used)]
use std::{io::Cursor, path::Path};
use super::*;
use crate::{
asset_handlers::id3_helper::test_helpers,
error::Error,
utils::{io_utils::tempdirectory, test::fixture_path},
};
const MINIMAL_FLAC: &[u8] = include_bytes!("../../tests/fixtures/sample1.flac");
fn fixture() -> std::path::PathBuf {
fixture_path("sample1.flac")
}
#[test]
fn test_write_flac() {
let handler = FlacIO::new("flac");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "sample1-out.flac");
test_helpers::run_write_read_roundtrip(&handler, &fixture(), &out);
}
#[test]
fn test_patch_write_flac() {
let handler = FlacIO::new("flac");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "sample1-patch.flac");
test_helpers::run_patch_same_size(&handler, &fixture(), &out);
}
#[test]
fn test_patch_cai_store_size_mismatch() {
let handler = FlacIO::new("flac");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "patch_mismatch.flac");
test_helpers::run_patch_size_mismatch(&handler, &fixture(), &out);
}
#[test]
fn test_remove_c2pa_flac() {
let handler = FlacIO::new("flac");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "sample1-nomanifest.flac");
test_helpers::run_remove_manifest(&handler, &fixture(), &out);
}
#[test]
fn test_remote_ref_flac() {
let handler = FlacIO::new("flac");
test_helpers::run_remote_ref_xmp(&handler, &handler, &fixture());
}
#[test]
fn test_get_object_locations_flac_structure() {
let handler = FlacIO::new("flac");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "locs_struct.flac");
test_helpers::run_get_object_locations_structure(&handler, &fixture(), &out);
}
#[test]
fn test_remove_cai_store_from_stream() {
let handler = FlacIO::new("flac");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "stream_remove.flac");
test_helpers::run_remove_from_stream(&handler, &fixture(), &out);
}
#[test]
fn test_write_cai_empty_store_removes_manifest() {
let handler = FlacIO::new("flac");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "empty_write.flac");
test_helpers::run_write_cai_empty_removes(&handler, &fixture(), &out);
}
#[test]
fn test_embed_reference_to_stream_unsupported_type() {
let handler = FlacIO::new("flac");
test_helpers::run_embed_reference_unsupported(&handler, &fixture());
}
#[test]
fn test_supported_types() {
let handler = FlacIO::new("flac");
test_helpers::run_supported_types(&handler, "flac", "audio/flac");
}
#[test]
fn test_embed_reference_file_path() {
let handler = FlacIO::new("flac");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "embed_ref.flac");
test_helpers::run_embed_reference_file_path(&handler, &handler, &fixture(), &out);
}
#[test]
fn test_read_cai_success_with_manifest() {
let handler = FlacIO::new("flac");
let temp = tempdirectory().unwrap();
let out = crate::utils::test::temp_dir_path(&temp, "with_manifest.flac");
test_helpers::run_read_cai_success_with_manifest(&handler, &fixture(), &out);
}
#[test]
fn test_read_cai_store_no_id3() {
let flac_io = FlacIO::new("flac");
let mut cursor = Cursor::new(MINIMAL_FLAC);
match flac_io.read_cai(&mut cursor) {
Err(Error::JumbfNotFound) => {}
other => panic!("expected JumbfNotFound for pure FLAC, got {:?}", other),
}
}
#[test]
fn test_read_cai_unsupported_type() {
test_helpers::run_read_cai_unsupported_type(&FlacIO::new("flac"));
}
#[test]
fn test_read_cai_invalid_id3_version() {
let flac_io = FlacIO::new("flac");
let mut buf = test_helpers::id3_header(1, 0).to_vec();
buf.extend_from_slice(MINIMAL_FLAC);
let mut cursor = Cursor::new(buf);
match flac_io.read_cai(&mut cursor) {
Err(Error::FlacError(FlacError::InvalidId3Version)) => {}
other => panic!("expected FlacError(InvalidId3Version), got {:?}", other),
}
}
#[test]
fn test_read_cai_io_error_too_short() {
test_helpers::run_read_cai_io_error_too_short(&FlacIO::new("flac"));
}
#[test]
fn test_read_cai_invalid_flac_after_id3() {
let flac_io = FlacIO::new("flac");
let mut buf = test_helpers::id3_header(4, 0).to_vec();
buf.extend_from_slice(b"XXXX");
buf.extend_from_slice(MINIMAL_FLAC);
let mut cursor = Cursor::new(buf);
match flac_io.read_cai(&mut cursor) {
Err(_) => {}
Ok(_) => panic!("expected error for ID3 followed by non-FLAC bytes"),
}
}
#[test]
fn test_read_cai_too_many_manifest_stores() {
test_helpers::run_read_cai_too_many_manifest_stores(&FlacIO::new("flac"), MINIMAL_FLAC);
}
#[test]
fn test_get_handler_and_reader() {
let flac_io = FlacIO::new("flac");
let handler = flac_io.get_handler("audio/flac");
let reader = flac_io.get_reader();
let mut cursor = Cursor::new(MINIMAL_FLAC);
match reader.read_cai(&mut cursor) {
Err(Error::JumbfNotFound) => {}
other => panic!("unexpected: {:?}", other),
}
assert!(handler.supported_types().contains(&"audio/flac"));
}
#[test]
fn test_read_cai_store_file_not_found() {
let flac_io = FlacIO::new("flac");
let path = Path::new("/nonexistent/sample.flac");
match flac_io.read_cai_store(path) {
Err(Error::IoError(_)) => {}
other => panic!("expected IoError for missing file, got {:?}", other),
}
}
}