c2pa/
jumbf_io.rs

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