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 }