c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

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 }