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 }