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

riff_io.rs (29197B)


      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::{LittleEndian, ReadBytesExt, WriteBytesExt};
     21 use conv::ValueFrom;
     22 use riff::*;
     23 use tempfile::Builder;
     24 
     25 use crate::{
     26     asset_io::{
     27         rename_or_copy, AssetIO, AssetPatch, CAIRead, CAIReadWrapper, CAIReadWrite,
     28         CAIReadWriteWrapper, CAIReader, CAIWriter, HashBlockObjectType, HashObjectPositions,
     29         RemoteRefEmbed, RemoteRefEmbedType,
     30     },
     31     error::{Error, Result},
     32     utils::xmp_inmemory_utils::{add_provenance, MIN_XMP},
     33 };
     34 
     35 static SUPPORTED_TYPES: [&str; 12] = [
     36     "avi",
     37     "wav",
     38     "webp",
     39     "image/webp",
     40     "audio/wav",
     41     "audio/wave",
     42     "audio/x-wav",
     43     "audio/vnd.wave",
     44     "application/x-troff-msvideo",
     45     "video/avi",
     46     "video/msvideo",
     47     "video/x-msvideo",
     48 ];
     49 
     50 pub struct RiffIO {
     51     #[allow(dead_code)]
     52     riff_format: String, // can be used for specialized RIFF cases
     53 }
     54 
     55 const C2PA_CHUNK_ID: ChunkId = ChunkId {
     56     value: [0x43, 0x32, 0x50, 0x41],
     57 }; // C2PA
     58 
     59 const VP8X_ID: ChunkId = ChunkId {
     60     value: [0x56, 0x50, 0x38, 0x58],
     61 }; // VP8X  chunk to hold auxiliary info
     62 
     63 const VP8_ID: ChunkId = ChunkId {
     64     value: [0x56, 0x50, 0x38, 0x20],
     65 }; // VP8 chunk
     66 
     67 const VP8L_ID: ChunkId = ChunkId {
     68     value: [0x56, 0x50, 0x38, 0x4c],
     69 }; // VP8L chunk
     70 
     71 const XMP_CHUNK_ID: ChunkId = ChunkId {
     72     value: [0x58, 0x4d, 0x50, 0x20],
     73 }; // XMP
     74 
     75 const XMP_FLAG: u32 = 4;
     76 
     77 fn read_items<T>(iter: &mut T) -> Vec<T::Item>
     78 where
     79     T: Iterator,
     80 {
     81     let mut vec: Vec<T::Item> = Vec::new();
     82     for item in iter {
     83         vec.push(item);
     84     }
     85     vec
     86 }
     87 
     88 fn get_height_and_width(chunk_contents: &[ChunkContents]) -> Result<(u16, u16)> {
     89     if let Some(ChunkContents::Data(_id, chunk_data)) = chunk_contents.iter().find(|c| match c {
     90         ChunkContents::Data(id, _) => *id == VP8L_ID,
     91         _ => false,
     92     }) {
     93         let mut chunk_stream = Cursor::new(chunk_data);
     94         chunk_stream.seek(SeekFrom::Start(1))?; // skip signature byte
     95 
     96         // width and length are 12 bits packed together
     97         let first_bytes = chunk_stream.read_u16::<LittleEndian>()?;
     98         let width = 1 + (first_bytes & 0x3fff); // add 1 for VP8L
     99         let last_two = (first_bytes & 0xc000) >> 14; // last two bits of first bytes are first 2 of height
    100         let height = 1 + (((chunk_stream.read_u16::<LittleEndian>()? & 0xfff) << 2) | last_two);
    101 
    102         return Ok((height, width));
    103     }
    104 
    105     if let Some(ChunkContents::Data(_id, chunk_data)) = chunk_contents.iter().find(|c| match c {
    106         ChunkContents::Data(id, _) => *id == VP8_ID,
    107         _ => false,
    108     }) {
    109         let mut chunk_stream = Cursor::new(chunk_data);
    110         chunk_stream.seek(SeekFrom::Start(6))?; // skip frame tag and start code
    111 
    112         let width = chunk_stream.read_u16::<LittleEndian>()? & 0x3fff;
    113         let height = chunk_stream.read_u16::<LittleEndian>()? & 0x3fff;
    114 
    115         return Ok((height, width));
    116     }
    117 
    118     Err(Error::InvalidAsset(
    119         "WEBP missing VP8 or VP8L segment".to_string(),
    120     ))
    121 }
    122 
    123 fn inject_c2pa<T>(
    124     chunk: &Chunk,
    125     stream: &mut T,
    126     data: &[u8],
    127     xmp_data: Option<&[u8]>,
    128     format: &str,
    129 ) -> Result<ChunkContents>
    130 where
    131     T: std::io::Seek + std::io::Read,
    132 {
    133     let id = chunk.id();
    134     let is_riff_chunk: bool = id == riff::RIFF_ID;
    135     stream.rewind()?;
    136 
    137     if is_riff_chunk || id == riff::LIST_ID {
    138         let chunk_type = chunk.read_type(stream).map_err(|_| {
    139             Error::InvalidAsset("RIFF handler could not parse file format {format}".to_string())
    140         })?;
    141         let mut children = read_items(&mut chunk.iter(stream));
    142         let mut children_contents: Vec<ChunkContents> = Vec::new();
    143 
    144         if is_riff_chunk && !data.is_empty() {
    145             // remove c2pa manifest store in RIFF chunk
    146             children.retain(|c| c.id() != C2PA_CHUNK_ID);
    147         }
    148 
    149         if is_riff_chunk && xmp_data.is_some() {
    150             // remove XMP in RIFF chunk so we can replace
    151             children.retain(|c| c.id() != XMP_CHUNK_ID);
    152         }
    153 
    154         // duplicate all top level children
    155         for child in children {
    156             children_contents.push(inject_c2pa(&child, stream, data, xmp_data, format)?);
    157         }
    158 
    159         // add XMP if needed
    160         if let Some(xmp) = xmp_data {
    161             if is_riff_chunk && !xmp.is_empty() {
    162                 // if this is a webp doc we must also update VP8X
    163                 if format == "webp" {
    164                     // if already present we can patch otherwise add
    165                     if let Some(ChunkContents::Data(_id, chunk_data)) =
    166                         children_contents.iter_mut().find(|c| match c {
    167                             ChunkContents::Data(id, _) => *id == VP8X_ID,
    168                             _ => false,
    169                         })
    170                     {
    171                         let mut chunk_stream = Cursor::new(chunk_data);
    172 
    173                         let mut flags = chunk_stream.read_u32::<LittleEndian>()?;
    174 
    175                         // add in XMP flag
    176                         flags |= XMP_FLAG;
    177 
    178                         chunk_stream.rewind()?;
    179 
    180                         // write back changes
    181                         chunk_stream.write_u32::<LittleEndian>(flags)?;
    182                     } else {
    183                         // add new VP8X
    184 
    185                         // get height and width from VBL
    186                         if let Ok((height, width)) = get_height_and_width(&children_contents) {
    187                             let data: Vec<u8> = Vec::new();
    188                             let mut chunk_writer = Cursor::new(data);
    189 
    190                             let flags: u32 = XMP_FLAG;
    191                             let vp8x_height = height as u32 - 1;
    192                             let vp8x_width = width as u32 - 1;
    193 
    194                             // write flags
    195                             chunk_writer.write_u32::<LittleEndian>(flags)?;
    196 
    197                             // write width then height
    198                             chunk_writer.write_u24::<LittleEndian>(vp8x_width)?;
    199                             chunk_writer.write_u24::<LittleEndian>(vp8x_height)?;
    200 
    201                             // make new VP8X chunk and prepend to children list
    202                             let mut tmp_vec: Vec<ChunkContents> = Vec::new();
    203                             tmp_vec.push(ChunkContents::Data(VP8X_ID, chunk_writer.into_inner()));
    204                             tmp_vec.extend(children_contents);
    205                             children_contents = tmp_vec;
    206                         } else {
    207                             return Err(Error::InvalidAsset(
    208                                 "Could not parse VP8 or VP8L".to_string(),
    209                             ));
    210                         }
    211                     }
    212                 }
    213 
    214                 children_contents.push(ChunkContents::Data(XMP_CHUNK_ID, xmp.to_vec()));
    215             }
    216         }
    217 
    218         // place at the end for maximum compatibility
    219         if is_riff_chunk && !data.is_empty() {
    220             children_contents.push(ChunkContents::Data(C2PA_CHUNK_ID, data.to_vec()));
    221         }
    222 
    223         Ok(ChunkContents::Children(id, chunk_type, children_contents))
    224     } else if id == riff::SEQT_ID {
    225         let children = read_items(&mut chunk.iter_no_type(stream));
    226         let mut children_contents: Vec<ChunkContents> = Vec::new();
    227 
    228         for child in children {
    229             children_contents.push(inject_c2pa(&child, stream, data, xmp_data, format)?);
    230         }
    231 
    232         Ok(ChunkContents::ChildrenNoType(id, children_contents))
    233     } else {
    234         let contents = chunk
    235             .read_contents(stream)
    236             .map_err(|_| Error::InvalidAsset("RIFF handler could not parse file".to_string()))?;
    237         Ok(ChunkContents::Data(id, contents))
    238     }
    239 }
    240 
    241 fn get_manifest_pos(reader: &mut dyn CAIRead) -> Option<(u64, u32)> {
    242     let mut asset: Vec<u8> = Vec::new();
    243     reader.rewind().ok()?;
    244     reader.read_to_end(&mut asset).ok()?;
    245 
    246     let mut chunk_reader = Cursor::new(asset);
    247 
    248     let top_level_chunks = riff::Chunk::read(&mut chunk_reader, 0).ok()?;
    249 
    250     if top_level_chunks.id() == RIFF_ID {
    251         for c in top_level_chunks.iter(&mut chunk_reader) {
    252             if c.id() == C2PA_CHUNK_ID {
    253                 return Some((c.offset(), c.len() + 8)); // 8 is len of data chunk header
    254             }
    255         }
    256     }
    257     None
    258 }
    259 
    260 impl CAIReader for RiffIO {
    261     fn read_cai(&self, input_stream: &mut dyn CAIRead) -> Result<Vec<u8>> {
    262         let mut chunk_reader = CAIReadWrapper {
    263             reader: input_stream,
    264         };
    265 
    266         let top_level_chunks = riff::Chunk::read(&mut chunk_reader, 0)?;
    267 
    268         if top_level_chunks.id() != RIFF_ID {
    269             return Err(Error::InvalidAsset("Invalid RIFF format".to_string()));
    270         }
    271 
    272         for c in top_level_chunks.iter(&mut chunk_reader) {
    273             if c.id() == C2PA_CHUNK_ID {
    274                 return Ok(c.read_contents(&mut chunk_reader)?);
    275             }
    276         }
    277 
    278         Err(Error::JumbfNotFound)
    279     }
    280 
    281     // Get XMP block
    282     fn read_xmp(&self, input_stream: &mut dyn CAIRead) -> Option<String> {
    283         let top_level_chunks = {
    284             let mut reader = CAIReadWrapper {
    285                 reader: input_stream,
    286             };
    287             Chunk::read(&mut reader, 0).ok()?
    288         };
    289 
    290         if top_level_chunks.id() != RIFF_ID {
    291             return None;
    292         }
    293 
    294         let mut chunk_reader = CAIReadWrapper {
    295             reader: input_stream,
    296         };
    297 
    298         for c in top_level_chunks.iter(&mut chunk_reader) {
    299             if c.id() == XMP_CHUNK_ID {
    300                 let output = c.read_contents(&mut chunk_reader).ok()?;
    301                 let output_string = String::from_utf8_lossy(&output);
    302 
    303                 return Some(output_string.to_string());
    304             }
    305         }
    306 
    307         None
    308     }
    309 }
    310 
    311 fn add_required_chunks(
    312     asset_type: &str,
    313     input_stream: &mut dyn CAIRead,
    314     output_stream: &mut dyn CAIReadWrite,
    315 ) -> Result<()> {
    316     let aio = RiffIO::new(asset_type);
    317 
    318     match aio.read_cai(input_stream) {
    319         Ok(_) => {
    320             // just clone
    321             input_stream.rewind()?;
    322             output_stream.rewind()?;
    323             std::io::copy(input_stream, output_stream)?;
    324             Ok(())
    325         }
    326         Err(_) => {
    327             input_stream.rewind()?;
    328             aio.write_cai(input_stream, output_stream, &[1, 2, 3, 4]) // save arbitrary data
    329         }
    330     }
    331 }
    332 
    333 impl AssetIO for RiffIO {
    334     fn new(riff_format: &str) -> Self {
    335         RiffIO {
    336             riff_format: riff_format.to_string(),
    337         }
    338     }
    339 
    340     fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> {
    341         Box::new(RiffIO::new(asset_type))
    342     }
    343 
    344     fn get_reader(&self) -> &dyn CAIReader {
    345         self
    346     }
    347 
    348     fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> {
    349         Some(Box::new(RiffIO::new(asset_type)))
    350     }
    351 
    352     fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> {
    353         Some(self)
    354     }
    355 
    356     fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> {
    357         let mut f = File::open(asset_path)?;
    358         self.read_cai(&mut f)
    359     }
    360 
    361     fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
    362         let mut input_stream = File::open(asset_path)?;
    363 
    364         let mut temp_file = Builder::new()
    365             .prefix("c2pa_temp")
    366             .rand_bytes(5)
    367             .tempfile()?;
    368 
    369         self.write_cai(&mut input_stream, &mut temp_file, store_bytes)?;
    370 
    371         // copy temp file to asset
    372         rename_or_copy(temp_file, asset_path)
    373     }
    374 
    375     fn get_object_locations(
    376         &self,
    377         asset_path: &std::path::Path,
    378     ) -> Result<Vec<HashObjectPositions>> {
    379         let mut f = std::fs::File::open(asset_path).map_err(|_err| Error::EmbeddingError)?;
    380 
    381         self.get_object_locations_from_stream(&mut f)
    382     }
    383 
    384     fn remove_cai_store(&self, asset_path: &Path) -> Result<()> {
    385         self.save_cai_store(asset_path, &[])
    386     }
    387 
    388     fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> {
    389         Some(self)
    390     }
    391 
    392     fn supported_types(&self) -> &[&str] {
    393         &SUPPORTED_TYPES
    394     }
    395 }
    396 
    397 impl CAIWriter for RiffIO {
    398     fn write_cai(
    399         &self,
    400         input_stream: &mut dyn CAIRead,
    401         output_stream: &mut dyn CAIReadWrite,
    402         store_bytes: &[u8],
    403     ) -> Result<()> {
    404         let top_level_chunks = {
    405             let mut reader = CAIReadWrapper {
    406                 reader: input_stream,
    407             };
    408             Chunk::read(&mut reader, 0)?
    409         };
    410 
    411         if top_level_chunks.id() != RIFF_ID {
    412             return Err(Error::InvalidAsset("Invalid RIFF format".to_string()));
    413         }
    414 
    415         let mut reader = CAIReadWrapper {
    416             reader: input_stream,
    417         };
    418 
    419         // replace/add manifest in memory
    420         let new_contents = inject_c2pa(
    421             &top_level_chunks,
    422             &mut reader,
    423             store_bytes,
    424             None,
    425             &self.riff_format,
    426         )?;
    427 
    428         let mut writer = CAIReadWriteWrapper {
    429             reader_writer: output_stream,
    430         };
    431 
    432         // save contents
    433         new_contents
    434             .write(&mut writer)
    435             .map_err(|_e| Error::EmbeddingError)?;
    436         Ok(())
    437     }
    438 
    439     fn get_object_locations_from_stream(
    440         &self,
    441         input_stream: &mut dyn CAIRead,
    442     ) -> Result<Vec<HashObjectPositions>> {
    443         let output_buf: Vec<u8> = Vec::new();
    444         let mut output_stream = Cursor::new(output_buf);
    445 
    446         add_required_chunks(&self.riff_format, input_stream, &mut output_stream)?;
    447 
    448         let mut positions: Vec<HashObjectPositions> = Vec::new();
    449 
    450         let (manifest_pos, manifest_len) =
    451             get_manifest_pos(&mut output_stream).ok_or(Error::EmbeddingError)?;
    452 
    453         positions.push(HashObjectPositions {
    454             offset: usize::value_from(manifest_pos)
    455                 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?,
    456             length: usize::value_from(manifest_len)
    457                 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?,
    458             htype: HashBlockObjectType::Cai,
    459         });
    460 
    461         // add hash of chunks before cai
    462         positions.push(HashObjectPositions {
    463             offset: 0,
    464             length: usize::value_from(manifest_pos)
    465                 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?,
    466             htype: HashBlockObjectType::Other,
    467         });
    468 
    469         // add position from cai to end
    470         let end = u64::value_from(manifest_pos)
    471             .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?
    472             + u64::value_from(manifest_len)
    473                 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
    474         let file_end = output_stream.seek(SeekFrom::End(0))?;
    475         positions.push(HashObjectPositions {
    476             offset: usize::value_from(end)
    477                 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, // len of cai
    478             length: usize::value_from(file_end - end)
    479                 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?,
    480             htype: HashBlockObjectType::Other,
    481         });
    482 
    483         Ok(positions)
    484     }
    485 
    486     fn remove_cai_store_from_stream(
    487         &self,
    488         input_stream: &mut dyn CAIRead,
    489         output_stream: &mut dyn CAIReadWrite,
    490     ) -> Result<()> {
    491         self.write_cai(input_stream, output_stream, &[])
    492     }
    493 }
    494 
    495 impl AssetPatch for RiffIO {
    496     fn patch_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
    497         let mut asset = OpenOptions::new()
    498             .write(true)
    499             .read(true)
    500             .create(false)
    501             .open(asset_path)?;
    502 
    503         let (manifest_pos, manifest_len) =
    504             get_manifest_pos(&mut asset).ok_or(Error::EmbeddingError)?;
    505 
    506         if store_bytes.len() + 8 == manifest_len as usize {
    507             asset.seek(SeekFrom::Start(manifest_pos + 8))?; // skip 8 byte chunk data header
    508             asset.write_all(store_bytes)?;
    509             Ok(())
    510         } else {
    511             Err(Error::InvalidAsset(
    512                 "patch_cai_store store size mismatch.".to_string(),
    513             ))
    514         }
    515     }
    516 }
    517 
    518 impl RemoteRefEmbed for RiffIO {
    519     #[allow(unused_variables)]
    520     fn embed_reference(
    521         &self,
    522         asset_path: &Path,
    523         embed_ref: crate::asset_io::RemoteRefEmbedType,
    524     ) -> Result<()> {
    525         let mut input_stream = File::open(asset_path)?;
    526 
    527         let mut output_stream = std::fs::OpenOptions::new()
    528             .read(true)
    529             .write(true)
    530             .open(asset_path)
    531             .map_err(Error::IoError)?;
    532 
    533         self.embed_reference_to_stream(&mut input_stream, &mut output_stream, embed_ref)
    534     }
    535 
    536     fn embed_reference_to_stream(
    537         &self,
    538         input_stream: &mut dyn CAIRead,
    539         output_stream: &mut dyn CAIReadWrite,
    540         embed_ref: RemoteRefEmbedType,
    541     ) -> Result<()> {
    542         match embed_ref {
    543             crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => {
    544                 match self.riff_format.as_ref() {
    545                     "avi" | "wav" | "webp" => {
    546                         if let Some(curr_xmp) = self.read_xmp(input_stream) {
    547                             let mut new_xmp = add_provenance(&curr_xmp, &manifest_uri)?;
    548                             if new_xmp.len() % 2 == 1 {
    549                                 // pad if needed to even length
    550                                 new_xmp.push(' ');
    551                             }
    552 
    553                             let top_level_chunks = {
    554                                 let mut reader = CAIReadWrapper {
    555                                     reader: input_stream,
    556                                 };
    557                                 Chunk::read(&mut reader, 0)?
    558                             };
    559 
    560                             if top_level_chunks.id() != RIFF_ID {
    561                                 return Err(Error::InvalidAsset("Invalid RIFF format".to_string()));
    562                             }
    563 
    564                             let mut reader = CAIReadWrapper {
    565                                 reader: input_stream,
    566                             };
    567 
    568                             // replace/add manifest in memory
    569                             let new_contents = inject_c2pa(
    570                                 &top_level_chunks,
    571                                 &mut reader,
    572                                 &[],
    573                                 Some(new_xmp.as_bytes()),
    574                                 &self.riff_format,
    575                             )?;
    576 
    577                             // save contents
    578                             let mut writer = CAIReadWriteWrapper {
    579                                 reader_writer: output_stream,
    580                             };
    581                             new_contents
    582                                 .write(&mut writer)
    583                                 .map_err(|_e| Error::EmbeddingError)?;
    584                             Ok(())
    585                         } else {
    586                             let mut new_xmp = add_provenance(MIN_XMP, &manifest_uri)?;
    587 
    588                             if new_xmp.len() % 2 == 1 {
    589                                 // pad if needed to even length
    590                                 new_xmp.push(' ');
    591                             }
    592 
    593                             let top_level_chunks = {
    594                                 let mut reader = CAIReadWrapper {
    595                                     reader: input_stream,
    596                                 };
    597                                 Chunk::read(&mut reader, 0)?
    598                             };
    599 
    600                             if top_level_chunks.id() != RIFF_ID {
    601                                 return Err(Error::InvalidAsset("Invalid RIFF format".to_string()));
    602                             }
    603 
    604                             let mut reader = CAIReadWrapper {
    605                                 reader: input_stream,
    606                             };
    607 
    608                             // replace/add manifest in memory
    609                             let new_contents = inject_c2pa(
    610                                 &top_level_chunks,
    611                                 &mut reader,
    612                                 &[],
    613                                 Some(new_xmp.as_bytes()),
    614                                 &self.riff_format,
    615                             )?;
    616 
    617                             // save contents
    618                             let mut writer = CAIReadWriteWrapper {
    619                                 reader_writer: output_stream,
    620                             };
    621                             new_contents
    622                                 .write(&mut writer)
    623                                 .map_err(|_e| Error::EmbeddingError)?;
    624                             Ok(())
    625                         }
    626                     }
    627                     _ => Err(Error::UnsupportedType),
    628                 }
    629             }
    630             crate::asset_io::RemoteRefEmbedType::StegoS(_) => Err(Error::UnsupportedType),
    631             crate::asset_io::RemoteRefEmbedType::StegoB(_) => Err(Error::UnsupportedType),
    632             crate::asset_io::RemoteRefEmbedType::Watermark(_) => Err(Error::UnsupportedType),
    633         }
    634     }
    635 }
    636 
    637 #[cfg(test)]
    638 pub mod tests {
    639     #![allow(clippy::expect_used)]
    640     #![allow(clippy::panic)]
    641     #![allow(clippy::unwrap_used)]
    642 
    643     use tempfile::tempdir;
    644 
    645     use super::*;
    646     use crate::utils::{
    647         hash_utils::vec_compare,
    648         test::{fixture_path, temp_dir_path},
    649         xmp_inmemory_utils::extract_provenance,
    650     };
    651 
    652     #[test]
    653     fn test_write_wav() {
    654         let more_data = "some more test data".as_bytes();
    655         let source = fixture_path("sample1.wav");
    656 
    657         let mut success = false;
    658         if let Ok(temp_dir) = tempdir() {
    659             let output = temp_dir_path(&temp_dir, "sample1-wav.wav");
    660 
    661             if let Ok(_size) = std::fs::copy(source, &output) {
    662                 let riff_io = RiffIO::new("wav");
    663 
    664                 if let Ok(()) = riff_io.save_cai_store(&output, more_data) {
    665                     if let Ok(read_test_data) = riff_io.read_cai_store(&output) {
    666                         assert!(vec_compare(more_data, &read_test_data));
    667                         success = true;
    668                     }
    669                 }
    670             }
    671         }
    672         assert!(success)
    673     }
    674 
    675     #[test]
    676     fn test_write_wav_stream() {
    677         let more_data = "some more test data".as_bytes();
    678         let mut source = File::open(fixture_path("sample1.wav")).unwrap();
    679 
    680         let riff_io = RiffIO::new("wav");
    681         if let Ok(temp_dir) = tempdir() {
    682             let output = temp_dir_path(&temp_dir, "sample1-wav.wav");
    683 
    684             let mut output_stream = File::create(&output).unwrap();
    685 
    686             riff_io
    687                 .write_cai(&mut source, &mut output_stream, more_data)
    688                 .unwrap();
    689 
    690             let mut source = File::open(output).unwrap();
    691             let read_test_data = riff_io.read_cai(&mut source).unwrap();
    692             assert!(vec_compare(more_data, &read_test_data));
    693         }
    694     }
    695 
    696     #[test]
    697     fn test_patch_write_wav() {
    698         let test_data = "some test data".as_bytes();
    699         let source = fixture_path("sample1.wav");
    700 
    701         let mut success = false;
    702         if let Ok(temp_dir) = tempdir() {
    703             let output = temp_dir_path(&temp_dir, "sample1-wav.wav");
    704 
    705             if let Ok(_size) = std::fs::copy(source, &output) {
    706                 let riff_io = RiffIO::new("wav");
    707 
    708                 if let Ok(()) = riff_io.save_cai_store(&output, test_data) {
    709                     if let Ok(source_data) = riff_io.read_cai_store(&output) {
    710                         // create replacement data of same size
    711                         let mut new_data = vec![0u8; source_data.len()];
    712                         new_data[..test_data.len()].copy_from_slice(test_data);
    713                         riff_io.patch_cai_store(&output, &new_data).unwrap();
    714 
    715                         let replaced = riff_io.read_cai_store(&output).unwrap();
    716 
    717                         assert_eq!(new_data, replaced);
    718 
    719                         success = true;
    720                     }
    721                 }
    722             }
    723         }
    724         assert!(success)
    725     }
    726 
    727     #[test]
    728     fn test_remove_c2pa() {
    729         let source = fixture_path("sample1.wav");
    730 
    731         let temp_dir = tempdir().unwrap();
    732         let output = temp_dir_path(&temp_dir, "sample1-wav.wav");
    733 
    734         std::fs::copy(source, &output).unwrap();
    735         let riff_io = RiffIO::new("wav");
    736 
    737         riff_io.remove_cai_store(&output).unwrap();
    738 
    739         // read back in asset, JumbfNotFound is expected since it was removed
    740         match riff_io.read_cai_store(&output) {
    741             Err(Error::JumbfNotFound) => (),
    742             _ => unreachable!(),
    743         }
    744     }
    745 
    746     #[test]
    747     fn test_read_xmp() {
    748         let source = fixture_path("test_xmp.webp");
    749         let mut reader = std::fs::File::open(source).unwrap();
    750 
    751         let riff_io = RiffIO::new("webp");
    752 
    753         let xmp = riff_io.read_xmp(&mut reader).unwrap();
    754         println!("XMP: {xmp}");
    755     }
    756 
    757     #[test]
    758     fn test_write_xmp() {
    759         let more_data = "some more test data";
    760         let source = fixture_path("test_xmp.webp");
    761 
    762         let mut success = false;
    763         if let Ok(temp_dir) = tempdir() {
    764             let output = temp_dir_path(&temp_dir, "test_xmp.webp");
    765 
    766             std::fs::copy(source, &output).unwrap();
    767 
    768             let riff_io = RiffIO::new("webp");
    769 
    770             if let Some(embed_handler) = riff_io.remote_ref_writer_ref() {
    771                 if let Ok(()) = embed_handler.embed_reference(
    772                     output.as_path(),
    773                     RemoteRefEmbedType::Xmp(more_data.to_string()),
    774                 ) {
    775                     let mut output_stream = std::fs::File::open(&output).unwrap();
    776 
    777                     // check the xmp
    778                     if let Some(xmp) = riff_io.read_xmp(&mut output_stream) {
    779                         println!("XMP: {xmp}");
    780 
    781                         if let Some(xmp_val) = extract_provenance(&xmp) {
    782                             if xmp_val == more_data {
    783                                 success = true;
    784                             }
    785                         }
    786                     }
    787                 }
    788             }
    789         }
    790         assert!(success)
    791     }
    792 
    793     #[test]
    794     fn test_insert_xmp() {
    795         let more_data = "some more test data";
    796         let source = fixture_path("test.webp");
    797 
    798         let mut success = false;
    799         if let Ok(temp_dir) = tempdir() {
    800             let output = temp_dir_path(&temp_dir, "test.webp");
    801 
    802             std::fs::copy(source, &output).unwrap();
    803 
    804             let riff_io = RiffIO::new("webp");
    805 
    806             if let Some(embed_handler) = riff_io.remote_ref_writer_ref() {
    807                 if let Ok(()) = embed_handler.embed_reference(
    808                     output.as_path(),
    809                     RemoteRefEmbedType::Xmp(more_data.to_string()),
    810                 ) {
    811                     let mut output_stream = std::fs::File::open(&output).unwrap();
    812 
    813                     // check the xmp
    814                     if let Some(xmp) = riff_io.read_xmp(&mut output_stream) {
    815                         println!("XMP: {xmp}");
    816 
    817                         if let Some(xmp_val) = extract_provenance(&xmp) {
    818                             if xmp_val == more_data {
    819                                 success = true;
    820                             }
    821                         }
    822                     }
    823                 }
    824             }
    825         }
    826         assert!(success)
    827     }
    828 
    829     #[test]
    830     fn test_insert_xmp_lossless() {
    831         let more_data = "some more test data";
    832         let source = fixture_path("test_lossless.webp");
    833 
    834         let mut success = false;
    835         if let Ok(temp_dir) = tempdir() {
    836             let output = temp_dir_path(&temp_dir, "test_lossless.webp");
    837 
    838             std::fs::copy(source, &output).unwrap();
    839 
    840             let riff_io = RiffIO::new("webp");
    841 
    842             if let Some(embed_handler) = riff_io.remote_ref_writer_ref() {
    843                 if let Ok(()) = embed_handler.embed_reference(
    844                     output.as_path(),
    845                     RemoteRefEmbedType::Xmp(more_data.to_string()),
    846                 ) {
    847                     let mut output_stream = std::fs::File::open(&output).unwrap();
    848 
    849                     // check the xmp
    850                     if let Some(xmp) = riff_io.read_xmp(&mut output_stream) {
    851                         println!("XMP: {xmp}");
    852 
    853                         if let Some(xmp_val) = extract_provenance(&xmp) {
    854                             if xmp_val == more_data {
    855                                 success = true;
    856                             }
    857                         }
    858                     }
    859                 }
    860             }
    861         }
    862         assert!(success)
    863     }
    864 }