c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

mp3_io.rs (14698B)


      1 // Copyright 2023 Adobe. All rights reserved.
      2 // This file is licensed to you under the Apache License,
      3 // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
      4 // or the MIT license (http://opensource.org/licenses/MIT),
      5 // at your option.
      6 
      7 // Unless required by applicable law or agreed to in writing,
      8 // this software is distributed on an "AS IS" BASIS, WITHOUT
      9 // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
     10 // implied. See the LICENSE-MIT and LICENSE-APACHE files for the
     11 // specific language governing permissions and limitations under
     12 // each license.
     13 
     14 use std::{
     15     fs::{File, OpenOptions},
     16     io::{Cursor, Seek, SeekFrom, Write},
     17     path::Path,
     18 };
     19 
     20 use byteorder::{BigEndian, ReadBytesExt};
     21 use conv::ValueFrom;
     22 use id3::{frame::EncapsulatedObject, *};
     23 use memchr::memmem;
     24 use tempfile::Builder;
     25 
     26 use crate::{
     27     asset_io::{
     28         rename_or_copy, AssetIO, AssetPatch, CAIRead, CAIReadWrapper, CAIReadWrite,
     29         CAIReadWriteWrapper, CAIReader, CAIWriter, HashBlockObjectType, HashObjectPositions,
     30         RemoteRefEmbed,
     31     },
     32     error::{Error, Result},
     33 };
     34 
     35 static SUPPORTED_TYPES: [&str; 2] = ["mp3", "audio/mpeg"];
     36 
     37 const GEOB_FRAME_MIME_TYPE: &str = "application/x-c2pa-manifest-store";
     38 const GEOB_FRAME_FILE_NAME: &str = "c2pa";
     39 const GEOB_FRAME_DESCRIPTION: &str = "c2pa manifest store";
     40 
     41 struct ID3V2Header {
     42     _version_major: u8,
     43     _version_minor: u8,
     44     _flags: u8,
     45     tag_size: u32,
     46 }
     47 
     48 impl ID3V2Header {
     49     pub fn read_header(reader: &mut dyn CAIRead) -> Result<ID3V2Header> {
     50         let mut header = [0; 10];
     51         reader.read_exact(&mut header).map_err(Error::IoError)?;
     52 
     53         if &header[0..3] != b"ID3" {
     54             return Err(Error::UnsupportedType);
     55         }
     56 
     57         let (version_major, version_minor) = (header[3], header[4]);
     58         if !(2..=4).contains(&version_major) {
     59             return Err(Error::UnsupportedType);
     60         }
     61 
     62         let flags = header[5];
     63 
     64         let mut size_reader = Cursor::new(&header[6..10]);
     65         let encoded_tag_size = size_reader
     66             .read_u32::<BigEndian>()
     67             .map_err(|_err| Error::InvalidAsset("could not read mp3 tag size".to_string()))?;
     68         let tag_size = ID3V2Header::decode_tag_size(encoded_tag_size);
     69 
     70         Ok(ID3V2Header {
     71             _version_major: version_major,
     72             _version_minor: version_minor,
     73             _flags: flags,
     74             tag_size,
     75         })
     76     }
     77 
     78     pub const fn get_size(&self) -> u32 {
     79         self.tag_size + 10
     80     }
     81 
     82     const fn decode_tag_size(n: u32) -> u32 {
     83         n & 0xff | (n & 0xff00) >> 1 | (n & 0xff0000) >> 2 | (n & 0xff000000) >> 3
     84     }
     85 }
     86 
     87 fn get_manifest_pos(input_stream: &mut dyn CAIRead) -> Option<(u64, u32)> {
     88     input_stream.rewind().ok()?;
     89     let header = ID3V2Header::read_header(input_stream).ok()?;
     90     input_stream.rewind().ok()?;
     91 
     92     let reader = CAIReadWrapper {
     93         reader: input_stream,
     94     };
     95 
     96     if let Ok(tag) = Tag::read_from(reader) {
     97         let mut manifests = Vec::new();
     98 
     99         for eo in tag.encapsulated_objects() {
    100             if eo.mime_type == GEOB_FRAME_MIME_TYPE {
    101                 manifests.push(eo.data.clone());
    102             }
    103         }
    104 
    105         if manifests.len() == 1 {
    106             input_stream.rewind().ok()?;
    107 
    108             let mut tag_bytes = vec![0u8; header.get_size() as usize];
    109             input_stream.read_exact(tag_bytes.as_mut_slice()).ok()?;
    110 
    111             let pos = memmem::find(&tag_bytes, &manifests[0])?;
    112 
    113             return Some((pos as u64, manifests[0].len() as u32));
    114         }
    115     }
    116     None
    117 }
    118 
    119 pub struct Mp3IO {
    120     _mp3_format: String,
    121 }
    122 
    123 impl CAIReader for Mp3IO {
    124     fn read_cai(&self, input_stream: &mut dyn CAIRead) -> Result<Vec<u8>> {
    125         let mut manifest: Option<Vec<u8>> = None;
    126 
    127         if let Ok(tag) = Tag::read_from(input_stream) {
    128             for eo in tag.encapsulated_objects() {
    129                 if eo.mime_type == GEOB_FRAME_MIME_TYPE {
    130                     match manifest {
    131                         Some(_) => {
    132                             return Err(Error::TooManyManifestStores);
    133                         }
    134                         None => manifest = Some(eo.data.clone()),
    135                     }
    136                 }
    137             }
    138         }
    139 
    140         manifest.ok_or(Error::JumbfNotFound)
    141     }
    142 
    143     // Get XMP block
    144     fn read_xmp(&self, _input_stream: &mut dyn CAIRead) -> Option<String> {
    145         None
    146     }
    147 }
    148 
    149 fn add_required_frame(
    150     asset_type: &str,
    151     input_stream: &mut dyn CAIRead,
    152     output_stream: &mut dyn CAIReadWrite,
    153 ) -> Result<()> {
    154     let mp3io = Mp3IO::new(asset_type);
    155 
    156     input_stream.rewind()?;
    157 
    158     match mp3io.read_cai(input_stream) {
    159         Ok(_) => {
    160             // just clone
    161             input_stream.rewind()?;
    162             output_stream.rewind()?;
    163             std::io::copy(input_stream, output_stream)?;
    164             Ok(())
    165         }
    166         Err(_) => {
    167             input_stream.rewind()?;
    168             mp3io.write_cai(input_stream, output_stream, &[1, 2, 3, 4]) // save arbitrary data
    169         }
    170     }
    171 }
    172 
    173 impl AssetIO for Mp3IO {
    174     fn new(mp3_format: &str) -> Self {
    175         Mp3IO {
    176             _mp3_format: mp3_format.to_string(),
    177         }
    178     }
    179 
    180     fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> {
    181         Box::new(Mp3IO::new(asset_type))
    182     }
    183 
    184     fn get_reader(&self) -> &dyn CAIReader {
    185         self
    186     }
    187 
    188     fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> {
    189         Some(Box::new(Mp3IO::new(asset_type)))
    190     }
    191 
    192     fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> {
    193         Some(self)
    194     }
    195 
    196     fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> {
    197         let mut f = File::open(asset_path)?;
    198         self.read_cai(&mut f)
    199     }
    200 
    201     fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
    202         let mut input_stream = std::fs::OpenOptions::new()
    203             .read(true)
    204             .write(true)
    205             .open(asset_path)
    206             .map_err(Error::IoError)?;
    207 
    208         let mut temp_file = Builder::new()
    209             .prefix("c2pa_temp")
    210             .rand_bytes(5)
    211             .tempfile()?;
    212 
    213         self.write_cai(&mut input_stream, &mut temp_file, store_bytes)?;
    214 
    215         // copy temp file to asset
    216         rename_or_copy(temp_file, asset_path)
    217     }
    218 
    219     fn get_object_locations(
    220         &self,
    221         asset_path: &std::path::Path,
    222     ) -> Result<Vec<HashObjectPositions>> {
    223         let mut f = std::fs::File::open(asset_path).map_err(|_err| Error::EmbeddingError)?;
    224 
    225         self.get_object_locations_from_stream(&mut f)
    226     }
    227 
    228     fn remove_cai_store(&self, asset_path: &Path) -> Result<()> {
    229         self.save_cai_store(asset_path, &[])
    230     }
    231 
    232     fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> {
    233         None
    234     }
    235 
    236     fn supported_types(&self) -> &[&str] {
    237         &SUPPORTED_TYPES
    238     }
    239 }
    240 
    241 impl CAIWriter for Mp3IO {
    242     fn write_cai(
    243         &self,
    244         input_stream: &mut dyn CAIRead,
    245         output_stream: &mut dyn CAIReadWrite,
    246         store_bytes: &[u8],
    247     ) -> Result<()> {
    248         let header = ID3V2Header::read_header(input_stream)?;
    249         input_stream.rewind()?;
    250 
    251         let mut out_tag = Tag::new();
    252 
    253         // wrapper to protect input stream from being gobbled
    254         let reader = CAIReadWrapper {
    255             reader: input_stream,
    256         };
    257 
    258         if let Ok(tag) = Tag::read_from(reader) {
    259             for f in tag.frames() {
    260                 match f.content() {
    261                     // remove existing manifest keeping existing frames
    262                     Content::EncapsulatedObject(eo) => {
    263                         if eo.mime_type != "application/x-c2pa-manifest-store" {
    264                             out_tag.add_frame(f.clone());
    265                         }
    266                     }
    267                     _ => {
    268                         out_tag.add_frame(f.clone());
    269                     }
    270                 }
    271             }
    272         }
    273 
    274         // only add new tags
    275         if !store_bytes.is_empty() {
    276             // Add new manifest store
    277             let frame = Frame::with_content(
    278                 "GEOB",
    279                 Content::EncapsulatedObject(EncapsulatedObject {
    280                     mime_type: GEOB_FRAME_MIME_TYPE.to_string(),
    281                     filename: GEOB_FRAME_FILE_NAME.to_string(),
    282                     description: GEOB_FRAME_DESCRIPTION.to_string(),
    283                     data: store_bytes.to_vec(),
    284                 }),
    285             );
    286 
    287             out_tag.add_frame(frame);
    288         }
    289 
    290         // wrapper to protect output stream from being gobbled
    291         let writer = CAIReadWriteWrapper {
    292             reader_writer: output_stream,
    293         };
    294 
    295         // write new tag to output stream
    296         out_tag
    297             .write_to(writer, Version::Id3v24)
    298             .map_err(|_e| Error::EmbeddingError)?;
    299 
    300         // skip past old ID3V2
    301         input_stream.seek(SeekFrom::Start(header.get_size() as u64))?;
    302 
    303         // copy source data to output
    304         std::io::copy(input_stream, output_stream)?;
    305 
    306         Ok(())
    307     }
    308 
    309     fn get_object_locations_from_stream(
    310         &self,
    311         input_stream: &mut dyn CAIRead,
    312     ) -> Result<Vec<HashObjectPositions>> {
    313         let output_buf: Vec<u8> = Vec::new();
    314         let mut output_stream = Cursor::new(output_buf);
    315 
    316         add_required_frame(&self._mp3_format, input_stream, &mut output_stream)?;
    317 
    318         let mut positions: Vec<HashObjectPositions> = Vec::new();
    319 
    320         let (manifest_pos, manifest_len) =
    321             get_manifest_pos(&mut output_stream).ok_or(Error::EmbeddingError)?;
    322 
    323         positions.push(HashObjectPositions {
    324             offset: usize::value_from(manifest_pos)
    325                 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?,
    326             length: usize::value_from(manifest_len)
    327                 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?,
    328             htype: HashBlockObjectType::Cai,
    329         });
    330 
    331         // add hash of chunks before cai
    332         positions.push(HashObjectPositions {
    333             offset: 0,
    334             length: usize::value_from(manifest_pos)
    335                 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?,
    336             htype: HashBlockObjectType::Other,
    337         });
    338 
    339         // add position from cai to end
    340         let end = u64::value_from(manifest_pos)
    341             .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?
    342             + u64::value_from(manifest_len)
    343                 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
    344         let file_end = output_stream.seek(SeekFrom::End(0))?;
    345         positions.push(HashObjectPositions {
    346             offset: usize::value_from(end)
    347                 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, // len of cai
    348             length: usize::value_from(file_end - end)
    349                 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?,
    350             htype: HashBlockObjectType::Other,
    351         });
    352 
    353         Ok(positions)
    354     }
    355 
    356     fn remove_cai_store_from_stream(
    357         &self,
    358         input_stream: &mut dyn CAIRead,
    359         output_stream: &mut dyn CAIReadWrite,
    360     ) -> Result<()> {
    361         self.write_cai(input_stream, output_stream, &[])
    362     }
    363 }
    364 
    365 impl AssetPatch for Mp3IO {
    366     fn patch_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
    367         let mut asset = OpenOptions::new()
    368             .write(true)
    369             .read(true)
    370             .create(false)
    371             .open(asset_path)?;
    372 
    373         let (manifest_pos, manifest_len) =
    374             get_manifest_pos(&mut asset).ok_or(Error::EmbeddingError)?;
    375 
    376         if store_bytes.len() == manifest_len as usize {
    377             asset.seek(SeekFrom::Start(manifest_pos))?;
    378             asset.write_all(store_bytes)?;
    379             Ok(())
    380         } else {
    381             Err(Error::InvalidAsset(
    382                 "patch_cai_store store size mismatch.".to_string(),
    383             ))
    384         }
    385     }
    386 }
    387 
    388 #[cfg(test)]
    389 pub mod tests {
    390     #![allow(clippy::expect_used)]
    391     #![allow(clippy::panic)]
    392     #![allow(clippy::unwrap_used)]
    393 
    394     use tempfile::tempdir;
    395 
    396     use super::*;
    397     use crate::utils::{
    398         hash_utils::vec_compare,
    399         test::{fixture_path, temp_dir_path},
    400     };
    401 
    402     #[test]
    403     fn test_write_mp3() {
    404         let more_data = "some more test data".as_bytes();
    405         let source = fixture_path("sample1.mp3");
    406 
    407         let mut success = false;
    408         if let Ok(temp_dir) = tempdir() {
    409             let output = temp_dir_path(&temp_dir, "sample1-mp3.mp3");
    410 
    411             if let Ok(_size) = std::fs::copy(source, &output) {
    412                 let mp3_io = Mp3IO::new("mp3");
    413 
    414                 if let Ok(()) = mp3_io.save_cai_store(&output, more_data) {
    415                     if let Ok(read_test_data) = mp3_io.read_cai_store(&output) {
    416                         assert!(vec_compare(more_data, &read_test_data));
    417                         success = true;
    418                     }
    419                 }
    420             }
    421         }
    422         assert!(success)
    423     }
    424 
    425     #[test]
    426     fn test_patch_write_mp3() {
    427         let test_data = "some test data".as_bytes();
    428         let source = fixture_path("sample1.mp3");
    429 
    430         let mut success = false;
    431         if let Ok(temp_dir) = tempdir() {
    432             let output = temp_dir_path(&temp_dir, "sample1-mp3.mp3");
    433 
    434             if let Ok(_size) = std::fs::copy(source, &output) {
    435                 let mp3_io = Mp3IO::new("mp3");
    436 
    437                 if let Ok(()) = mp3_io.save_cai_store(&output, test_data) {
    438                     if let Ok(source_data) = mp3_io.read_cai_store(&output) {
    439                         // create replacement data of same size
    440                         let mut new_data = vec![0u8; source_data.len()];
    441                         new_data[..test_data.len()].copy_from_slice(test_data);
    442                         mp3_io.patch_cai_store(&output, &new_data).unwrap();
    443 
    444                         let replaced = mp3_io.read_cai_store(&output).unwrap();
    445 
    446                         assert_eq!(new_data, replaced);
    447 
    448                         success = true;
    449                     }
    450                 }
    451             }
    452         }
    453         assert!(success)
    454     }
    455 
    456     #[test]
    457     fn test_remove_c2pa() {
    458         let source = fixture_path("sample1.mp3");
    459 
    460         let temp_dir = tempdir().unwrap();
    461         let output = temp_dir_path(&temp_dir, "sample1-mp3.mp3");
    462 
    463         std::fs::copy(source, &output).unwrap();
    464         let mp3_io = Mp3IO::new("wav");
    465 
    466         mp3_io.remove_cai_store(&output).unwrap();
    467 
    468         // read back in asset, JumbfNotFound is expected since it was removed
    469         match mp3_io.read_cai_store(&output) {
    470             Err(Error::JumbfNotFound) => (),
    471             _ => unreachable!(),
    472         }
    473     }
    474 }