commit 439c9fddd6ea5a49bdb67f98714b593e6564957a
parent f7c044f3566bfbf90c9bc091501cef696488910c
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date: Fri, 29 Sep 2023 12:33:20 -0400
Support for MP3 (#295)
* Support for MP3
Remove obsolete unit test
fix potential errors writing streams for PNG and RIFF
* format issue
* PR review fixes
* Attempt to appease build check
* fix formatting
* Bug fix and build issue fix
* Fix formatting
* Avoid extra allocations (#298)
* Variation that avoids Vec allocation and avoids copying manifests that cause errors
* cargo fmt
---------
Co-authored-by: Eric Scouten <scouten@adobe.com>
Diffstat:
7 files changed, 515 insertions(+), 17 deletions(-)
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
@@ -76,6 +76,7 @@ coset = "0.3.1"
extfmt = "0.1.1"
fast-xml = "0.23.1"
hex = "0.4.3"
+id3 = "1.7.0"
img-parts = "0.3.0"
log = "0.4.8"
lopdf = { version = "0.31.0", optional = true }
diff --git a/sdk/src/asset_handlers/mod.rs b/sdk/src/asset_handlers/mod.rs
@@ -14,6 +14,7 @@
pub mod bmff_io;
pub mod c2pa_io;
pub mod jpeg_io;
+pub mod mp3_io;
pub mod png_io;
pub mod riff_io;
pub mod svg_io;
diff --git a/sdk/src/asset_handlers/mp3_io.rs b/sdk/src/asset_handlers/mp3_io.rs
@@ -0,0 +1,476 @@
+// Copyright 2023 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::{
+ fs::{File, OpenOptions},
+ io::{Cursor, Seek, SeekFrom, Write},
+ path::Path,
+};
+
+use byteorder::{BigEndian, ReadBytesExt};
+use conv::ValueFrom;
+use id3::{frame::EncapsulatedObject, *};
+use tempfile::Builder;
+use twoway::find_bytes;
+
+use crate::{
+ asset_io::{
+ AssetIO, AssetPatch, CAIRead, CAIReadWrapper, CAIReadWrite, CAIReadWriteWrapper, CAIReader,
+ CAIWriter, HashBlockObjectType, HashObjectPositions, RemoteRefEmbed,
+ },
+ error::{Error, Result},
+};
+
+static SUPPORTED_TYPES: [&str; 2] = ["mp3", "audio/mpeg"];
+
+const GEOB_FRAME_MIME_TYPE: &str = "application/x-c2pa-manifest-store";
+const GEOB_FRAME_FILE_NAME: &str = "c2pa";
+const GEOB_FRAME_DESCRIPTION: &str = "c2pa manifest store";
+
+struct ID3V2Header {
+ _version_major: u8,
+ _version_minor: u8,
+ _flags: u8,
+ tag_size: u32,
+}
+
+impl ID3V2Header {
+ pub fn read_header(reader: &mut dyn CAIRead) -> Result<ID3V2Header> {
+ let mut header = [0; 10];
+ reader.read_exact(&mut header).map_err(Error::IoError)?;
+
+ if &header[0..3] != b"ID3" {
+ return Err(Error::UnsupportedType);
+ }
+
+ let (version_major, version_minor) = (header[3], header[4]);
+ if !(2..=4).contains(&version_major) {
+ return Err(Error::UnsupportedType);
+ }
+
+ let flags = header[5];
+
+ let mut size_reader = Cursor::new(&header[6..10]);
+ let encoded_tag_size = size_reader
+ .read_u32::<BigEndian>()
+ .map_err(|_err| Error::InvalidAsset("could not read mp3 tag size".to_string()))?;
+ let tag_size = ID3V2Header::decode_tag_size(encoded_tag_size);
+
+ Ok(ID3V2Header {
+ _version_major: version_major,
+ _version_minor: version_minor,
+ _flags: flags,
+ tag_size,
+ })
+ }
+
+ pub fn get_size(&self) -> u32 {
+ self.tag_size + 10
+ }
+
+ fn decode_tag_size(n: u32) -> u32 {
+ n & 0xff | (n & 0xff00) >> 1 | (n & 0xff0000) >> 2 | (n & 0xff000000) >> 3
+ }
+}
+
+fn get_manifest_pos(input_stream: &mut dyn CAIRead) -> Option<(u64, u32)> {
+ input_stream.rewind().ok()?;
+ let header = ID3V2Header::read_header(input_stream).ok()?;
+ input_stream.rewind().ok()?;
+
+ let reader = CAIReadWrapper {
+ reader: input_stream,
+ };
+
+ if let Ok(tag) = Tag::read_from(reader) {
+ let mut manifests = Vec::new();
+
+ for eo in tag.encapsulated_objects() {
+ if eo.mime_type == GEOB_FRAME_MIME_TYPE {
+ manifests.push(eo.data.clone());
+ }
+ }
+
+ if manifests.len() == 1 {
+ input_stream.rewind().ok()?;
+
+ let mut tag_bytes = vec![0u8; header.get_size() as usize];
+ input_stream.read_exact(tag_bytes.as_mut_slice()).ok()?;
+
+ let pos = find_bytes(&tag_bytes, &manifests[0])?;
+
+ return Some((pos as u64, manifests[0].len() as u32));
+ }
+ }
+ None
+}
+
+pub struct Mp3IO {
+ _mp3_format: String,
+}
+
+impl CAIReader for Mp3IO {
+ fn read_cai(&self, input_stream: &mut dyn CAIRead) -> Result<Vec<u8>> {
+ let mut manifest: Option<Vec<u8>> = None;
+
+ if let Ok(tag) = Tag::read_from(input_stream) {
+ for eo in tag.encapsulated_objects() {
+ if eo.mime_type == GEOB_FRAME_MIME_TYPE {
+ match manifest {
+ Some(_) => {
+ return Err(Error::TooManyManifestStores);
+ }
+ None => manifest = Some(eo.data.clone()),
+ }
+ }
+ }
+ }
+
+ manifest.ok_or(Error::JumbfNotFound)
+ }
+
+ // Get XMP block
+ fn read_xmp(&self, _input_stream: &mut dyn CAIRead) -> Option<String> {
+ None
+ }
+}
+
+fn add_required_frame(
+ asset_type: &str,
+ input_stream: &mut dyn CAIRead,
+ output_stream: &mut dyn CAIReadWrite,
+) -> Result<()> {
+ let mp3io = Mp3IO::new(asset_type);
+
+ input_stream.rewind()?;
+
+ match mp3io.read_cai(input_stream) {
+ Ok(_) => {
+ // just clone
+ input_stream.rewind()?;
+ output_stream.rewind()?;
+ std::io::copy(input_stream, output_stream)?;
+ Ok(())
+ }
+ Err(_) => {
+ input_stream.rewind()?;
+ mp3io.write_cai(input_stream, output_stream, &[1, 2, 3, 4]) // save arbitrary data
+ }
+ }
+}
+
+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 CAIReader {
+ self
+ }
+
+ fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> {
+ Some(Box::new(Mp3IO::new(asset_type)))
+ }
+
+ fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> {
+ Some(self)
+ }
+
+ fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> {
+ let mut f = File::open(asset_path)?;
+ self.read_cai(&mut f)
+ }
+
+ fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
+ let mut input_stream = std::fs::OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open(asset_path)
+ .map_err(Error::IoError)?;
+
+ let mut temp_file = Builder::new()
+ .prefix("c2pa_temp")
+ .rand_bytes(5)
+ .tempfile()?;
+
+ self.write_cai(&mut input_stream, &mut temp_file, store_bytes)?;
+
+ // copy temp file to asset
+ std::fs::rename(temp_file.path(), asset_path)
+ // if rename fails, try to copy in case we are on different volumes
+ .or_else(|_| std::fs::copy(temp_file.path(), asset_path).and(Ok(())))
+ .map_err(Error::IoError)
+ }
+
+ fn get_object_locations(
+ &self,
+ asset_path: &std::path::Path,
+ ) -> Result<Vec<HashObjectPositions>> {
+ let mut f = std::fs::File::open(asset_path).map_err(|_err| Error::EmbeddingError)?;
+
+ self.get_object_locations_from_stream(&mut f)
+ }
+
+ fn remove_cai_store(&self, asset_path: &Path) -> Result<()> {
+ self.save_cai_store(asset_path, &[])
+ }
+
+ fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> {
+ None
+ }
+
+ fn supported_types(&self) -> &[&str] {
+ &SUPPORTED_TYPES
+ }
+}
+
+impl CAIWriter for Mp3IO {
+ fn write_cai(
+ &self,
+ input_stream: &mut dyn CAIRead,
+ output_stream: &mut dyn CAIReadWrite,
+ store_bytes: &[u8],
+ ) -> Result<()> {
+ let header = ID3V2Header::read_header(input_stream)?;
+ input_stream.rewind()?;
+
+ let mut out_tag = Tag::new();
+
+ // wrapper to protect imput stream from being gobbled
+ let reader = CAIReadWrapper {
+ reader: input_stream,
+ };
+
+ if let Ok(tag) = Tag::read_from(reader) {
+ for f in tag.frames() {
+ match f.content() {
+ // remove existing manifest keeping existing frames
+ Content::EncapsulatedObject(eo) => {
+ if eo.mime_type != "application/x-c2pa-manifest-store" {
+ out_tag.add_frame(f.clone());
+ }
+ }
+ _ => {
+ out_tag.add_frame(f.clone());
+ }
+ }
+ }
+ }
+
+ // only add new tags
+ if !store_bytes.is_empty() {
+ // Add new manifest store
+ let frame = Frame::with_content(
+ "GEOB",
+ Content::EncapsulatedObject(EncapsulatedObject {
+ mime_type: GEOB_FRAME_MIME_TYPE.to_string(),
+ filename: GEOB_FRAME_FILE_NAME.to_string(),
+ description: GEOB_FRAME_DESCRIPTION.to_string(),
+ data: store_bytes.to_vec(),
+ }),
+ );
+
+ out_tag.add_frame(frame);
+ }
+
+ // wrapper to protect output stream from being gobbled
+ let writer = CAIReadWriteWrapper {
+ reader_writer: output_stream,
+ };
+
+ // write new tag to output stream
+ out_tag
+ .write_to(writer, Version::Id3v24)
+ .map_err(|_e| Error::EmbeddingError)?;
+
+ // skip past old ID3V2
+ input_stream.seek(SeekFrom::Start(header.get_size() as u64))?;
+
+ // copy source data to output
+ std::io::copy(input_stream, output_stream)?;
+
+ Ok(())
+ }
+
+ fn get_object_locations_from_stream(
+ &self,
+ input_stream: &mut dyn CAIRead,
+ ) -> Result<Vec<HashObjectPositions>> {
+ let output_buf: Vec<u8> = Vec::new();
+ let mut output_stream = Cursor::new(output_buf);
+
+ add_required_frame(&self._mp3_format, input_stream, &mut output_stream)?;
+
+ let mut positions: Vec<HashObjectPositions> = Vec::new();
+
+ let (manifest_pos, manifest_len) =
+ get_manifest_pos(&mut output_stream).ok_or(Error::EmbeddingError)?;
+
+ positions.push(HashObjectPositions {
+ offset: usize::value_from(manifest_pos)
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?,
+ length: usize::value_from(manifest_len)
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?,
+ htype: HashBlockObjectType::Cai,
+ });
+
+ // add hash of chunks before cai
+ positions.push(HashObjectPositions {
+ offset: 0,
+ length: usize::value_from(manifest_pos)
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?,
+ htype: HashBlockObjectType::Other,
+ });
+
+ // add position from cai to end
+ let end = u64::value_from(manifest_pos)
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?
+ + u64::value_from(manifest_len)
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
+ let file_end = output_stream.seek(SeekFrom::End(0))?;
+ positions.push(HashObjectPositions {
+ offset: usize::value_from(end)
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, // len of cai
+ length: usize::value_from(file_end - end)
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?,
+ htype: HashBlockObjectType::Other,
+ });
+
+ Ok(positions)
+ }
+
+ 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 Mp3IO {
+ fn patch_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
+ let mut asset = OpenOptions::new()
+ .write(true)
+ .read(true)
+ .create(false)
+ .open(asset_path)?;
+
+ let (manifest_pos, manifest_len) =
+ get_manifest_pos(&mut asset).ok_or(Error::EmbeddingError)?;
+
+ if store_bytes.len() == manifest_len as usize {
+ asset.seek(SeekFrom::Start(manifest_pos))?;
+ asset.write_all(store_bytes)?;
+ Ok(())
+ } else {
+ Err(Error::InvalidAsset(
+ "patch_cai_store store size mismatch.".to_string(),
+ ))
+ }
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::panic)]
+ #![allow(clippy::unwrap_used)]
+
+ use tempfile::tempdir;
+
+ use super::*;
+ use crate::utils::{
+ hash_utils::vec_compare,
+ test::{fixture_path, temp_dir_path},
+ };
+
+ #[test]
+ fn test_write_mp3() {
+ let more_data = "some more test data".as_bytes();
+ let source = fixture_path("sample1.mp3");
+
+ let mut success = false;
+ if let Ok(temp_dir) = tempdir() {
+ let output = temp_dir_path(&temp_dir, "sample1-mp3.mp3");
+
+ if let Ok(_size) = std::fs::copy(source, &output) {
+ let mp3_io = Mp3IO::new("mp3");
+
+ if let Ok(()) = mp3_io.save_cai_store(&output, more_data) {
+ if let Ok(read_test_data) = mp3_io.read_cai_store(&output) {
+ assert!(vec_compare(more_data, &read_test_data));
+ success = true;
+ }
+ }
+ }
+ }
+ assert!(success)
+ }
+
+ #[test]
+ fn test_patch_write_mp3() {
+ let test_data = "some test data".as_bytes();
+ let source = fixture_path("sample1.mp3");
+
+ let mut success = false;
+ if let Ok(temp_dir) = tempdir() {
+ let output = temp_dir_path(&temp_dir, "sample1-mp3.mp3");
+
+ if let Ok(_size) = std::fs::copy(source, &output) {
+ let mp3_io = Mp3IO::new("mp3");
+
+ if let Ok(()) = mp3_io.save_cai_store(&output, test_data) {
+ if let Ok(source_data) = mp3_io.read_cai_store(&output) {
+ // create replacement data of same size
+ let mut new_data = vec![0u8; source_data.len()];
+ new_data[..test_data.len()].copy_from_slice(test_data);
+ mp3_io.patch_cai_store(&output, &new_data).unwrap();
+
+ let replaced = mp3_io.read_cai_store(&output).unwrap();
+
+ assert_eq!(new_data, replaced);
+
+ success = true;
+ }
+ }
+ }
+ }
+ assert!(success)
+ }
+
+ #[test]
+ fn test_remove_c2pa() {
+ let source = fixture_path("sample1.mp3");
+
+ let temp_dir = tempdir().unwrap();
+ let output = temp_dir_path(&temp_dir, "sample1-mp3.mp3");
+
+ std::fs::copy(source, &output).unwrap();
+ let mp3_io = Mp3IO::new("wav");
+
+ mp3_io.remove_cai_store(&output).unwrap();
+
+ // read back in asset, JumbfNotFound is expected since it was removed
+ match mp3_io.read_cai_store(&output) {
+ Err(Error::JumbfNotFound) => (),
+ _ => unreachable!(),
+ }
+ }
+}
diff --git a/sdk/src/asset_handlers/png_io.rs b/sdk/src/asset_handlers/png_io.rs
@@ -20,6 +20,7 @@ use std::{
use byteorder::{BigEndian, ReadBytesExt};
use conv::ValueFrom;
use serde_bytes::ByteBuf;
+use tempfile::Builder;
use crate::{
assertions::{BoxMap, C2PA_BOXHASH},
@@ -477,15 +478,18 @@ impl AssetIO for PngIO {
.open(asset_path)
.map_err(Error::IoError)?;
- let mut output_stream = std::fs::OpenOptions::new()
- .read(true)
- .write(true)
- .open(asset_path)
- .map_err(Error::IoError)?;
+ let mut temp_file = Builder::new()
+ .prefix("c2pa_temp")
+ .rand_bytes(5)
+ .tempfile()?;
- self.write_cai(&mut stream, &mut output_stream, store_bytes)?;
+ self.write_cai(&mut stream, &mut temp_file, store_bytes)?;
- Ok(())
+ // copy temp file to asset
+ std::fs::rename(temp_file.path(), asset_path)
+ // if rename fails, try to copy in case we are on different volumes
+ .or_else(|_| std::fs::copy(temp_file.path(), asset_path).and(Ok(())))
+ .map_err(Error::IoError)
}
fn get_object_locations(
diff --git a/sdk/src/asset_handlers/riff_io.rs b/sdk/src/asset_handlers/riff_io.rs
@@ -20,6 +20,7 @@ use std::{
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use conv::ValueFrom;
use riff::*;
+use tempfile::Builder;
use crate::{
asset_io::{
@@ -356,13 +357,18 @@ impl AssetIO for RiffIO {
fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
let mut input_stream = File::open(asset_path)?;
- let mut output_stream = std::fs::OpenOptions::new()
- .read(true)
- .write(true)
- .open(asset_path)
- .map_err(Error::IoError)?;
+ let mut temp_file = Builder::new()
+ .prefix("c2pa_temp")
+ .rand_bytes(5)
+ .tempfile()?;
+
+ self.write_cai(&mut input_stream, &mut temp_file, store_bytes)?;
- self.write_cai(&mut input_stream, &mut output_stream, store_bytes)
+ // copy temp file to asset
+ std::fs::rename(temp_file.path(), asset_path)
+ // if rename fails, try to copy in case we are on different volumes
+ .or_else(|_| std::fs::copy(temp_file.path(), asset_path).and(Ok(())))
+ .map_err(Error::IoError)
}
fn get_object_locations(
diff --git a/sdk/src/jumbf_io.rs b/sdk/src/jumbf_io.rs
@@ -24,8 +24,8 @@ use lazy_static::lazy_static;
use crate::asset_handlers::pdf_io::PdfIO;
use crate::{
asset_handlers::{
- bmff_io::BmffIO, c2pa_io::C2paIO, jpeg_io::JpegIO, png_io::PngIO, riff_io::RiffIO,
- svg_io::SvgIO, tiff_io::TiffIO,
+ bmff_io::BmffIO, c2pa_io::C2paIO, jpeg_io::JpegIO, mp3_io::Mp3IO, png_io::PngIO,
+ riff_io::RiffIO, svg_io::SvgIO, tiff_io::TiffIO,
},
asset_io::{AssetIO, CAIRead, CAIReadWrite, CAIReader, CAIWriter, HashObjectPositions},
error::{Error, Result},
@@ -44,6 +44,7 @@ lazy_static! {
Box::new(RiffIO::new("")),
Box::new(SvgIO::new("")),
Box::new(TiffIO::new("")),
+ Box::new(Mp3IO::new("")),
];
let mut handler_map = HashMap::new();
@@ -71,6 +72,7 @@ lazy_static! {
Box::new(RiffIO::new("")),
Box::new(SvgIO::new("")),
Box::new(TiffIO::new("")),
+ Box::new(Mp3IO::new("")),
];
let mut handler_map = HashMap::new();
@@ -318,6 +320,7 @@ pub mod tests {
Box::new(RiffIO::new("")),
Box::new(TiffIO::new("")),
Box::new(SvgIO::new("")),
+ Box::new(Mp3IO::new("")),
];
// build handler map
@@ -341,6 +344,7 @@ pub mod tests {
Box::new(RiffIO::new("")),
Box::new(TiffIO::new("")),
Box::new(SvgIO::new("")),
+ Box::new(Mp3IO::new("")),
];
// build handler map
@@ -354,8 +358,13 @@ pub mod tests {
#[test]
fn test_get_writer() {
- let handlers: Vec<Box<dyn AssetIO>> =
- vec![Box::new(JpegIO::new("")), Box::new(PngIO::new(""))];
+ let handlers: Vec<Box<dyn AssetIO>> = vec![
+ Box::new(JpegIO::new("")),
+ Box::new(PngIO::new("")),
+ Box::new(Mp3IO::new("")),
+ Box::new(SvgIO::new("")),
+ Box::new(RiffIO::new("")),
+ ];
// build handler map
for h in handlers {
@@ -403,5 +412,6 @@ pub mod tests {
assert!(supported.iter().any(|s| s == "tiff"));
assert!(supported.iter().any(|s| s == "dng"));
assert!(supported.iter().any(|s| s == "svg"));
+ assert!(supported.iter().any(|s| s == "mp3"));
}
}
diff --git a/sdk/tests/fixtures/sample1.mp3 b/sdk/tests/fixtures/sample1.mp3
Binary files differ.