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

jumbf_io.rs (19303B)


      1 // Copyright 2022 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::{collections::HashMap, io::Cursor};
     15 #[cfg(feature = "file_io")]
     16 use std::{
     17     fs::{self, File},
     18     path::{Path, PathBuf},
     19 };
     20 
     21 use lazy_static::lazy_static;
     22 
     23 #[cfg(feature = "pdf")]
     24 use crate::asset_handlers::pdf_io::PdfIO;
     25 use crate::{
     26     asset_handlers::{
     27         bmff_io::BmffIO, c2pa_io::C2paIO, jpeg_io::JpegIO, mp3_io::Mp3IO, png_io::PngIO,
     28         riff_io::RiffIO, svg_io::SvgIO, tiff_io::TiffIO,
     29     },
     30     asset_io::{AssetIO, CAIRead, CAIReadWrite, CAIReader, CAIWriter, HashObjectPositions},
     31     error::{Error, Result},
     32 };
     33 
     34 // initialize asset handlers
     35 lazy_static! {
     36     static ref ASSET_HANDLERS: HashMap<String, Box<dyn AssetIO>> = {
     37         let handlers: Vec<Box<dyn AssetIO>> = vec![
     38             #[cfg(feature = "pdf")]
     39             Box::new(PdfIO::new("")),
     40             Box::new(BmffIO::new("")),
     41             Box::new(C2paIO::new("")),
     42             Box::new(JpegIO::new("")),
     43             Box::new(PngIO::new("")),
     44             Box::new(RiffIO::new("")),
     45             Box::new(SvgIO::new("")),
     46             Box::new(TiffIO::new("")),
     47             Box::new(Mp3IO::new("")),
     48         ];
     49 
     50         let mut handler_map = HashMap::new();
     51 
     52         // build handler map
     53         for h in handlers {
     54             // get the supported types add entry for each
     55             for supported_type in h.supported_types() {
     56                 handler_map.insert(supported_type.to_string(), h.get_handler(supported_type));
     57             }
     58         }
     59 
     60         handler_map
     61     };
     62 }
     63 
     64 // initialize streaming write handlers
     65 lazy_static! {
     66     static ref CAI_WRITERS: HashMap<String, Box<dyn CAIWriter>> = {
     67         let handlers: Vec<Box<dyn AssetIO>> = vec![
     68             Box::new(BmffIO::new("")),
     69             Box::new(C2paIO::new("")),
     70             Box::new(JpegIO::new("")),
     71             Box::new(PngIO::new("")),
     72             Box::new(RiffIO::new("")),
     73             Box::new(SvgIO::new("")),
     74             Box::new(TiffIO::new("")),
     75             Box::new(Mp3IO::new("")),
     76         ];
     77         let mut handler_map = HashMap::new();
     78 
     79         // build handler map
     80         for h in handlers {
     81             // get the supported types add entry for each
     82             for supported_type in h.supported_types() {
     83                 if let Some(writer) = h.get_writer(supported_type) { // get streaming writer if supported
     84                     handler_map.insert(supported_type.to_string(), writer);
     85                 }
     86             }
     87         }
     88 
     89         handler_map
     90     };
     91 }
     92 
     93 pub(crate) fn is_bmff_format(asset_type: &str) -> bool {
     94     let bmff_io = BmffIO::new("");
     95     bmff_io.supported_types().contains(&asset_type)
     96 }
     97 
     98 /// Return jumbf block from in memory asset
     99 #[allow(dead_code)]
    100 pub fn load_jumbf_from_memory(asset_type: &str, data: &[u8]) -> Result<Vec<u8>> {
    101     let mut buf_reader = Cursor::new(data);
    102 
    103     load_jumbf_from_stream(asset_type, &mut buf_reader)
    104 }
    105 
    106 /// Return jumbf block from stream asset
    107 pub fn load_jumbf_from_stream(asset_type: &str, input_stream: &mut dyn CAIRead) -> Result<Vec<u8>> {
    108     let cai_block = match get_cailoader_handler(asset_type) {
    109         Some(asset_handler) => asset_handler.read_cai(input_stream)?,
    110         None => return Err(Error::UnsupportedType),
    111     };
    112     if cai_block.is_empty() {
    113         return Err(Error::JumbfNotFound);
    114     }
    115     Ok(cai_block)
    116 }
    117 /// writes the jumbf data in store_bytes
    118 /// reads an asset of asset_type from reader, adds jumbf data and then writes to writer
    119 pub fn save_jumbf_to_stream(
    120     asset_type: &str,
    121     input_stream: &mut dyn CAIRead,
    122     output_stream: &mut dyn CAIReadWrite,
    123     store_bytes: &[u8],
    124 ) -> Result<()> {
    125     match get_caiwriter_handler(asset_type) {
    126         Some(asset_handler) => asset_handler.write_cai(input_stream, output_stream, store_bytes),
    127         None => Err(Error::UnsupportedType),
    128     }
    129 }
    130 
    131 /// writes the jumbf data in store_bytes into an asset in data and returns the newly created asset
    132 pub fn save_jumbf_to_memory(asset_type: &str, data: &[u8], store_bytes: &[u8]) -> Result<Vec<u8>> {
    133     let mut input_stream = Cursor::new(data);
    134     let output_vec: Vec<u8> = Vec::with_capacity(data.len() + store_bytes.len() + 1024);
    135     let mut output_stream = Cursor::new(output_vec);
    136 
    137     save_jumbf_to_stream(
    138         asset_type,
    139         &mut input_stream,
    140         &mut output_stream,
    141         store_bytes,
    142     )?;
    143     Ok(output_stream.into_inner())
    144 }
    145 
    146 #[cfg(feature = "file_io")]
    147 pub(crate) fn get_assetio_handler_from_path(asset_path: &Path) -> Option<&dyn AssetIO> {
    148     let ext = get_file_extension(asset_path)?;
    149 
    150     ASSET_HANDLERS.get(&ext).map(|h| h.as_ref())
    151 }
    152 
    153 pub(crate) fn get_assetio_handler(ext: &str) -> Option<&dyn AssetIO> {
    154     let ext = ext.to_lowercase();
    155 
    156     ASSET_HANDLERS.get(&ext).map(|h| h.as_ref())
    157 }
    158 
    159 pub(crate) fn get_cailoader_handler(asset_type: &str) -> Option<&dyn CAIReader> {
    160     let asset_type = asset_type.to_lowercase();
    161 
    162     ASSET_HANDLERS.get(&asset_type).map(|h| h.get_reader())
    163 }
    164 
    165 pub(crate) fn get_caiwriter_handler(asset_type: &str) -> Option<&dyn CAIWriter> {
    166     let asset_type = asset_type.to_lowercase();
    167 
    168     CAI_WRITERS.get(&asset_type).map(|h| h.as_ref())
    169 }
    170 
    171 #[cfg(feature = "file_io")]
    172 pub(crate) fn get_file_extension(path: &Path) -> Option<String> {
    173     let ext_osstr = path.extension()?;
    174 
    175     let ext = ext_osstr.to_str()?;
    176 
    177     Some(ext.to_lowercase())
    178 }
    179 
    180 #[cfg(feature = "file_io")]
    181 pub(crate) fn get_supported_file_extension(path: &Path) -> Option<String> {
    182     let ext = get_file_extension(path)?;
    183 
    184     if ASSET_HANDLERS.get(&ext).is_some() {
    185         Some(ext)
    186     } else {
    187         None
    188     }
    189 }
    190 
    191 #[cfg(feature = "file_io")]
    192 /// save_jumbf to a file
    193 /// in_path - path is source file
    194 /// out_path - path to the output file
    195 /// If no output file is given an new file will be created with "-c2pa" appending to file name e.g. "test.jpg" => "test-c2pa.jpg"
    196 /// If input == output then the input file will be overwritten.
    197 pub fn save_jumbf_to_file(data: &[u8], in_path: &Path, out_path: Option<&Path>) -> Result<()> {
    198     let ext = get_file_extension(in_path).ok_or(Error::UnsupportedType)?;
    199 
    200     // if no output path make a new file based off of source file name
    201     let asset_out_path: PathBuf = match out_path {
    202         Some(p) => p.to_owned(),
    203         None => {
    204             let filename_osstr = in_path.file_stem().ok_or(Error::UnsupportedType)?;
    205             let filename = filename_osstr.to_str().ok_or(Error::UnsupportedType)?;
    206 
    207             let out_name = format!("{filename}-c2pa.{ext}");
    208             in_path.to_owned().with_file_name(out_name)
    209         }
    210     };
    211 
    212     // clone output to be overwritten
    213     if in_path != asset_out_path {
    214         fs::copy(in_path, &asset_out_path).map_err(Error::IoError)?;
    215     }
    216 
    217     match get_assetio_handler(&ext) {
    218         Some(asset_handler) => {
    219             // patch if possible to save time and resources
    220             if let Some(patch_handler) = asset_handler.asset_patch_ref() {
    221                 if patch_handler.patch_cai_store(&asset_out_path, data).is_ok() {
    222                     return Ok(());
    223                 }
    224             }
    225 
    226             // couldn't patch so just save
    227             asset_handler.save_cai_store(&asset_out_path, data)
    228         }
    229         _ => Err(Error::UnsupportedType),
    230     }
    231 }
    232 
    233 /// Updates jumbf content in a file, this will directly patch the contents no other processing is done.
    234 /// The search for content to replace only occurs over the jumbf content.
    235 /// Note: it is recommended that the replace contents be <= length of the search content so that the length of the
    236 /// file does not change. If it does that could make the new file unreadable. This function is primarily useful for
    237 /// generating test data since depending on how the file is rewritten the hashing mechanism should detect any tampering of the data.
    238 ///
    239 /// out_path - path to file to be updated
    240 /// search_bytes - bytes to be replaced
    241 /// replace_bytes - replacement bytes
    242 /// returns the location where splice occurred
    243 #[cfg(test)] // this only used in unit tests
    244 #[cfg(feature = "file_io")]
    245 pub(crate) fn update_file_jumbf(
    246     out_path: &Path,
    247     search_bytes: &[u8],
    248     replace_bytes: &[u8],
    249 ) -> Result<usize> {
    250     use crate::utils::patch::patch_bytes;
    251 
    252     let mut jumbf = load_jumbf_from_file(out_path)?;
    253 
    254     let splice_point = patch_bytes(&mut jumbf, search_bytes, replace_bytes)?;
    255 
    256     save_jumbf_to_file(&jumbf, out_path, Some(out_path))?;
    257 
    258     Ok(splice_point)
    259 }
    260 
    261 #[cfg(feature = "file_io")]
    262 /// load the JUMBF block from an asset if available
    263 pub fn load_jumbf_from_file(in_path: &Path) -> Result<Vec<u8>> {
    264     let ext = get_file_extension(in_path).ok_or(Error::UnsupportedType)?;
    265 
    266     match get_cailoader_handler(&ext) {
    267         Some(asset_handler) => {
    268             let mut f = File::open(in_path)?;
    269             asset_handler.read_cai(&mut f)
    270         }
    271         _ => Err(Error::UnsupportedType),
    272     }
    273 }
    274 
    275 #[cfg(feature = "file_io")]
    276 pub(crate) fn object_locations(in_path: &Path) -> Result<Vec<HashObjectPositions>> {
    277     let ext = get_file_extension(in_path).ok_or(Error::UnsupportedType)?;
    278 
    279     match get_assetio_handler(&ext) {
    280         Some(asset_handler) => asset_handler.get_object_locations(in_path),
    281         _ => Err(Error::UnsupportedType),
    282     }
    283 }
    284 
    285 pub(crate) fn object_locations_from_stream(
    286     format: &str,
    287     stream: &mut dyn CAIRead,
    288 ) -> Result<Vec<HashObjectPositions>> {
    289     match get_caiwriter_handler(format) {
    290         Some(handler) => handler.get_object_locations_from_stream(stream),
    291         _ => Err(Error::UnsupportedType),
    292     }
    293 }
    294 
    295 #[cfg(feature = "file_io")]
    296 /// removes the C2PA JUMBF from an asset
    297 /// Note: Use with caution since this deletes C2PA data
    298 /// It is useful when creating remote manifests from embedded manifests
    299 ///
    300 /// path - path to file to be updated
    301 /// returns Unsupported type or errors from remove_cai_store
    302 pub fn remove_jumbf_from_file(path: &Path) -> Result<()> {
    303     let ext = get_file_extension(path).ok_or(Error::UnsupportedType)?;
    304     match get_assetio_handler(&ext) {
    305         Some(asset_handler) => asset_handler.remove_cai_store(path),
    306         _ => Err(Error::UnsupportedType),
    307     }
    308 }
    309 
    310 /// returns a list of supported file extensions and mime types
    311 pub fn get_supported_types() -> Vec<String> {
    312     ASSET_HANDLERS.keys().map(|k| k.to_owned()).collect()
    313 }
    314 
    315 #[cfg(test)]
    316 pub mod tests {
    317     #![allow(clippy::panic)]
    318     #![allow(clippy::unwrap_used)]
    319 
    320     use std::io::Seek;
    321 
    322     use super::*;
    323     use crate::{
    324         asset_io::RemoteRefEmbedType,
    325         utils::test::{create_test_store, temp_signer},
    326     };
    327 
    328     #[test]
    329     fn test_get_assetio() {
    330         let handlers: Vec<Box<dyn AssetIO>> = vec![
    331             Box::new(C2paIO::new("")),
    332             Box::new(BmffIO::new("")),
    333             Box::new(JpegIO::new("")),
    334             Box::new(PngIO::new("")),
    335             Box::new(RiffIO::new("")),
    336             Box::new(TiffIO::new("")),
    337             Box::new(SvgIO::new("")),
    338             Box::new(Mp3IO::new("")),
    339         ];
    340 
    341         // build handler map
    342         for h in handlers {
    343             // get the supported types add entry for each
    344             for supported_type in h.supported_types() {
    345                 assert!(get_assetio_handler(supported_type).is_some());
    346             }
    347         }
    348     }
    349 
    350     #[test]
    351     fn test_get_reader() {
    352         let handlers: Vec<Box<dyn AssetIO>> = vec![
    353             Box::new(C2paIO::new("")),
    354             Box::new(BmffIO::new("")),
    355             Box::new(JpegIO::new("")),
    356             #[cfg(feature = "pdf")]
    357             Box::new(PdfIO::new("")),
    358             Box::new(PngIO::new("")),
    359             Box::new(RiffIO::new("")),
    360             Box::new(TiffIO::new("")),
    361             Box::new(SvgIO::new("")),
    362             Box::new(Mp3IO::new("")),
    363         ];
    364 
    365         // build handler map
    366         for h in handlers {
    367             // get the supported types add entry for each
    368             for supported_type in h.supported_types() {
    369                 assert!(get_cailoader_handler(supported_type).is_some());
    370             }
    371         }
    372     }
    373 
    374     #[test]
    375     fn test_get_writer() {
    376         let handlers: Vec<Box<dyn AssetIO>> = vec![
    377             Box::new(JpegIO::new("")),
    378             Box::new(PngIO::new("")),
    379             Box::new(Mp3IO::new("")),
    380             Box::new(SvgIO::new("")),
    381             Box::new(RiffIO::new("")),
    382         ];
    383 
    384         // build handler map
    385         for h in handlers {
    386             // get the supported types add entry for each
    387             for supported_type in h.supported_types() {
    388                 assert!(get_caiwriter_handler(supported_type).is_some());
    389             }
    390         }
    391     }
    392 
    393     #[test]
    394     fn test_get_supported_list() {
    395         let supported = get_supported_types();
    396 
    397         let pdf_supported = supported.iter().any(|s| s == "pdf");
    398         assert_eq!(pdf_supported, cfg!(feature = "pdf"));
    399 
    400         assert!(supported.iter().any(|s| s == "jpg"));
    401         assert!(supported.iter().any(|s| s == "jpeg"));
    402         assert!(supported.iter().any(|s| s == "png"));
    403         assert!(supported.iter().any(|s| s == "mov"));
    404         assert!(supported.iter().any(|s| s == "mp4"));
    405         assert!(supported.iter().any(|s| s == "m4a"));
    406         assert!(supported.iter().any(|s| s == "avi"));
    407         assert!(supported.iter().any(|s| s == "webp"));
    408         assert!(supported.iter().any(|s| s == "wav"));
    409         assert!(supported.iter().any(|s| s == "tif"));
    410         assert!(supported.iter().any(|s| s == "tiff"));
    411         assert!(supported.iter().any(|s| s == "dng"));
    412         assert!(supported.iter().any(|s| s == "svg"));
    413         assert!(supported.iter().any(|s| s == "mp3"));
    414     }
    415 
    416     fn test_jumbf(asset_type: &str, reader: &mut dyn CAIRead) {
    417         let mut writer = Cursor::new(Vec::new());
    418         let store = create_test_store().unwrap();
    419         let signer = temp_signer();
    420         let jumbf = store.to_jumbf(&*signer).unwrap();
    421         save_jumbf_to_stream(asset_type, reader, &mut writer, &jumbf).unwrap();
    422         writer.set_position(0);
    423         let jumbf2 = load_jumbf_from_stream(asset_type, &mut writer).unwrap();
    424         assert_eq!(jumbf, jumbf2);
    425 
    426         // test removing cai store
    427         writer.set_position(0);
    428         let handler = get_caiwriter_handler(asset_type).unwrap();
    429         let mut removed = Cursor::new(Vec::new());
    430         handler
    431             .remove_cai_store_from_stream(&mut writer, &mut removed)
    432             .unwrap();
    433         removed.set_position(0);
    434         let result = load_jumbf_from_stream(asset_type, &mut removed);
    435         if (asset_type != "wav")
    436             && (asset_type != "avi" && asset_type != "mp3" && asset_type != "webp")
    437         {
    438             assert!(matches!(&result.err().unwrap(), Error::JumbfNotFound));
    439         }
    440         //assert!(matches!(result.err().unwrap(), Error::JumbfNotFound));
    441     }
    442 
    443     fn test_remote_ref(asset_type: &str, reader: &mut dyn CAIRead) {
    444         const REMOTE_URL: &str = "https://example.com/remote_manifest";
    445         let asset_handler = get_assetio_handler(asset_type).unwrap();
    446         let remote_ref_writer = asset_handler.remote_ref_writer_ref().unwrap();
    447         let mut writer = Cursor::new(Vec::new());
    448         let embed_ref = RemoteRefEmbedType::Xmp(REMOTE_URL.to_string());
    449         remote_ref_writer
    450             .embed_reference_to_stream(reader, &mut writer, embed_ref)
    451             .unwrap();
    452         writer.set_position(0);
    453         let xmp = asset_handler.get_reader().read_xmp(&mut writer).unwrap();
    454         let loaded = crate::utils::xmp_inmemory_utils::extract_provenance(&xmp).unwrap();
    455         assert_eq!(loaded, REMOTE_URL.to_string());
    456     }
    457 
    458     #[test]
    459     fn test_streams_jpeg() {
    460         let mut reader = std::fs::File::open("tests/fixtures/IMG_0003.jpg").unwrap();
    461         test_jumbf("jpeg", &mut reader);
    462         reader.rewind().unwrap();
    463         test_remote_ref("jpeg", &mut reader);
    464     }
    465 
    466     #[test]
    467     fn test_streams_png() {
    468         let mut reader = std::fs::File::open("tests/fixtures/sample1.png").unwrap();
    469         test_jumbf("png", &mut reader);
    470         reader.rewind().unwrap();
    471         test_remote_ref("png", &mut reader);
    472     }
    473 
    474     #[test]
    475     fn test_streams_webp() {
    476         let mut reader = std::fs::File::open("tests/fixtures/sample1.webp").unwrap();
    477         test_jumbf("webp", &mut reader);
    478         reader.rewind().unwrap();
    479         test_remote_ref("webp", &mut reader);
    480     }
    481 
    482     #[test]
    483     fn test_streams_wav() {
    484         let mut reader = std::fs::File::open("tests/fixtures/sample1.wav").unwrap();
    485         test_jumbf("wav", &mut reader);
    486         reader.rewind().unwrap();
    487         test_remote_ref("wav", &mut reader);
    488     }
    489 
    490     #[test]
    491     fn test_streams_avi() {
    492         let mut reader = std::fs::File::open("tests/fixtures/test.avi").unwrap();
    493         test_jumbf("avi", &mut reader);
    494         //reader.rewind().unwrap();
    495         //test_remote_ref("avi", &mut reader); // not working
    496     }
    497 
    498     #[test]
    499     fn test_streams_tiff() {
    500         let mut reader = std::fs::File::open("tests/fixtures/TUSCANY.TIF").unwrap();
    501         test_jumbf("tiff", &mut reader);
    502         reader.rewind().unwrap();
    503         test_remote_ref("tiff", &mut reader);
    504     }
    505 
    506     #[test]
    507     fn test_streams_svg() {
    508         let mut reader = std::fs::File::open("tests/fixtures/sample1.svg").unwrap();
    509         test_jumbf("svg", &mut reader);
    510         //reader.rewind().unwrap();
    511         //test_remote_ref("svg", &mut reader); // svg doesn't support remote refs
    512     }
    513 
    514     #[test]
    515     fn test_streams_mp3() {
    516         let mut reader = std::fs::File::open("tests/fixtures/sample1.mp3").unwrap();
    517         test_jumbf("mp3", &mut reader);
    518         // mp3 doesn't support remote refs
    519         //reader.rewind().unwrap();
    520         //test_remote_ref("mp3", &mut reader); // not working
    521     }
    522 
    523     #[test]
    524     fn test_streams_avif() {
    525         let mut reader = std::fs::File::open("tests/fixtures/sample1.avif").unwrap();
    526         test_jumbf("avif", &mut reader);
    527         //reader.rewind().unwrap();
    528         //test_remote_ref("avif", &mut reader);  // not working
    529     }
    530 
    531     #[test]
    532     fn test_streams_heic() {
    533         let mut reader = std::fs::File::open("tests/fixtures/sample1.heic").unwrap();
    534         test_jumbf("heic", &mut reader);
    535     }
    536 
    537     #[test]
    538     fn test_streams_heif() {
    539         let mut reader = std::fs::File::open("tests/fixtures/sample1.heif").unwrap();
    540         test_jumbf("heif", &mut reader);
    541         //reader.rewind().unwrap();
    542         //test_remote_ref("heif", &mut reader);   // not working
    543     }
    544 
    545     #[test]
    546     fn test_streams_mp4() {
    547         let mut reader = std::fs::File::open("tests/fixtures/video1.mp4").unwrap();
    548         test_jumbf("mp4", &mut reader);
    549         reader.rewind().unwrap();
    550         test_remote_ref("mp4", &mut reader);
    551     }
    552 
    553     #[test]
    554     fn test_streams_c2pa() {
    555         let mut reader = std::fs::File::open("tests/fixtures/cloud_manifest.c2pa").unwrap();
    556         test_jumbf("c2pa", &mut reader);
    557     }
    558 }