1use 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
37lazy_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 for h in handlers {
58 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
68lazy_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 for h in handlers {
86 for supported_type in h.supported_types() {
88 if let Some(writer) = h.get_writer(supported_type) { 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#[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
111pub 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}
122pub 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
136pub 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
196pub(crate) fn supported_reader_mime_types() -> Vec<String> {
198 CAI_READERS.keys().map(String::to_owned).collect()
199}
200
201pub(crate) fn supported_builder_mime_types() -> Vec<String> {
203 CAI_WRITERS.keys().map(String::to_owned).collect()
204}
205
206#[cfg(feature = "file_io")]
207pub 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 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 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 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 asset_handler.save_cai_store(&asset_out_path, data)
251 }
252 _ => Err(Error::UnsupportedType),
253 }
254}
255
256#[allow(dead_code)] #[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")]
285pub 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#[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
362pub 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 for h in handlers {
396 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 for h in handlers {
420 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 for h in handlers {
440 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 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 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 }
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 }
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 }
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 }
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 }
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 }
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}