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

pdf_io.rs (7144B)


      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::{fs::File, path::Path};
     15 
     16 use crate::{
     17     asset_handlers::pdf::{C2paPdf, Pdf},
     18     asset_io::{AssetIO, CAIRead, CAIReader, CAIWriter, ComposedManifestRef, HashObjectPositions},
     19     Error,
     20     Error::{JumbfNotFound, NotImplemented, PdfReadError},
     21 };
     22 
     23 static SUPPORTED_TYPES: [&str; 2] = ["pdf", "application/pdf"];
     24 static WRITE_NOT_IMPLEMENTED: &str = "PDF write functionality will be added in a future release";
     25 
     26 pub struct PdfIO {}
     27 
     28 impl CAIReader for PdfIO {
     29     fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> crate::Result<Vec<u8>> {
     30         asset_reader.rewind()?;
     31         let pdf = Pdf::from_reader(asset_reader).map_err(|e| Error::InvalidAsset(e.to_string()))?;
     32         self.read_manifest_bytes(pdf)
     33     }
     34 
     35     fn read_xmp(&self, asset_reader: &mut dyn CAIRead) -> Option<String> {
     36         if asset_reader.rewind().is_err() {
     37             return None;
     38         }
     39 
     40         let Ok(pdf) = Pdf::from_reader(asset_reader) else {
     41             return None;
     42         };
     43 
     44         self.read_xmp_from_pdf(pdf)
     45     }
     46 }
     47 
     48 impl PdfIO {
     49     fn read_manifest_bytes(&self, pdf: impl C2paPdf) -> crate::Result<Vec<u8>> {
     50         let Ok(result) = pdf.read_manifest_bytes() else {
     51             return Err(PdfReadError);
     52         };
     53 
     54         let Some(bytes) = result else {
     55             return Err(JumbfNotFound);
     56         };
     57 
     58         match bytes.as_slice() {
     59             [bytes] => Ok(bytes.to_vec()),
     60             _ => Err(NotImplemented(
     61                 "c2pa-rs only supports reading PDFs with one manifest".into(),
     62             )),
     63         }
     64     }
     65 
     66     fn read_xmp_from_pdf(&self, pdf: impl C2paPdf) -> Option<String> {
     67         pdf.read_xmp()
     68     }
     69 }
     70 
     71 impl AssetIO for PdfIO {
     72     fn new(_asset_type: &str) -> Self
     73     where
     74         Self: Sized,
     75     {
     76         Self {}
     77     }
     78 
     79     fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> {
     80         Box::new(PdfIO::new(asset_type))
     81     }
     82 
     83     fn get_reader(&self) -> &dyn CAIReader {
     84         self
     85     }
     86 
     87     fn get_writer(&self, _asset_type: &str) -> Option<Box<dyn CAIWriter>> {
     88         None
     89     }
     90 
     91     fn read_cai_store(&self, asset_path: &Path) -> crate::Result<Vec<u8>> {
     92         let mut f = File::open(asset_path)?;
     93         self.read_cai(&mut f)
     94     }
     95 
     96     fn save_cai_store(&self, _asset_path: &Path, _store_bytes: &[u8]) -> crate::Result<()> {
     97         Err(NotImplemented(WRITE_NOT_IMPLEMENTED.into()))
     98     }
     99 
    100     fn get_object_locations(&self, _asset_path: &Path) -> crate::Result<Vec<HashObjectPositions>> {
    101         Err(NotImplemented(WRITE_NOT_IMPLEMENTED.into()))
    102     }
    103 
    104     fn remove_cai_store(&self, _asset_path: &Path) -> crate::Result<()> {
    105         Err(NotImplemented(WRITE_NOT_IMPLEMENTED.into()))
    106     }
    107 
    108     fn supported_types(&self) -> &[&str] {
    109         &SUPPORTED_TYPES
    110     }
    111 
    112     fn composed_data_ref(&self) -> Option<&dyn ComposedManifestRef> {
    113         Some(self)
    114     }
    115 }
    116 
    117 impl ComposedManifestRef for PdfIO {
    118     // Return entire CAI block as Vec<u8>
    119     fn compose_manifest(&self, manifest_data: &[u8], _format: &str) -> Result<Vec<u8>, Error> {
    120         Ok(manifest_data.to_vec())
    121     }
    122 }
    123 
    124 #[cfg(test)]
    125 pub mod tests {
    126     #![allow(clippy::panic)]
    127     #![allow(clippy::unwrap_used)]
    128 
    129     use std::io::Cursor;
    130 
    131     use crate::{
    132         asset_handlers,
    133         asset_handlers::{pdf::MockC2paPdf, pdf_io::PdfIO},
    134         asset_io::{AssetIO, CAIReader},
    135     };
    136 
    137     static MANIFEST_BYTES: &[u8; 2] = &[10u8, 20u8];
    138 
    139     #[test]
    140     fn test_error_reading_manifest_fails() {
    141         let mut mock_pdf = MockC2paPdf::default();
    142         mock_pdf.expect_read_manifest_bytes().returning(|| {
    143             Err(asset_handlers::pdf::Error::UnableToReadPdf(
    144                 lopdf::Error::ReferenceLimit,
    145             ))
    146         });
    147 
    148         let pdf_io = PdfIO::new("pdf");
    149         assert!(matches!(
    150             pdf_io.read_manifest_bytes(mock_pdf),
    151             Err(crate::Error::PdfReadError)
    152         ))
    153     }
    154 
    155     #[test]
    156     fn test_no_manifest_found_returns_no_jumbf_error() {
    157         let mut mock_pdf = MockC2paPdf::default();
    158         mock_pdf.expect_read_manifest_bytes().returning(|| Ok(None));
    159         let pdf_io = PdfIO::new("pdf");
    160 
    161         assert!(matches!(
    162             pdf_io.read_manifest_bytes(mock_pdf),
    163             Err(crate::Error::JumbfNotFound)
    164         ));
    165     }
    166 
    167     #[test]
    168     fn test_one_manifest_found_returns_bytes() {
    169         let mut mock_pdf = MockC2paPdf::default();
    170         mock_pdf
    171             .expect_read_manifest_bytes()
    172             .returning(|| Ok(Some(vec![MANIFEST_BYTES])));
    173 
    174         let pdf_io = PdfIO::new("pdf");
    175         assert_eq!(
    176             pdf_io.read_manifest_bytes(mock_pdf).unwrap(),
    177             MANIFEST_BYTES.to_vec()
    178         );
    179     }
    180 
    181     #[test]
    182     fn test_multiple_manifest_fail_with_not_implemented_error() {
    183         let mut mock_pdf = MockC2paPdf::default();
    184         mock_pdf
    185             .expect_read_manifest_bytes()
    186             .returning(|| Ok(Some(vec![MANIFEST_BYTES, MANIFEST_BYTES, MANIFEST_BYTES])));
    187 
    188         let pdf_io = PdfIO::new("pdf");
    189 
    190         assert!(matches!(
    191             pdf_io.read_manifest_bytes(mock_pdf),
    192             Err(crate::Error::NotImplemented(_))
    193         ));
    194     }
    195 
    196     #[test]
    197     fn test_returns_none_when_no_xmp() {
    198         let mut mock_pdf = MockC2paPdf::default();
    199         mock_pdf.expect_read_xmp().returning(|| None);
    200 
    201         let pdf_io = PdfIO::new("pdf");
    202         assert!(pdf_io.read_xmp_from_pdf(mock_pdf).is_none());
    203     }
    204 
    205     #[test]
    206     fn test_returns_some_when_some_xmp() {
    207         let mut mock_pdf = MockC2paPdf::default();
    208         mock_pdf.expect_read_xmp().returning(|| Some("xmp".into()));
    209 
    210         let pdf_io = PdfIO::new("pdf");
    211         assert!(pdf_io.read_xmp_from_pdf(mock_pdf).is_some());
    212     }
    213 
    214     #[test]
    215     fn test_cai_read_finds_no_manifest() {
    216         let source = crate::utils::test::fixture_path("basic.pdf");
    217         let pdf_io = PdfIO::new("pdf");
    218 
    219         assert!(matches!(
    220             pdf_io.read_cai_store(&source),
    221             Err(crate::Error::JumbfNotFound)
    222         ));
    223     }
    224 
    225     #[test]
    226     fn test_cai_read_xmp_finds_xmp_data() {
    227         let source = include_bytes!("../../tests/fixtures/basic.pdf");
    228         let mut stream = Cursor::new(source.to_vec());
    229 
    230         let pdf_io = PdfIO::new("pdf");
    231         assert!(pdf_io.read_xmp(&mut stream).is_some());
    232     }
    233 
    234     #[test]
    235     fn test_read_cai_express_pdf_finds_single_manifest_store() {
    236         let source = include_bytes!("../../tests/fixtures/express-signed.pdf");
    237         let pdf_io = PdfIO::new("pdf");
    238         let mut pdf_stream = Cursor::new(source.to_vec());
    239         assert!(pdf_io.read_cai(&mut pdf_stream).is_ok());
    240     }
    241 }