diff --git a/sdk/src/asset_handlers/bmff_io.rs b/sdk/src/asset_handlers/bmff_io.rs index fd6377c99..f97409305 100644 --- a/sdk/src/asset_handlers/bmff_io.rs +++ b/sdk/src/asset_handlers/bmff_io.rs @@ -1537,6 +1537,21 @@ pub(crate) fn read_bmff_c2pa_boxes(reader: &mut dyn CAIRead) -> Result Result> { + reader.seek(SeekFrom::Start(4))?; + + let mut header = [0u8; 4]; + reader.read_exact(&mut header)?; + + if header[..4] != *b"ftyp" { + return Err(BmffError::InvalidFileSignature { + reason: format!( + "invalid BMFF structure: expected box type \"ftyp\" at offset 4, found {}", + String::from_utf8_lossy(&header[..4]) + ), + } + .into()); + } + let c2pa_boxes = read_bmff_c2pa_boxes(reader)?; // is this an update manifest? @@ -2271,6 +2286,12 @@ impl RemoteRefEmbed for BmffIO { } } +#[derive(Debug, thiserror::Error)] +pub enum BmffError { + #[error("invalid file signature: {reason}")] + InvalidFileSignature { reason: String }, +} + #[cfg(test)] pub mod tests { #![allow(clippy::expect_used)] diff --git a/sdk/src/asset_handlers/c2pa_io.rs b/sdk/src/asset_handlers/c2pa_io.rs index 862a90f8e..1ec168d69 100644 --- a/sdk/src/asset_handlers/c2pa_io.rs +++ b/sdk/src/asset_handlers/c2pa_io.rs @@ -35,6 +35,8 @@ pub struct C2paIO {} impl CAIReader for C2paIO { fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result> { + asset_reader.rewind()?; + let mut cai_data = Vec::new(); // read the whole file asset_reader.read_to_end(&mut cai_data)?; diff --git a/sdk/src/asset_handlers/gif_io.rs b/sdk/src/asset_handlers/gif_io.rs index 8931ec0b2..f7ca2e128 100644 --- a/sdk/src/asset_handlers/gif_io.rs +++ b/sdk/src/asset_handlers/gif_io.rs @@ -817,11 +817,26 @@ impl Header { let mut signature = [0u8; 3]; stream.read_exact(&mut signature)?; if signature != *b"GIF" { - return Err(Error::InvalidAsset("GIF signature invalid".to_owned())); + return Err(GifError::InvalidFileSignature { + reason: format!( + "invalid header signature: expected \"GIF\", found \"{}\"", + String::from_utf8_lossy(&signature) + ), + } + .into()); } let mut version = [0u8; 3]; stream.read_exact(&mut version)?; + if version != *b"87a" && version != *b"89a" { + return Err(GifError::InvalidFileSignature { + reason: format!( + "invalid header version: expected \"89a\" or \"87a\", found \"{}\"", + String::from_utf8_lossy(&version) + ), + } + .into()); + } Ok(Header { // version @@ -1140,6 +1155,12 @@ fn gif_chunks(mut encoded_bytes: &[u8]) -> impl Iterator { }) } +#[derive(Debug, thiserror::Error)] +pub enum GifError { + #[error("invalid file signature: {reason}")] + InvalidFileSignature { reason: String }, +} + #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] diff --git a/sdk/src/asset_handlers/jpeg_io.rs b/sdk/src/asset_handlers/jpeg_io.rs index 2e0ad763f..d9c64030b 100644 --- a/sdk/src/asset_handlers/jpeg_io.rs +++ b/sdk/src/asset_handlers/jpeg_io.rs @@ -191,8 +191,17 @@ impl CAIReader for JpegIO { asset_reader.rewind()?; asset_reader.read_to_end(&mut buf).map_err(Error::IoError)?; - let dimg_opt = DynImage::from_bytes(buf.into()) - .map_err(|_err| Error::InvalidAsset("Could not parse input JPEG".to_owned()))?; + let dimg_opt = DynImage::from_bytes(buf.into()).map_err(|err| match err { + img_parts::Error::WrongSignature => JpegError::InvalidFileSignature { + reason: format!( + "it may be because the stream does not start with \"{} {}\"", + markers::P, + markers::SOI + ), + } + .into(), + _ => Error::InvalidAsset("Could not parse input JPEG".to_owned()), + })?; if let Some(dimg) = dimg_opt { match dimg { @@ -1138,6 +1147,12 @@ impl ComposedManifestRef for JpegIO { } } +#[derive(Debug, thiserror::Error)] +pub enum JpegError { + #[error("invalid file signature: {reason}")] + InvalidFileSignature { reason: String }, +} + #[cfg(test)] pub mod tests { #![allow(clippy::unwrap_used)] diff --git a/sdk/src/asset_handlers/mp3_io.rs b/sdk/src/asset_handlers/mp3_io.rs index 46ab3c10c..dfad84753 100644 --- a/sdk/src/asset_handlers/mp3_io.rs +++ b/sdk/src/asset_handlers/mp3_io.rs @@ -495,6 +495,9 @@ impl AssetPatch for Mp3IO { } } +#[derive(Debug, thiserror::Error)] +pub enum Mp3Error {} + #[cfg(test)] pub mod tests { #![allow(clippy::expect_used)] diff --git a/sdk/src/asset_handlers/pdf_io.rs b/sdk/src/asset_handlers/pdf_io.rs index 0210b611a..d7c9ed6c0 100644 --- a/sdk/src/asset_handlers/pdf_io.rs +++ b/sdk/src/asset_handlers/pdf_io.rs @@ -16,8 +16,7 @@ use std::{fs::File, path::Path}; use crate::{ asset_handlers::pdf::{C2paPdf, Pdf}, asset_io::{AssetIO, CAIRead, CAIReader, CAIWriter, ComposedManifestRef, HashObjectPositions}, - Error, - Error::{JumbfNotFound, NotImplemented, PdfReadError}, + Error::{self, JumbfNotFound, NotImplemented, PdfReadError}, }; static SUPPORTED_TYPES: [&str; 2] = ["pdf", "application/pdf"]; @@ -28,6 +27,21 @@ pub struct PdfIO {} impl CAIReader for PdfIO { fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> crate::Result> { asset_reader.rewind()?; + + let mut header = [0u8; 5]; + asset_reader.read_exact(&mut header)?; + if header != *b"%PDF-" { + return Err(PdfError::InvalidFileSignature { + reason: format!( + "invalid header signature: expected \"%PDF-\", found {}", + String::from_utf8_lossy(&header) + ), + } + .into()); + } + + asset_reader.rewind()?; + let pdf = Pdf::from_reader(asset_reader).map_err(|e| Error::InvalidAsset(e.to_string()))?; self.read_manifest_bytes(pdf) } @@ -121,6 +135,12 @@ impl ComposedManifestRef for PdfIO { } } +#[derive(Debug, thiserror::Error)] +pub enum PdfError { + #[error("invalid file signature: {reason}")] + InvalidFileSignature { reason: String }, +} + #[cfg(test)] pub mod tests { #![allow(clippy::panic)] diff --git a/sdk/src/asset_handlers/png_io.rs b/sdk/src/asset_handlers/png_io.rs index af995d09d..dac93e2a0 100644 --- a/sdk/src/asset_handlers/png_io.rs +++ b/sdk/src/asset_handlers/png_io.rs @@ -71,10 +71,12 @@ fn get_png_chunk_positions(f: &mut R) -> Result Result Ok(String::from_utf8_lossy(&s).to_string()) } + pub struct PngIO {} impl CAIReader for PngIO { @@ -782,6 +785,12 @@ impl ComposedManifestRef for PngIO { } } +#[derive(Debug, thiserror::Error)] +pub enum PngError { + #[error("invalid file signature: {reason}")] + InvalidFileSignature { reason: String }, +} + #[cfg(test)] #[allow(clippy::panic)] #[allow(clippy::unwrap_used)] @@ -935,7 +944,7 @@ pub mod tests { let mut output_stream = Cursor::new(output); assert!(matches!( png_io.write_cai(&mut stream, &mut output_stream, &[]), - Err(Error::InvalidAsset(_),) + Err(Error::PngError(PngError::InvalidFileSignature { .. })) )); } diff --git a/sdk/src/asset_handlers/riff_io.rs b/sdk/src/asset_handlers/riff_io.rs index b1547dd18..1aa3efe35 100644 --- a/sdk/src/asset_handlers/riff_io.rs +++ b/sdk/src/asset_handlers/riff_io.rs @@ -264,7 +264,14 @@ impl CAIReader for RiffIO { let top_level_chunks = Chunk::read(&mut chunk_reader, 0)?; if top_level_chunks.id() != RIFF_ID { - return Err(Error::InvalidAsset("Invalid RIFF format".to_string())); + return Err(RiffError::InvalidFileSignature { + reason: format!( + "invalid header: expected \"{}\", got \"{}\"", + String::from_utf8_lossy(&RIFF_ID.value), + String::from_utf8_lossy(&top_level_chunks.id().value), + ), + } + .into()); } for result in top_level_chunks.iter(&mut chunk_reader) { @@ -619,6 +626,12 @@ impl RemoteRefEmbed for RiffIO { } } +#[derive(Debug, thiserror::Error)] +pub enum RiffError { + #[error("invalid file signature: {reason}")] + InvalidFileSignature { reason: String }, +} + #[cfg(test)] pub mod tests { #![allow(clippy::expect_used)] diff --git a/sdk/src/asset_handlers/svg_io.rs b/sdk/src/asset_handlers/svg_io.rs index f3b5d55ba..c41a8b709 100644 --- a/sdk/src/asset_handlers/svg_io.rs +++ b/sdk/src/asset_handlers/svg_io.rs @@ -221,9 +221,21 @@ fn detect_manifest_location( let name = String::from_utf8_lossy(e.name().into_inner()).into_owned(); xml_path.push(name); - if xml_path.len() == 2 && xml_path[0] == SVG && xml_path[1] == METADATA { - detected_level = DetectedTagsDepth::Metadata; - insertion_point = xml_reader.buffer_position(); + if xml_path.len() == 2 { + if xml_path[0] == SVG { + if xml_path[1] == METADATA { + detected_level = DetectedTagsDepth::Metadata; + insertion_point = xml_reader.buffer_position(); + } + } else { + return Err(SvgError::InvalidFileSignature { + reason: format!( + "invalid tag structure: root element must be \"{}\", found \"{}\"", + SVG, xml_path[0] + ), + } + .into()); + } } if xml_path.len() == 3 @@ -736,6 +748,12 @@ impl RemoteRefEmbed for SvgIO { } } +#[derive(Debug, thiserror::Error)] +pub enum SvgError { + #[error("invalid file signature: {reason}")] + InvalidFileSignature { reason: String }, +} + #[cfg(test)] pub mod tests { #![allow(clippy::expect_used)] diff --git a/sdk/src/asset_handlers/tiff_io.rs b/sdk/src/asset_handlers/tiff_io.rs index f20731c91..b5d0693ae 100644 --- a/sdk/src/asset_handlers/tiff_io.rs +++ b/sdk/src/asset_handlers/tiff_io.rs @@ -150,7 +150,6 @@ pub struct ImageFileDirectory { } impl ImageFileDirectory { - #[allow(dead_code)] pub fn get_tag(&self, tag_id: u16) -> Option<&IfdEntry> { self.entries.get(&tag_id) } @@ -162,16 +161,15 @@ impl ImageFileDirectory { } // Struct to map the contents of a TIFF file -#[allow(dead_code)] pub(crate) struct TiffStructure { byte_order: Endianness, big_tiff: bool, + #[allow(dead_code)] first_ifd_offset: u64, first_ifd: Option, } impl TiffStructure { - #[allow(dead_code)] pub fn load(reader: &mut R) -> Result where R: Read + Seek + ?Sized, @@ -182,37 +180,52 @@ impl TiffStructure { let byte_order = match endianness { II => Endianness::Little, MM => Endianness::Big, - _ => { - return Err(Error::InvalidAsset( - "Could not parse input image".to_owned(), - )) + endianness => { + return Err(TiffError::InvalidFileSignature { + reason: format!( + "invalid header signature: expected endianness \"II\" or \"MM\", found \"{}\"", + String::from_utf8_lossy(&endianness) + ), + } + .into()) } }; let mut byte_reader = ByteOrdered::runtime(reader, byte_order); - let big_tiff = match byte_reader.read_u16() { - Ok(42) => false, - Ok(43) => { + let big_tiff = match byte_reader.read_u16()? { + 42 => false, + 43 => { // read Big TIFF structs // Read byte size of offsets, must be 8 - if byte_reader.read_u16()? != 8 { - return Err(Error::InvalidAsset( - "Could not parse input image".to_owned(), - )); + let first_ifd_offset = byte_reader.read_u16()?; + if first_ifd_offset != 8 { + return Err(TiffError::InvalidFileSignature { + reason: format!( + "invalid header signature: expected first IFD offset for BigTiff to be \"8\", found \"{}\"", + first_ifd_offset + ), + }.into()); } // must currently be 0 - if byte_reader.read_u16()? != 0 { - return Err(Error::InvalidAsset( - "Could not parse input image".to_owned(), - )); + let reserved = byte_reader.read_u16()?; + if reserved != 0 { + return Err(TiffError::InvalidFileSignature { + reason: format!( + "invalid header signature: expected bytes after first IFD offset for BigTiff to be \"0\", found \"{}\"", + reserved + ), + }.into()); } true } - _ => { - return Err(Error::InvalidAsset( - "Could not parse input image".to_owned(), - )) + magic => { + return Err(TiffError::InvalidFileSignature { + reason: format!( + "invalid header signature: expected magic \"2A\" (TIFF) \"2B\" (BigTIFF), found \"{:02X}\"", + magic + ), + }.into()); } }; @@ -1332,6 +1345,7 @@ where asset_reader.read_to_vec(xmp_ifd_entry.value_count).ok() } + pub struct TiffIO {} impl CAIReader for TiffIO { @@ -1618,6 +1632,12 @@ impl ComposedManifestRef for TiffIO { } } +#[derive(Debug, thiserror::Error)] +pub enum TiffError { + #[error("invalid file signature: {reason}")] + InvalidFileSignature { reason: String }, +} + #[cfg(test)] pub mod tests { #![allow(clippy::panic)] diff --git a/sdk/src/asset_io.rs b/sdk/src/asset_io.rs index 2f7a80306..5fed86a1b 100644 --- a/sdk/src/asset_io.rs +++ b/sdk/src/asset_io.rs @@ -260,7 +260,7 @@ pub trait RemoteRefEmbed { /// `ComposedManifestRefEmbed` is used to generate a C2PA manifest. The /// returned `Vec` contains data preformatted to be directly compatible -/// with the type specified in `format`. +/// with the type specified in `format`. pub trait ComposedManifestRef { // Return entire CAI block as Vec fn compose_manifest(&self, manifest_data: &[u8], format: &str) -> Result>; diff --git a/sdk/src/error.rs b/sdk/src/error.rs index d9dfb931c..129115f89 100644 --- a/sdk/src/error.rs +++ b/sdk/src/error.rs @@ -15,7 +15,13 @@ use thiserror::Error; +#[cfg(feature = "pdf")] +use crate::asset_handlers::pdf_io::PdfError; use crate::{ + asset_handlers::{ + bmff_io::BmffError, gif_io::GifError, jpeg_io::JpegError, mp3_io::Mp3Error, + png_io::PngError, riff_io::RiffError, svg_io::SvgError, tiff_io::TiffError, + }, crypto::{cose::CoseError, raw_signature::RawSignerError, time_stamp::TimeStampError}, http::HttpResolverError, }; @@ -370,6 +376,34 @@ pub enum Error { // The string should be one of the C2PA validation codes #[error("C2PA Validation Error: {0}")] C2PAValidation(String), + + #[error("error parsing BMFF: {0}")] + BmffError(#[from] BmffError), + + #[error("error parsing GIF: {0}")] + GifError(#[from] GifError), + + #[error("error parsing JPEG: {0}")] + JpegError(#[from] JpegError), + + #[error("error parsing MP3: {0}")] + Mp3Error(#[from] Mp3Error), + + #[cfg(feature = "pdf")] + #[error("error parsing PDF: {0}")] + PdfError(#[from] PdfError), + + #[error("error parsing PNG: {0}")] + PngError(#[from] PngError), + + #[error("error parsing RIFF: {0}")] + RiffError(#[from] RiffError), + + #[error("error parsing SVG: {0}")] + SvgError(#[from] SvgError), + + #[error("error parsing TIFF: {0}")] + TiffError(#[from] TiffError), } /// A specialized `Result` type for C2PA toolkit operations. diff --git a/sdk/src/jumbf_io.rs b/sdk/src/jumbf_io.rs index 38c299b5d..5d8e8104b 100644 --- a/sdk/src/jumbf_io.rs +++ b/sdk/src/jumbf_io.rs @@ -136,6 +136,7 @@ pub fn save_jumbf_to_stream( /// writes the jumbf data in store_bytes into an asset in data and returns the newly created asset pub fn save_jumbf_to_memory(asset_type: &str, data: &[u8], store_bytes: &[u8]) -> Result> { let mut input_stream = Cursor::new(data); + let output_vec: Vec = Vec::with_capacity(data.len() + store_bytes.len() + 1024); let mut output_stream = Cursor::new(output_vec); @@ -508,8 +509,12 @@ pub mod tests { .unwrap(); removed.set_position(0); let result = load_jumbf_from_stream(asset_type, &mut removed); - if (asset_type != "wav") - && (asset_type != "avi" && asset_type != "mp3" && asset_type != "webp") + // For c2pa this is expected to return `Error::InvalidFormat`. + if asset_type != "wav" + && asset_type != "avi" + && asset_type != "mp3" + && asset_type != "webp" + && asset_type != "c2pa" { assert!(matches!(&result.err().unwrap(), Error::JumbfNotFound)); }