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 }