png_io.rs (35227B)
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::{ 15 fs::File, 16 io::{Cursor, Read, Seek, SeekFrom}, 17 path::Path, 18 }; 19 20 use byteorder::{BigEndian, ReadBytesExt}; 21 use conv::ValueFrom; 22 use png_pong::chunk::InternationalText; 23 use serde_bytes::ByteBuf; 24 use tempfile::Builder; 25 26 use crate::{ 27 assertions::{BoxMap, C2PA_BOXHASH}, 28 asset_io::{ 29 rename_or_copy, AssetBoxHash, AssetIO, CAIRead, CAIReadWrite, CAIReader, CAIWriter, 30 ComposedManifestRef, HashBlockObjectType, HashObjectPositions, RemoteRefEmbed, 31 RemoteRefEmbedType, 32 }, 33 error::{Error, Result}, 34 utils::xmp_inmemory_utils::{add_provenance, MIN_XMP}, 35 }; 36 37 const PNG_ID: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10]; 38 const CAI_CHUNK: [u8; 4] = *b"caBX"; 39 const IMG_HDR: [u8; 4] = *b"IHDR"; 40 const ITXT_CHUNK: [u8; 4] = *b"iTXt"; 41 const XMP_KEY: &str = "XML:com.adobe.xmp"; 42 const PNG_END: [u8; 4] = *b"IEND"; 43 const PNG_HDR_LEN: u64 = 12; 44 45 static SUPPORTED_TYPES: [&str; 2] = ["png", "image/png"]; 46 47 #[derive(Clone, Debug)] 48 struct PngChunkPos { 49 pub start: u64, 50 pub length: u32, 51 pub name: [u8; 4], 52 #[allow(dead_code)] 53 pub name_str: String, 54 } 55 56 impl PngChunkPos { 57 pub const fn end(&self) -> u64 { 58 self.start + self.length as u64 + PNG_HDR_LEN 59 } 60 } 61 62 fn get_png_chunk_positions<R: Read + Seek + ?Sized>(f: &mut R) -> Result<Vec<PngChunkPos>> { 63 let current_len = f.seek(SeekFrom::End(0))?; 64 let mut chunk_positions: Vec<PngChunkPos> = Vec::new(); 65 66 // move to beginning of file 67 f.rewind()?; 68 69 let mut buf4 = [0; 4]; 70 let mut hdr = [0; 8]; 71 72 // check PNG signature 73 f.read_exact(&mut hdr) 74 .map_err(|_err| Error::InvalidAsset("PNG invalid".to_string()))?; 75 if hdr != PNG_ID { 76 return Err(Error::InvalidAsset("PNG invalid".to_string())); 77 } 78 79 loop { 80 let current_pos = f.stream_position()?; 81 82 // read the chunk length 83 let length = f 84 .read_u32::<BigEndian>() 85 .map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?; 86 87 // read the chunk type 88 f.read_exact(&mut buf4) 89 .map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?; 90 let name = buf4; 91 92 // seek past data 93 f.seek(SeekFrom::Current(length as i64)) 94 .map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?; 95 96 // read crc 97 f.read_exact(&mut buf4) 98 .map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?; 99 100 let chunk_name = String::from_utf8(name.to_vec()) 101 .map_err(|_err| Error::InvalidAsset("PNG bad chunk name".to_string()))?; 102 103 let pcp = PngChunkPos { 104 start: current_pos, 105 length, 106 name, 107 name_str: chunk_name, 108 }; 109 110 // add to list 111 chunk_positions.push(pcp); 112 113 // should we break the loop 114 if name == PNG_END || f.stream_position()? > current_len { 115 break; 116 } 117 } 118 119 Ok(chunk_positions) 120 } 121 122 fn get_cai_data<R: Read + Seek + ?Sized>(f: &mut R) -> Result<Vec<u8>> { 123 let ps = get_png_chunk_positions(f)?; 124 125 if ps 126 .clone() 127 .into_iter() 128 .filter(|pcp| pcp.name == CAI_CHUNK) 129 .count() 130 > 1 131 { 132 return Err(Error::TooManyManifestStores); 133 } 134 135 let pcp = ps 136 .into_iter() 137 .find(|pcp| pcp.name == CAI_CHUNK) 138 .ok_or(Error::JumbfNotFound)?; 139 140 let length: usize = pcp.length as usize; 141 142 f.seek(SeekFrom::Start(pcp.start + 8))?; // skip ahead from chunk start + length(4) + name(4) 143 144 let mut data: Vec<u8> = vec![0; length]; 145 f.read_exact(&mut data[..]) 146 .map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?; 147 148 Ok(data) 149 } 150 151 fn add_required_chunks_to_stream( 152 input_stream: &mut dyn CAIRead, 153 output_stream: &mut dyn CAIReadWrite, 154 ) -> Result<()> { 155 let mut buf: Vec<u8> = Vec::new(); 156 input_stream.rewind()?; 157 input_stream.read_to_end(&mut buf).map_err(Error::IoError)?; 158 input_stream.rewind()?; 159 160 let img_out = img_parts::DynImage::from_bytes(buf.into()) 161 .map_err(|_err| Error::InvalidAsset("Could not parse input PNG".to_owned()))?; 162 163 if let Some(img_parts::DynImage::Png(png)) = img_out { 164 if png.chunk_by_type(CAI_CHUNK).is_none() { 165 let no_bytes: Vec<u8> = Vec::new(); 166 let aio = PngIO {}; 167 aio.write_cai(input_stream, output_stream, &no_bytes)?; 168 } else { 169 // just clone 170 input_stream.rewind()?; 171 output_stream.rewind()?; 172 std::io::copy(input_stream, output_stream)?; 173 } 174 } else { 175 return Err(Error::UnsupportedType); 176 } 177 178 Ok(()) 179 } 180 181 fn read_string(asset_reader: &mut dyn CAIRead, max_read: u32) -> Result<String> { 182 let mut bytes_read: u32 = 0; 183 let mut s: Vec<u8> = Vec::with_capacity(80); 184 185 loop { 186 let c = asset_reader.read_u8()?; 187 if c == 0 { 188 break; 189 } 190 191 s.push(c); 192 193 bytes_read += 1; 194 195 if bytes_read == max_read { 196 break; 197 } 198 } 199 200 Ok(String::from_utf8_lossy(&s).to_string()) 201 } 202 pub struct PngIO {} 203 204 impl CAIReader for PngIO { 205 fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result<Vec<u8>> { 206 let cai_data = get_cai_data(asset_reader)?; 207 Ok(cai_data) 208 } 209 210 // Get XMP block 211 fn read_xmp(&self, asset_reader: &mut dyn CAIRead) -> Option<String> { 212 let ps = get_png_chunk_positions(asset_reader).ok()?; 213 let mut xmp_str: Option<String> = None; 214 215 ps.into_iter().find(|pcp| { 216 if pcp.name == ITXT_CHUNK { 217 // seek to start of chunk 218 if asset_reader.seek(SeekFrom::Start(pcp.start + 8)).is_err() { 219 // move +8 to get past header 220 return false; 221 } 222 223 // parse the iTxt block 224 if let Ok(key) = read_string(asset_reader, pcp.length) { 225 if key.is_empty() || key.len() > 79 { 226 return false; 227 } 228 229 // is this an XMP key 230 if key != XMP_KEY { 231 return false; 232 } 233 234 // parse rest of iTxt to get the xmp value 235 let compressed = match asset_reader.read_u8() { 236 Ok(c) => c != 0, 237 Err(_) => return false, 238 }; 239 240 let _compression_method = match asset_reader.read_u8() { 241 Ok(c) => c != 0, 242 Err(_) => return false, 243 }; 244 245 let _langtag = match read_string(asset_reader, pcp.length) { 246 Ok(s) => s, 247 Err(_) => return false, 248 }; 249 250 let _transkey = match read_string(asset_reader, pcp.length) { 251 Ok(s) => s, 252 Err(_) => return false, 253 }; 254 255 // read iTxt data 256 let mut data = vec![ 257 0u8; 258 pcp.length as usize 259 - (key.len() + _langtag.len() + _transkey.len() + 5) 260 ]; // data len - size of key - size of land - size of transkey - 3 "0" string terminators - compressed u8 - compression method u8 261 if asset_reader.read_exact(&mut data).is_err() { 262 return false; 263 } 264 265 // convert to string, decompress if needed 266 let val = if compressed { 267 /* should not be needed for current XMP 268 use flate2::read::GzDecoder; 269 270 let cursor = Cursor::new(data); 271 272 let mut d = GzDecoder::new(cursor); 273 let mut s = String::new(); 274 if d.read_to_string(&mut s).is_err() { 275 return false; 276 } 277 s 278 */ 279 return false; 280 } else { 281 String::from_utf8_lossy(&data).to_string() 282 }; 283 284 xmp_str = Some(val); 285 286 true 287 } else { 288 false 289 } 290 } else { 291 false 292 } 293 }); 294 295 xmp_str 296 } 297 } 298 299 impl CAIWriter for PngIO { 300 fn write_cai( 301 &self, 302 input_stream: &mut dyn CAIRead, 303 output_stream: &mut dyn CAIReadWrite, 304 store_bytes: &[u8], 305 ) -> Result<()> { 306 let mut cai_data = Vec::new(); 307 let mut cai_encoder = png_pong::Encoder::new(&mut cai_data).into_chunk_enc(); 308 309 let mut png_buf = Vec::new(); 310 input_stream.rewind()?; 311 input_stream 312 .read_to_end(&mut png_buf) 313 .map_err(Error::IoError)?; 314 315 let mut cursor = Cursor::new(png_buf); 316 let mut ps = get_png_chunk_positions(&mut cursor)?; 317 318 // get back buffer 319 png_buf = cursor.into_inner(); 320 321 // create CAI store chunk 322 let cai_unknown = png_pong::chunk::Unknown { 323 name: CAI_CHUNK, 324 data: store_bytes.to_vec(), 325 }; 326 327 let mut cai_chunk = png_pong::chunk::Chunk::Unknown(cai_unknown); 328 cai_encoder 329 .encode(&mut cai_chunk) 330 .map_err(|_| Error::EmbeddingError)?; 331 332 /* splice in new chunk. Each PNG chunk has the following format: 333 chunk data length (4 bytes big endian) 334 chunk identifier (4 byte character sequence) 335 chunk data (0 - n bytes of chunk data) 336 chunk crc (4 bytes in crc in format defined in PNG spec) 337 */ 338 339 // erase existing cai data 340 let empty_buf = Vec::new(); 341 let mut iter = ps.into_iter(); 342 if let Some(existing_cai_data) = iter.find(|png_cp| png_cp.name == CAI_CHUNK) { 343 // replace existing CAI data 344 let cai_start = usize::value_from(existing_cai_data.start) 345 .map_err(|_err| Error::InvalidAsset("value out of range".to_owned()))?; // get beginning of chunk which starts 4 bytes before label 346 347 let cai_end = usize::value_from(existing_cai_data.end()) 348 .map_err(|_err| Error::InvalidAsset("value out of range".to_owned()))?; 349 350 png_buf.splice(cai_start..cai_end, empty_buf.iter().cloned()); 351 }; 352 353 // update positions and reset png_buf 354 cursor = Cursor::new(png_buf); 355 ps = get_png_chunk_positions(&mut cursor)?; 356 iter = ps.into_iter(); 357 png_buf = cursor.into_inner(); 358 359 // add new cai data after the image header chunk 360 if let Some(img_hdr) = iter.find(|png_cp| png_cp.name == IMG_HDR) { 361 let img_hdr_end = usize::value_from(img_hdr.end()) 362 .map_err(|_err| Error::InvalidAsset("value out of range".to_owned()))?; 363 364 png_buf.splice(img_hdr_end..img_hdr_end, cai_data.iter().cloned()); 365 } else { 366 return Err(Error::EmbeddingError); 367 } 368 369 output_stream.rewind()?; 370 output_stream.write_all(&png_buf)?; 371 372 Ok(()) 373 } 374 375 fn get_object_locations_from_stream( 376 &self, 377 input_stream: &mut dyn CAIRead, 378 ) -> Result<Vec<HashObjectPositions>> { 379 let mut positions: Vec<HashObjectPositions> = Vec::new(); 380 381 // Ensure the stream has the required chunks so we can generate the required offsets. 382 let output: Vec<u8> = Vec::new(); 383 let mut output_stream = Cursor::new(output); 384 385 add_required_chunks_to_stream(input_stream, &mut output_stream)?; 386 387 let mut png_buf: Vec<u8> = Vec::new(); 388 output_stream.rewind()?; 389 output_stream 390 .read_to_end(&mut png_buf) 391 .map_err(Error::IoError)?; 392 output_stream.rewind()?; 393 394 let mut cursor = Cursor::new(png_buf); 395 let ps = get_png_chunk_positions(&mut cursor)?; 396 397 // get back buffer 398 png_buf = cursor.into_inner(); 399 400 let pcp = ps 401 .into_iter() 402 .find(|pcp| pcp.name == CAI_CHUNK) 403 .ok_or(Error::JumbfNotFound)?; 404 405 positions.push(HashObjectPositions { 406 offset: pcp.start as usize, 407 length: pcp.length as usize + PNG_HDR_LEN as usize, 408 htype: HashBlockObjectType::Cai, 409 }); 410 411 // add hash of chunks before cai 412 positions.push(HashObjectPositions { 413 offset: 0, 414 length: pcp.start as usize, 415 htype: HashBlockObjectType::Other, 416 }); 417 418 // add position from cai to end 419 let end = pcp.end() as usize; 420 let file_end = png_buf.len(); 421 positions.push(HashObjectPositions { 422 offset: end, // len of cai 423 length: file_end - end, 424 htype: HashBlockObjectType::Other, 425 }); 426 427 Ok(positions) 428 } 429 430 fn remove_cai_store_from_stream( 431 &self, 432 input_stream: &mut dyn CAIRead, 433 output_stream: &mut dyn CAIReadWrite, 434 ) -> Result<()> { 435 // get png byte 436 let ps = get_png_chunk_positions(input_stream)?; 437 438 // get image bytes 439 input_stream.rewind()?; 440 let mut png_buf: Vec<u8> = Vec::new(); 441 input_stream.read_to_end(&mut png_buf)?; 442 443 /* splice in new chunk. Each PNG chunk has the following format: 444 chunk data length (4 bytes big endian) 445 chunk identifier (4 byte character sequence) 446 chunk data (0 - n bytes of chunk data) 447 chunk crc (4 bytes in crc in format defined in PNG spec) 448 */ 449 450 // erase existing 451 let empty_buf = Vec::new(); 452 let mut iter = ps.into_iter(); 453 if let Some(existing_cai) = iter.find(|pcp| pcp.name == CAI_CHUNK) { 454 // replace existing CAI 455 let start = usize::value_from(existing_cai.start) 456 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label 457 458 let end = usize::value_from(existing_cai.end()) 459 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; 460 461 png_buf.splice(start..end, empty_buf.iter().cloned()); 462 } 463 464 // save png data 465 output_stream.write_all(&png_buf)?; 466 467 Ok(()) 468 } 469 } 470 471 impl AssetIO for PngIO { 472 fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> { 473 let mut f = File::open(asset_path)?; 474 self.read_cai(&mut f) 475 } 476 477 fn save_cai_store(&self, asset_path: &Path, store_bytes: &[u8]) -> Result<()> { 478 let mut stream = std::fs::OpenOptions::new() 479 .read(true) 480 .open(asset_path) 481 .map_err(Error::IoError)?; 482 483 let mut temp_file = Builder::new() 484 .prefix("c2pa_temp") 485 .rand_bytes(5) 486 .tempfile()?; 487 488 self.write_cai(&mut stream, &mut temp_file, store_bytes)?; 489 490 // copy temp file to asset 491 rename_or_copy(temp_file, asset_path) 492 } 493 494 fn get_object_locations( 495 &self, 496 asset_path: &std::path::Path, 497 ) -> Result<Vec<HashObjectPositions>> { 498 let mut file = std::fs::OpenOptions::new() 499 .read(true) 500 .write(true) 501 .open(asset_path) 502 .map_err(Error::IoError)?; 503 504 self.get_object_locations_from_stream(&mut file) 505 } 506 507 fn remove_cai_store(&self, asset_path: &Path) -> Result<()> { 508 // get png byte 509 let mut png_buf = std::fs::read(asset_path).map_err(|_err| Error::EmbeddingError)?; 510 511 let mut cursor = Cursor::new(png_buf); 512 let ps = get_png_chunk_positions(&mut cursor)?; 513 514 // get back buffer 515 png_buf = cursor.into_inner(); 516 517 /* splice in new chunk. Each PNG chunk has the following format: 518 chunk data length (4 bytes big endian) 519 chunk identifier (4 byte character sequence) 520 chunk data (0 - n bytes of chunk data) 521 chunk crc (4 bytes in crc in format defined in PNG spec) 522 */ 523 524 // erase existing 525 let empty_buf = Vec::new(); 526 let mut iter = ps.into_iter(); 527 if let Some(existing_cai) = iter.find(|pcp| pcp.name == CAI_CHUNK) { 528 // replace existing CAI 529 let start = usize::value_from(existing_cai.start) 530 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label 531 532 let end = usize::value_from(existing_cai.end()) 533 .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; 534 535 png_buf.splice(start..end, empty_buf.iter().cloned()); 536 } 537 538 // save png data 539 std::fs::write(asset_path, png_buf)?; 540 541 Ok(()) 542 } 543 544 fn new(_asset_type: &str) -> Self 545 where 546 Self: Sized, 547 { 548 PngIO {} 549 } 550 551 fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> { 552 Box::new(PngIO::new(asset_type)) 553 } 554 555 fn get_reader(&self) -> &dyn CAIReader { 556 self 557 } 558 559 fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> { 560 Some(Box::new(PngIO::new(asset_type))) 561 } 562 563 fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> { 564 Some(self) 565 } 566 567 fn asset_box_hash_ref(&self) -> Option<&dyn AssetBoxHash> { 568 Some(self) 569 } 570 571 fn composed_data_ref(&self) -> Option<&dyn ComposedManifestRef> { 572 Some(self) 573 } 574 575 fn supported_types(&self) -> &[&str] { 576 &SUPPORTED_TYPES 577 } 578 } 579 580 fn get_xmp_insertion_point(asset_reader: &mut dyn CAIRead) -> Option<(u64, u32)> { 581 let ps = get_png_chunk_positions(asset_reader).ok()?; 582 583 let xmp_box = ps.iter().find(|pcp| { 584 if pcp.name == ITXT_CHUNK { 585 // seek to start of chunk 586 if asset_reader.seek(SeekFrom::Start(pcp.start + 8)).is_err() { 587 // move +8 to get past header 588 return false; 589 } 590 591 // parse the iTxt block 592 if let Ok(key) = read_string(asset_reader, pcp.length) { 593 if key.is_empty() || key.len() > 79 { 594 return false; 595 } 596 597 // is this an XMP key 598 if key == XMP_KEY { 599 return true; 600 } 601 } 602 false 603 } else { 604 false 605 } 606 }); 607 608 if let Some(xmp) = xmp_box { 609 // overwrite existing box 610 Some((xmp.start, xmp.length + PNG_HDR_LEN as u32)) 611 } else { 612 // insert after IHDR 613 ps.iter() 614 .find(|png_cp| png_cp.name == IMG_HDR) 615 .map(|img_hdr| (img_hdr.end(), 0)) 616 } 617 } 618 impl RemoteRefEmbed for PngIO { 619 #[allow(unused_variables)] 620 fn embed_reference(&self, asset_path: &Path, embed_ref: RemoteRefEmbedType) -> Result<()> { 621 match embed_ref { 622 crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => { 623 let output_buf = Vec::new(); 624 let mut output_stream = Cursor::new(output_buf); 625 626 // do here so source file is closed after update 627 { 628 let mut source_stream = std::fs::File::open(asset_path)?; 629 self.embed_reference_to_stream( 630 &mut source_stream, 631 &mut output_stream, 632 RemoteRefEmbedType::Xmp(manifest_uri), 633 )?; 634 } 635 636 std::fs::write(asset_path, output_stream.into_inner())?; 637 638 Ok(()) 639 } 640 crate::asset_io::RemoteRefEmbedType::StegoS(_) => Err(Error::UnsupportedType), 641 crate::asset_io::RemoteRefEmbedType::StegoB(_) => Err(Error::UnsupportedType), 642 crate::asset_io::RemoteRefEmbedType::Watermark(_) => Err(Error::UnsupportedType), 643 } 644 } 645 646 fn embed_reference_to_stream( 647 &self, 648 source_stream: &mut dyn CAIRead, 649 output_stream: &mut dyn CAIReadWrite, 650 embed_ref: RemoteRefEmbedType, 651 ) -> Result<()> { 652 match embed_ref { 653 crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => { 654 source_stream.rewind()?; 655 656 let xmp = match self.read_xmp(source_stream) { 657 Some(s) => s, 658 None => format!("http://ns.adobe.com/xap/1.0/\0 {}", MIN_XMP), 659 }; 660 661 // update XMP 662 let updated_xmp = add_provenance(&xmp, &manifest_uri)?; 663 664 // make XMP chunk 665 let mut xmp_data = Vec::new(); 666 let mut xmp_encoder = png_pong::Encoder::new(&mut xmp_data).into_chunk_enc(); 667 668 let mut xmp_chunk = png_pong::chunk::Chunk::InternationalText(InternationalText { 669 key: XMP_KEY.to_string(), 670 langtag: "".to_string(), 671 transkey: "".to_string(), 672 val: updated_xmp, 673 compressed: false, 674 }); 675 xmp_encoder 676 .encode(&mut xmp_chunk) 677 .map_err(|_| Error::EmbeddingError)?; 678 679 // patch output stream 680 let mut png_buf = Vec::new(); 681 source_stream.rewind()?; 682 source_stream 683 .read_to_end(&mut png_buf) 684 .map_err(Error::IoError)?; 685 686 if let Some((start, xmp_len)) = get_xmp_insertion_point(source_stream) { 687 let mut png_buf = Vec::new(); 688 source_stream.rewind()?; 689 source_stream 690 .read_to_end(&mut png_buf) 691 .map_err(Error::IoError)?; 692 693 // replace existing XMP 694 let xmp_start = usize::value_from(start) 695 .map_err(|_err| Error::InvalidAsset("value out of range".to_owned()))?; // get beginning of chunk which starts 4 bytes before label 696 697 let xmp_end = usize::value_from(start + xmp_len as u64) 698 .map_err(|_err| Error::InvalidAsset("value out of range".to_owned()))?; 699 700 png_buf.splice(xmp_start..xmp_end, xmp_data.iter().cloned()); 701 702 output_stream.rewind()?; 703 output_stream.write_all(&png_buf)?; 704 705 Ok(()) 706 } else { 707 Err(Error::EmbeddingError) 708 } 709 } 710 crate::asset_io::RemoteRefEmbedType::StegoS(_) => Err(Error::UnsupportedType), 711 crate::asset_io::RemoteRefEmbedType::StegoB(_) => Err(Error::UnsupportedType), 712 crate::asset_io::RemoteRefEmbedType::Watermark(_) => Err(Error::UnsupportedType), 713 } 714 } 715 } 716 717 impl AssetBoxHash for PngIO { 718 fn get_box_map(&self, input_stream: &mut dyn CAIRead) -> Result<Vec<BoxMap>> { 719 input_stream.rewind()?; 720 721 let ps = get_png_chunk_positions(input_stream)?; 722 723 let mut box_maps = Vec::new(); 724 725 // add PNGh header 726 let pngh_bm = BoxMap { 727 names: vec!["PNGh".to_string()], 728 alg: None, 729 hash: ByteBuf::from(Vec::new()), 730 pad: ByteBuf::from(Vec::new()), 731 range_start: 0, 732 range_len: 8, 733 }; 734 box_maps.push(pngh_bm); 735 736 // add the other boxes 737 for pc in ps.into_iter() { 738 // add special C2PA box 739 if pc.name == CAI_CHUNK { 740 let c2pa_bm = BoxMap { 741 names: vec![C2PA_BOXHASH.to_string()], 742 alg: None, 743 hash: ByteBuf::from(Vec::new()), 744 pad: ByteBuf::from(Vec::new()), 745 range_start: pc.start as usize, 746 range_len: (pc.length + 12) as usize, // length(4) + name(4) + crc(4) 747 }; 748 box_maps.push(c2pa_bm); 749 continue; 750 } 751 752 // all other chunks 753 let c2pa_bm = BoxMap { 754 names: vec![pc.name_str], 755 alg: None, 756 hash: ByteBuf::from(Vec::new()), 757 pad: ByteBuf::from(Vec::new()), 758 range_start: pc.start as usize, 759 range_len: (pc.length + 12) as usize, // length(4) + name(4) + crc(4) 760 }; 761 box_maps.push(c2pa_bm); 762 } 763 764 Ok(box_maps) 765 } 766 } 767 768 impl ComposedManifestRef for PngIO { 769 fn compose_manifest(&self, manifest_data: &[u8], _format: &str) -> Result<Vec<u8>> { 770 let mut cai_data = Vec::new(); 771 let mut cai_encoder = png_pong::Encoder::new(&mut cai_data).into_chunk_enc(); 772 773 // create CAI store chunk 774 let cai_unknown = png_pong::chunk::Unknown { 775 name: CAI_CHUNK, 776 data: manifest_data.to_vec(), 777 }; 778 779 let mut cai_chunk = png_pong::chunk::Chunk::Unknown(cai_unknown); 780 cai_encoder 781 .encode(&mut cai_chunk) 782 .map_err(|_| Error::EmbeddingError)?; 783 784 Ok(cai_data) 785 } 786 } 787 788 #[cfg(test)] 789 #[allow(clippy::panic)] 790 #[allow(clippy::unwrap_used)] 791 pub mod tests { 792 use std::io::Write; 793 794 use memchr::memmem; 795 796 use super::*; 797 use crate::utils::test::{self, temp_dir_path}; 798 799 #[test] 800 fn test_png_xmp() { 801 let ap = test::fixture_path("libpng-test_with_url.png"); 802 803 let png_io = PngIO {}; 804 let xmp = png_io 805 .read_xmp(&mut std::fs::File::open(ap).unwrap()) 806 .unwrap(); 807 808 // make sure we can parse it 809 let provenance = crate::utils::xmp_inmemory_utils::extract_provenance(&xmp).unwrap(); 810 811 assert!(provenance.contains("libpng-test")); 812 } 813 814 #[test] 815 fn test_png_xmp_write() { 816 let ap = test::fixture_path("libpng-test.png"); 817 let mut source_stream = std::fs::File::open(ap).unwrap(); 818 819 let temp_dir = tempfile::tempdir().unwrap(); 820 let output = temp_dir_path(&temp_dir, "out.png"); 821 let mut output_stream = std::fs::OpenOptions::new() 822 .read(true) 823 .write(true) 824 .create(true) 825 .truncate(true) 826 .open(output) 827 .unwrap(); 828 829 let png_io = PngIO {}; 830 //let _orig_xmp = png_io 831 // .read_xmp(&mut source_stream ) 832 // .unwrap(); 833 834 // change the xmp 835 let eh = png_io.remote_ref_writer_ref().unwrap(); 836 eh.embed_reference_to_stream( 837 &mut source_stream, 838 &mut output_stream, 839 RemoteRefEmbedType::Xmp("some test data".to_string()), 840 ) 841 .unwrap(); 842 843 output_stream.rewind().unwrap(); 844 let new_xmp = png_io.read_xmp(&mut output_stream).unwrap(); 845 // make sure we can parse it 846 let provenance = crate::utils::xmp_inmemory_utils::extract_provenance(&new_xmp).unwrap(); 847 848 assert!(provenance.contains("some test data")); 849 } 850 851 #[test] 852 fn test_png_parse() { 853 let ap = test::fixture_path("libpng-test.png"); 854 855 let png_bytes = std::fs::read(&ap).unwrap(); 856 857 // grab PNG chunks and positions 858 let mut f = std::fs::File::open(ap).unwrap(); 859 let positions = get_png_chunk_positions(&mut f).unwrap(); 860 861 for hop in positions { 862 if let Some(start) = memmem::find(&png_bytes, &hop.name) { 863 if hop.start != (start - 4) as u64 { 864 panic!("find_bytes found the wrong position"); 865 // assert!(true); 866 } 867 868 println!( 869 "Chunk {} position matches, start: {}, length: {} ", 870 hop.name_str, hop.start, hop.length 871 ); 872 } 873 } 874 } 875 876 #[test] 877 fn test_write_cai_using_stream_existing_cai_data() { 878 let source = include_bytes!("../../tests/fixtures/exp-test1.png"); 879 let mut stream = Cursor::new(source.to_vec()); 880 let png_io = PngIO {}; 881 882 // cai data already exists 883 assert!(matches!( 884 png_io.read_cai(&mut stream), 885 Ok(data) if !data.is_empty(), 886 )); 887 888 // write new data 889 let output: Vec<u8> = Vec::new(); 890 let mut output_stream = Cursor::new(output); 891 892 let data_to_write: Vec<u8> = vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34]; 893 assert!(png_io 894 .write_cai(&mut stream, &mut output_stream, &data_to_write) 895 .is_ok()); 896 897 // new data replaces the existing cai data 898 let data_written = png_io.read_cai(&mut output_stream).unwrap(); 899 assert_eq!(data_to_write, data_written); 900 } 901 902 #[test] 903 fn test_write_cai_using_stream_no_cai_data() { 904 let source = include_bytes!("../../tests/fixtures/libpng-test.png"); 905 let mut stream = Cursor::new(source.to_vec()); 906 let png_io = PngIO {}; 907 908 // no cai data present in stream. 909 assert!(matches!( 910 png_io.read_cai(&mut stream), 911 Err(Error::JumbfNotFound) 912 )); 913 914 // write new data. 915 let output: Vec<u8> = Vec::new(); 916 let mut output_stream = Cursor::new(output); 917 918 let data_to_write: Vec<u8> = vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34]; 919 assert!(png_io 920 .write_cai(&mut stream, &mut output_stream, &data_to_write) 921 .is_ok()); 922 923 // assert new cai data is present. 924 let data_written = png_io.read_cai(&mut output_stream).unwrap(); 925 assert_eq!(data_to_write, data_written); 926 } 927 928 #[test] 929 fn test_write_cai_data_to_stream_wrong_format() { 930 let source = include_bytes!("../../tests/fixtures/C.jpg"); 931 let mut stream = Cursor::new(source.to_vec()); 932 let png_io = PngIO {}; 933 934 let output: Vec<u8> = Vec::new(); 935 let mut output_stream = Cursor::new(output); 936 assert!(matches!( 937 png_io.write_cai(&mut stream, &mut output_stream, &[]), 938 Err(Error::InvalidAsset(_),) 939 )); 940 } 941 942 #[test] 943 fn test_stream_object_locations() { 944 let source = include_bytes!("../../tests/fixtures/exp-test1.png"); 945 let mut stream = Cursor::new(source.to_vec()); 946 let png_io = PngIO {}; 947 let cai_pos = png_io 948 .get_object_locations_from_stream(&mut stream) 949 .unwrap() 950 .into_iter() 951 .find(|pos| pos.htype == HashBlockObjectType::Cai) 952 .unwrap(); 953 954 assert_eq!(cai_pos.offset, 33); 955 assert_eq!(cai_pos.length, 3439701); 956 } 957 958 #[test] 959 fn test_stream_object_locations_with_incorrect_file_type() { 960 let source = include_bytes!("../../tests/fixtures/unsupported_type.txt"); 961 let mut stream = Cursor::new(source.to_vec()); 962 let png_io = PngIO {}; 963 assert!(matches!( 964 png_io.get_object_locations_from_stream(&mut stream), 965 Err(Error::UnsupportedType) 966 )); 967 } 968 969 #[test] 970 fn test_stream_object_locations_adds_offsets_to_file_without_claims() { 971 let source = include_bytes!("../../tests/fixtures/libpng-test.png"); 972 let mut stream = Cursor::new(source.to_vec()); 973 974 let png_io = PngIO {}; 975 assert!(png_io 976 .get_object_locations_from_stream(&mut stream) 977 .unwrap() 978 .into_iter() 979 .any(|chunk| chunk.htype == HashBlockObjectType::Cai)); 980 } 981 982 #[test] 983 fn test_remove_c2pa() { 984 let source = test::fixture_path("exp-test1.png"); 985 let temp_dir = tempfile::tempdir().unwrap(); 986 let output = test::temp_dir_path(&temp_dir, "exp-test1_tmp.png"); 987 std::fs::copy(source, &output).unwrap(); 988 989 let png_io = PngIO {}; 990 png_io.remove_cai_store(&output).unwrap(); 991 992 // read back in asset, JumbfNotFound is expected since it was removed 993 match png_io.read_cai_store(&output) { 994 Err(Error::JumbfNotFound) => (), 995 _ => unreachable!(), 996 } 997 } 998 999 #[test] 1000 fn test_remove_c2pa_from_stream() { 1001 let source = crate::utils::test::fixture_path("exp-test1.png"); 1002 1003 let source_bytes = std::fs::read(source).unwrap(); 1004 let mut source_stream = Cursor::new(source_bytes); 1005 1006 let png_io = PngIO {}; 1007 let png_writer = png_io.get_writer("png").unwrap(); 1008 1009 let output_bytes = Vec::new(); 1010 let mut output_stream = Cursor::new(output_bytes); 1011 1012 png_writer 1013 .remove_cai_store_from_stream(&mut source_stream, &mut output_stream) 1014 .unwrap(); 1015 1016 // read back in asset, JumbfNotFound is expected since it was removed 1017 let png_reader = png_io.get_reader(); 1018 match png_reader.read_cai(&mut output_stream) { 1019 Err(Error::JumbfNotFound) => (), 1020 _ => unreachable!(), 1021 } 1022 } 1023 1024 #[test] 1025 fn test_embeddable_manifest() { 1026 let png_io = PngIO {}; 1027 1028 let source = crate::utils::test::fixture_path("exp-test1.png"); 1029 1030 let ol = png_io.get_object_locations(&source).unwrap(); 1031 1032 let cai_loc = ol 1033 .iter() 1034 .find(|o| o.htype == HashBlockObjectType::Cai) 1035 .unwrap(); 1036 let curr_manifest = png_io.read_cai_store(&source).unwrap(); 1037 1038 let temp_dir = tempfile::tempdir().unwrap(); 1039 let output = crate::utils::test::temp_dir_path(&temp_dir, "exp-test1-out.png"); 1040 1041 std::fs::copy(source, &output).unwrap(); 1042 1043 // remove existing 1044 png_io.remove_cai_store(&output).unwrap(); 1045 1046 // generate new manifest data 1047 let em = png_io 1048 .composed_data_ref() 1049 .unwrap() 1050 .compose_manifest(&curr_manifest, "png") 1051 .unwrap(); 1052 1053 // insert new manifest 1054 let outbuf = Vec::new(); 1055 let mut out_stream = Cursor::new(outbuf); 1056 1057 let mut before = vec![0u8; cai_loc.offset]; 1058 let mut in_file = std::fs::File::open(&output).unwrap(); 1059 1060 // write before 1061 in_file.read_exact(before.as_mut_slice()).unwrap(); 1062 out_stream.write_all(&before).unwrap(); 1063 1064 // write composed bytes 1065 out_stream.write_all(&em).unwrap(); 1066 1067 // write bytes after 1068 let mut after_buf = Vec::new(); 1069 in_file.read_to_end(&mut after_buf).unwrap(); 1070 out_stream.write_all(&after_buf).unwrap(); 1071 1072 // read manifest back in from new in-memory PNG 1073 out_stream.rewind().unwrap(); 1074 let restored_manifest = png_io.read_cai(&mut out_stream).unwrap(); 1075 1076 assert_eq!(&curr_manifest, &restored_manifest); 1077 } 1078 }