use std::io::{self, Read, Seek, SeekFrom};
use byteorder::{BigEndian, ReadBytesExt};
use png_pong::chunk::InternationalText;
use crate::{
asset_io::{
AllowedExclusion, AssetBoxHash, AssetIO, BoxMap, C2paReader, C2paWriter,
ComposedManifestRef, ObjectLocations, ObjectType, ReadSeek, ReadWriteSeek,
RemoteManifestUrl, WriteXmp, C2PA_BOXHASH,
},
error::{Error, Result},
utils::io_utils::{patch_stream, ReaderUtils},
};
const PNG_ID: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
const CAI_CHUNK: [u8; 4] = *b"caBX";
const IMG_HDR: [u8; 4] = *b"IHDR";
const ITXT_CHUNK: [u8; 4] = *b"iTXt";
const XMP_KEY: &str = "XML:com.adobe.xmp";
const PNG_END: [u8; 4] = *b"IEND";
const PNG_HDR_LEN: u64 = 12;
static SUPPORTED_TYPES: [&str; 2] = ["png", "image/png"];
#[derive(Clone, Debug)]
struct PngChunkPos {
pub start: u64,
pub length: u32,
pub name: [u8; 4],
#[allow(dead_code)]
pub name_str: String,
}
impl PngChunkPos {
pub fn end(&self) -> u64 {
self.start + self.length as u64 + PNG_HDR_LEN
}
}
fn get_png_chunk_positions<R: Read + Seek + ?Sized>(f: &mut R) -> Result<Vec<PngChunkPos>> {
let current_len = f.seek(SeekFrom::End(0))?;
let mut chunk_positions: Vec<PngChunkPos> = Vec::new();
f.rewind()?;
let mut buf4 = [0; 4];
let mut hdr = [0; 8];
f.read_exact(&mut hdr)?;
if hdr != PNG_ID {
return Err(Error::InvalidAsset(format!(
"invalid header: expected {PNG_ID:02X?}, got {hdr:02X?}"
)));
}
loop {
let current_pos = f.stream_position()?;
let length = f
.read_u32::<BigEndian>()
.map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?;
f.read_exact(&mut buf4)
.map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?;
let name = buf4;
f.seek(SeekFrom::Current(length as i64))
.map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?;
f.read_exact(&mut buf4)
.map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?;
let chunk_name = String::from_utf8(name.to_vec())
.map_err(|_err| Error::InvalidAsset("PNG bad chunk name".to_string()))?;
let pcp = PngChunkPos {
start: current_pos,
length,
name,
name_str: chunk_name,
};
chunk_positions.push(pcp);
if name == PNG_END || f.stream_position()? > current_len {
break;
}
}
Ok(chunk_positions)
}
fn get_cai_data<R: Read + Seek + ?Sized>(mut f: &mut R) -> Result<Vec<u8>> {
let ps = get_png_chunk_positions(f)?;
if ps.iter().filter(|pcp| pcp.name == CAI_CHUNK).count() > 1 {
return Err(Error::TooManyManifestStores);
}
let pcp = ps
.iter()
.find(|pcp| pcp.name == CAI_CHUNK)
.ok_or(Error::JumbfNotFound)?;
let length: usize = pcp.length as usize;
f.seek(SeekFrom::Start(pcp.start + 8))?;
f.read_to_vec(length as u64)
}
fn read_string(asset_reader: &mut dyn ReadSeek, max_read: u32) -> Result<(String, u32)> {
let mut bytes_read: u32 = 0;
let mut s: Vec<u8> = Vec::with_capacity(80);
while bytes_read < max_read {
let c = asset_reader.read_u8()?;
bytes_read += 1;
if c == 0 {
break;
}
s.push(c);
}
Ok((String::from_utf8_lossy(&s).to_string(), bytes_read))
}
pub struct PngIO {}
impl C2paReader for PngIO {
fn read_c2pa(&self, input_stream: &mut dyn ReadSeek) -> Result<Vec<u8>> {
let cai_data = get_cai_data(input_stream)?;
Ok(cai_data)
}
fn read_xmp(&self, mut input_stream: &mut dyn ReadSeek) -> Option<String> {
let ps = get_png_chunk_positions(input_stream).ok()?;
let mut xmp_str: Option<String> = None;
ps.iter().find(|pcp| {
if pcp.name == ITXT_CHUNK {
if input_stream.seek(SeekFrom::Start(pcp.start + 8)).is_err() {
return false;
}
let mut remaining: u32 = pcp.length;
let (key, consumed) = match read_string(input_stream, remaining) {
Ok(v) => v,
Err(_) => return false,
};
remaining = match remaining.checked_sub(consumed) {
Some(r) => r,
None => return false,
};
if key.is_empty() || key.len() > 79 {
return false;
}
if key != XMP_KEY {
return false;
}
remaining = match remaining.checked_sub(2) {
Some(r) => r,
None => return false,
};
let compressed = match input_stream.read_u8() {
Ok(c) => c != 0,
Err(_) => return false,
};
let _compression_method = match input_stream.read_u8() {
Ok(c) => c != 0,
Err(_) => return false,
};
let (_langtag, consumed) = match read_string(input_stream, remaining) {
Ok(v) => v,
Err(_) => return false,
};
remaining = match remaining.checked_sub(consumed) {
Some(r) => r,
None => return false,
};
let (_transkey, consumed) = match read_string(input_stream, remaining) {
Ok(v) => v,
Err(_) => return false,
};
remaining = match remaining.checked_sub(consumed) {
Some(r) => r,
None => return false,
};
let data = match input_stream.read_to_vec(remaining as u64) {
Ok(v) => v,
Err(_) => return false,
};
let val = if compressed {
return false;
} else {
String::from_utf8_lossy(&data).to_string()
};
xmp_str = Some(val);
true
} else {
false
}
});
xmp_str
}
}
impl C2paWriter for PngIO {
fn write_c2pa(
&self,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
store_bytes: &[u8],
) -> Result<()> {
let mut c2pa_data = Vec::new();
let mut c2pa_encoder = png_pong::Encoder::new(&mut c2pa_data).into_chunk_enc();
let mut c2pa_chunk = png_pong::chunk::Chunk::Unknown(png_pong::chunk::Unknown {
name: CAI_CHUNK,
data: store_bytes.to_vec(),
});
c2pa_encoder
.encode(&mut c2pa_chunk)
.map_err(|_| Error::EmbeddingError)?;
input_stream.rewind()?;
let ps = get_png_chunk_positions(input_stream)?;
let ihdr_end = ps
.iter()
.find(|pcp| pcp.name == IMG_HDR)
.ok_or(Error::EmbeddingError)?
.end();
let existing_c2pa_position = ps
.iter()
.find(|pcp| pcp.name == CAI_CHUNK)
.map(|pcp| (pcp.start, pcp.end()));
input_stream.rewind()?;
output_stream.rewind()?;
match existing_c2pa_position {
Some((c2pa_start, c2pa_end)) if c2pa_end <= ihdr_end => {
io::copy(&mut input_stream.take(c2pa_start), output_stream)?;
input_stream.seek(SeekFrom::Start(c2pa_end))?;
io::copy(&mut input_stream.take(ihdr_end - c2pa_end), output_stream)?;
output_stream.write_all(&c2pa_data)?;
io::copy(input_stream, output_stream)?;
}
Some((c2pa_start, c2pa_end)) => {
io::copy(&mut input_stream.take(ihdr_end), output_stream)?;
output_stream.write_all(&c2pa_data)?;
io::copy(&mut input_stream.take(c2pa_start - ihdr_end), output_stream)?;
input_stream.seek(SeekFrom::Start(c2pa_end))?;
io::copy(input_stream, output_stream)?;
}
None => {
io::copy(&mut input_stream.take(ihdr_end), output_stream)?;
output_stream.write_all(&c2pa_data)?;
io::copy(input_stream, output_stream)?;
}
}
Ok(())
}
fn get_object_locations(
&self,
input_stream: &mut dyn ReadSeek,
) -> Result<Vec<ObjectLocations>> {
let mut positions: Vec<ObjectLocations> = Vec::new();
input_stream.rewind()?;
let mut ps = get_png_chunk_positions(input_stream)?;
let (ps, file_end) = if ps.iter().any(|chunk| chunk.name == CAI_CHUNK) {
let file_end = input_stream.seek(SeekFrom::End(0))?;
(ps, file_end)
} else {
let ihdr_index = ps
.iter()
.position(|c| c.name == IMG_HDR)
.ok_or(Error::EmbeddingError)?;
ps.insert(
ihdr_index + 1,
PngChunkPos {
start: ps[ihdr_index].end(),
length: 0,
name: CAI_CHUNK,
name_str: String::from_utf8_lossy(&CAI_CHUNK).into_owned(),
},
);
let file_end = input_stream.seek(SeekFrom::End(0))?;
(ps, file_end + PNG_HDR_LEN)
};
let pcp = ps
.into_iter()
.find(|pcp| pcp.name == CAI_CHUNK)
.ok_or(Error::JumbfNotFound)?;
let cai_offset = pcp.start;
let cai_length = pcp.length as u64 + PNG_HDR_LEN;
let end = pcp.end();
positions.push(ObjectLocations {
offset: cai_offset,
length: cai_length,
htype: ObjectType::C2pa,
});
positions.push(ObjectLocations {
offset: 0,
length: cai_offset,
htype: ObjectType::Other,
});
positions.push(ObjectLocations {
offset: end, length: file_end - end,
htype: ObjectType::Other,
});
Ok(positions)
}
fn remove_c2pa(
&self,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
) -> Result<()> {
let ps = get_png_chunk_positions(input_stream)?;
let existing_c2pa = ps.iter().find(|pcp| pcp.name == CAI_CHUNK);
input_stream.rewind()?;
match existing_c2pa {
Some(c2pa) => {
patch_stream(
input_stream,
output_stream,
c2pa.start,
c2pa.end() - c2pa.start,
&[],
)?;
}
None => {
io::copy(input_stream, output_stream)?;
}
}
Ok(())
}
}
impl AssetIO for PngIO {
fn new(_asset_type: &str) -> Self
where
Self: Sized,
{
PngIO {}
}
fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> {
Box::new(PngIO::new(asset_type))
}
fn get_reader(&self) -> &dyn C2paReader {
self
}
fn get_writer(&self, asset_type: &str) -> Option<Box<dyn C2paWriter>> {
Some(Box::new(PngIO::new(asset_type)))
}
fn remote_manifest_url_ref(&self) -> Option<&dyn RemoteManifestUrl> {
Some(self)
}
fn write_xmp_ref(&self) -> Option<&dyn WriteXmp> {
Some(self)
}
fn asset_box_hash_ref(&self) -> Option<&dyn AssetBoxHash> {
Some(self)
}
fn composed_data_ref(&self) -> Option<&dyn ComposedManifestRef> {
Some(self)
}
fn supported_types(&self) -> &[&str] {
&SUPPORTED_TYPES
}
}
fn get_xmp_insertion_point(asset_reader: &mut dyn ReadSeek) -> Option<(u64, u32)> {
let ps = get_png_chunk_positions(asset_reader).ok()?;
let xmp_box = ps.iter().find(|pcp| {
if pcp.name == ITXT_CHUNK {
if asset_reader.seek(SeekFrom::Start(pcp.start + 8)).is_err() {
return false;
}
if let Ok((key, _consumed)) = read_string(asset_reader, pcp.length) {
if key.is_empty() || key.len() > 79 {
return false;
}
if key == XMP_KEY {
return true;
}
}
false
} else {
false
}
});
if let Some(xmp) = xmp_box {
Some((xmp.start, xmp.length.checked_add(PNG_HDR_LEN as u32)?))
} else {
ps.iter()
.find(|png_cp| png_cp.name == IMG_HDR)
.map(|img_hdr| (img_hdr.end(), 0))
}
}
impl WriteXmp for PngIO {
fn write_xmp(
&self,
input_stream: &mut dyn ReadSeek,
output_stream: &mut dyn ReadWriteSeek,
xmp: &str,
) -> Result<()> {
input_stream.rewind()?;
let mut xmp_data = Vec::new();
let mut xmp_encoder = png_pong::Encoder::new(&mut xmp_data).into_chunk_enc();
let mut xmp_chunk = png_pong::chunk::Chunk::InternationalText(InternationalText {
key: XMP_KEY.to_string(),
langtag: "".to_string(),
transkey: "".to_string(),
val: xmp.to_string(),
compressed: false,
});
xmp_encoder
.encode(&mut xmp_chunk)
.map_err(|_| Error::EmbeddingError)?;
if let Some((xmp_start, xmp_len)) = get_xmp_insertion_point(input_stream) {
output_stream.rewind()?;
patch_stream(
input_stream,
output_stream,
xmp_start,
xmp_len as u64,
&xmp_data,
)?;
Ok(())
} else {
Err(Error::EmbeddingError)
}
}
}
fn classify_png_allowed_exclusions(name: &str, range_len: u64) -> Vec<AllowedExclusion> {
match name {
C2PA_BOXHASH => vec![AllowedExclusion::whole_box(range_len)],
"eXIf" | "iTXt" | "tEXt" | "zTXt" => vec![AllowedExclusion::after_header(8, range_len)],
_ => Vec::new(),
}
}
impl AssetBoxHash for PngIO {
fn get_box_map(&self, input_stream: &mut dyn ReadSeek) -> Result<Vec<BoxMap>> {
input_stream.rewind()?;
let ps = get_png_chunk_positions(input_stream)?;
let has_c2pa = ps.iter().any(|pc| pc.name == CAI_CHUNK);
let mut box_maps = Vec::new();
box_maps.push(
BoxMap::new(vec!["PNGh".to_string()], 0, 8)
.with_allowed_exclusions(classify_png_allowed_exclusions("PNGh", 8)),
);
for pc in ps.into_iter() {
if pc.name == CAI_CHUNK {
let range_len = pc.length as u64 + 12; box_maps.push(
BoxMap::new(vec![C2PA_BOXHASH.to_string()], pc.start, range_len)
.with_allowed_exclusions(classify_png_allowed_exclusions(
C2PA_BOXHASH,
range_len,
)),
);
continue;
}
let chunk_end = pc.end(); let is_ihdr = pc.name == IMG_HDR;
let range_len = pc.length as u64 + 12; let allowed_exclusions = classify_png_allowed_exclusions(&pc.name_str, range_len);
box_maps.push(
BoxMap::new(vec![pc.name_str], pc.start, range_len)
.with_allowed_exclusions(allowed_exclusions),
);
if !has_c2pa && is_ihdr {
box_maps.push(
BoxMap::new(vec![C2PA_BOXHASH.to_string()], chunk_end, 0)
.excluded()
.with_allowed_exclusions(classify_png_allowed_exclusions(C2PA_BOXHASH, 0)),
);
}
}
Ok(box_maps)
}
}
impl ComposedManifestRef for PngIO {
fn compose_manifest(&self, manifest_data: &[u8], _format: &str) -> Result<Vec<u8>> {
let mut cai_data = Vec::new();
let mut cai_encoder = png_pong::Encoder::new(&mut cai_data).into_chunk_enc();
let cai_unknown = png_pong::chunk::Unknown {
name: CAI_CHUNK,
data: manifest_data.to_vec(),
};
let mut cai_chunk = png_pong::chunk::Chunk::Unknown(cai_unknown);
cai_encoder
.encode(&mut cai_chunk)
.map_err(|_| Error::EmbeddingError)?;
Ok(cai_data)
}
}
#[cfg(test)]
#[allow(clippy::panic)]
#[allow(clippy::unwrap_used)]
pub mod tests {
use std::io::{Cursor, Write};
use memchr::memmem;
use super::*;
use crate::{
assertions::ExclusionKind,
utils::{
io_utils::tempdirectory,
test::{self, temp_dir_path},
},
};
#[test]
fn test_classify_png_allowed_exclusions() {
assert_eq!(
classify_png_allowed_exclusions(C2PA_BOXHASH, 20),
vec![AllowedExclusion {
start: 0,
length: 20,
kind: ExclusionKind::ManifestOrPadding,
}]
);
assert_eq!(
classify_png_allowed_exclusions("eXIf", 20),
vec![AllowedExclusion {
start: 8,
length: 12,
kind: ExclusionKind::AssetMetadata,
}]
);
assert_eq!(
classify_png_allowed_exclusions("tEXt", 20),
vec![AllowedExclusion {
start: 8,
length: 12,
kind: ExclusionKind::AssetMetadata,
}]
);
assert!(classify_png_allowed_exclusions("IHDR", 20).is_empty());
assert!(classify_png_allowed_exclusions("PNGh", 8).is_empty());
assert!(classify_png_allowed_exclusions("pHYs", 20).is_empty());
}
#[test]
fn test_png_xmp() {
let ap = test::fixture_path("libpng-test_with_url.png");
let png_io = PngIO {};
let xmp = png_io
.read_xmp(&mut std::fs::File::open(ap).unwrap())
.unwrap();
let provenance = crate::utils::xmp_inmemory_utils::extract_provenance(&xmp).unwrap();
assert!(provenance.contains("libpng-test"));
}
#[test]
fn test_png_xmp_write() {
let ap = test::fixture_path("libpng-test.png");
let mut source_stream = std::fs::File::open(ap).unwrap();
let temp_dir = tempdirectory().unwrap();
let output = temp_dir_path(&temp_dir, "out.png");
let mut output_stream = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(output)
.unwrap();
let png_io = PngIO {};
let eh = png_io.remote_manifest_url_ref().unwrap();
eh.write_remote_manifest_url(&mut source_stream, &mut output_stream, "some test data")
.unwrap();
output_stream.rewind().unwrap();
let new_xmp = png_io.read_xmp(&mut output_stream).unwrap();
let provenance = crate::utils::xmp_inmemory_utils::extract_provenance(&new_xmp).unwrap();
assert!(provenance.contains("some test data"));
}
#[test]
fn test_png_parse() {
let ap = test::fixture_path("libpng-test.png");
let png_bytes = std::fs::read(&ap).unwrap();
let mut f = std::fs::File::open(ap).unwrap();
let positions = get_png_chunk_positions(&mut f).unwrap();
for hop in positions {
if let Some(start) = memmem::find(&png_bytes, &hop.name) {
if hop.start != (start - 4) as u64 {
panic!("find_bytes found the wrong position");
}
println!(
"Chunk {} position matches, start: {}, length: {} ",
hop.name_str, hop.start, hop.length
);
}
}
}
#[test]
fn test_write_cai_using_stream_existing_cai_data() {
let source = include_bytes!("../../tests/fixtures/exp-test1.png");
let mut stream = Cursor::new(source.to_vec());
let png_io = PngIO {};
assert!(matches!(
png_io.read_c2pa(&mut stream),
Ok(data) if !data.is_empty(),
));
let output: Vec<u8> = Vec::new();
let mut output_stream = Cursor::new(output);
let data_to_write: Vec<u8> = vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34];
assert!(png_io
.write_c2pa(&mut stream, &mut output_stream, &data_to_write)
.is_ok());
let data_written = png_io.read_c2pa(&mut output_stream).unwrap();
assert_eq!(data_to_write, data_written);
}
#[test]
fn test_write_cai_using_stream_no_cai_data() {
let source = include_bytes!("../../tests/fixtures/libpng-test.png");
let mut stream = Cursor::new(source.to_vec());
let png_io = PngIO {};
assert!(matches!(
png_io.read_c2pa(&mut stream),
Err(Error::JumbfNotFound)
));
let output: Vec<u8> = Vec::new();
let mut output_stream = Cursor::new(output);
let data_to_write: Vec<u8> = vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34];
assert!(png_io
.write_c2pa(&mut stream, &mut output_stream, &data_to_write)
.is_ok());
let data_written = png_io.read_c2pa(&mut output_stream).unwrap();
assert_eq!(data_to_write, data_written);
}
#[test]
fn test_write_cai_data_to_stream_wrong_format() {
let source = include_bytes!("../../tests/fixtures/C.jpg");
let mut stream = Cursor::new(source.to_vec());
let png_io = PngIO {};
let output: Vec<u8> = Vec::new();
let mut output_stream = Cursor::new(output);
assert!(matches!(
png_io.write_c2pa(&mut stream, &mut output_stream, &[]),
Err(Error::InvalidAsset(_))
));
}
#[test]
fn test_stream_object_locations() {
let source = include_bytes!("../../tests/fixtures/exp-test1.png");
let mut stream = Cursor::new(source.to_vec());
let png_io = PngIO {};
let cai_pos = png_io
.get_object_locations(&mut stream)
.unwrap()
.into_iter()
.find(|pos| pos.htype == ObjectType::C2pa)
.unwrap();
assert_eq!(cai_pos.offset, 33);
assert_eq!(cai_pos.length, 3439701);
}
#[test]
fn test_stream_object_locations_with_incorrect_file_type() {
let source = include_bytes!("../../tests/fixtures/unsupported_type.txt");
let mut stream = Cursor::new(source.to_vec());
let png_io = PngIO {};
assert!(matches!(
png_io.get_object_locations(&mut stream),
Err(Error::InvalidAsset(_))
));
}
#[test]
fn test_stream_object_locations_adds_offsets_to_file_without_claims() {
let source = include_bytes!("../../tests/fixtures/libpng-test.png");
let mut stream = Cursor::new(source.to_vec());
let png_io = PngIO {};
assert!(png_io
.get_object_locations(&mut stream)
.unwrap()
.into_iter()
.any(|chunk| chunk.htype == ObjectType::C2pa));
}
#[test]
fn test_remove_c2pa() {
let source = test::fixture_path("exp-test1.png");
let temp_dir = tempdirectory().unwrap();
let output = test::temp_dir_path(&temp_dir, "exp-test1_tmp.png");
std::fs::copy(source, &output).unwrap();
let png_io = PngIO {};
png_io.remove_c2pa_store(&output).unwrap();
let mut file_reader = std::fs::File::open(&output).unwrap();
match png_io.read_c2pa(&mut file_reader) {
Err(Error::JumbfNotFound) => (),
_ => unreachable!(),
}
}
#[test]
fn test_remove_c2pa_from_stream() {
let source = crate::utils::test::fixture_path("exp-test1.png");
let source_bytes = std::fs::read(source).unwrap();
let mut source_stream = Cursor::new(source_bytes);
let png_io = PngIO {};
let png_writer = png_io.get_writer("png").unwrap();
let output_bytes = Vec::new();
let mut output_stream = Cursor::new(output_bytes);
png_writer
.remove_c2pa(&mut source_stream, &mut output_stream)
.unwrap();
let png_reader = png_io.get_reader();
match png_reader.read_c2pa(&mut output_stream) {
Err(Error::JumbfNotFound) => (),
_ => unreachable!(),
}
}
#[test]
fn test_cai_chunk_length_near_u32_max_returns_error() {
let mut data: Vec<u8> = Vec::new();
data.extend_from_slice(&PNG_ID);
let ihdr_payload: [u8; 13] = [0, 0, 0, 1, 0, 0, 0, 1, 8, 2, 0, 0, 0];
data.extend_from_slice(&(ihdr_payload.len() as u32).to_be_bytes());
data.extend_from_slice(b"IHDR");
data.extend_from_slice(&ihdr_payload);
data.extend_from_slice(&[0x90, 0x77, 0x53, 0xde]); data.extend_from_slice(&(u32::MAX - 11).to_be_bytes());
data.extend_from_slice(b"caBX");
let png_io = PngIO {};
let mut stream = Cursor::new(data);
assert!(png_io.get_object_locations(&mut stream).is_err());
}
#[test]
fn test_embeddable_manifest() {
let png_io = PngIO {};
let source = crate::utils::test::fixture_path("exp-test1.png");
let mut source_reader = std::fs::File::open(&source).unwrap();
let ol = png_io.get_object_locations(&mut source_reader).unwrap();
let cai_loc = ol.iter().find(|o| o.htype == ObjectType::C2pa).unwrap();
let curr_manifest = png_io.read_c2pa(&mut source_reader).unwrap();
let temp_dir = tempdirectory().unwrap();
let output = crate::utils::test::temp_dir_path(&temp_dir, "exp-test1-out.png");
std::fs::copy(source, &output).unwrap();
png_io.remove_c2pa_store(&output).unwrap();
let em = png_io
.composed_data_ref()
.unwrap()
.compose_manifest(&curr_manifest, "png")
.unwrap();
let outbuf = Vec::new();
let mut out_stream = Cursor::new(outbuf);
let mut before = vec![0u8; usize::try_from(cai_loc.offset).unwrap()];
let mut in_file = std::fs::File::open(&output).unwrap();
in_file.read_exact(before.as_mut_slice()).unwrap();
out_stream.write_all(&before).unwrap();
out_stream.write_all(&em).unwrap();
let mut after_buf = Vec::new();
in_file.read_to_end(&mut after_buf).unwrap();
out_stream.write_all(&after_buf).unwrap();
out_stream.rewind().unwrap();
let restored_manifest = png_io.read_c2pa(&mut out_stream).unwrap();
assert_eq!(&curr_manifest, &restored_manifest);
}
#[test]
fn test_read_xmp_malformed_itxt_does_not_underflow() {
let mut data: Vec<u8> = Vec::new();
data.extend_from_slice(&PNG_ID);
let ihdr_payload: [u8; 13] = [0, 0, 0, 1, 0, 0, 0, 1, 8, 2, 0, 0, 0];
data.extend_from_slice(&(ihdr_payload.len() as u32).to_be_bytes());
data.extend_from_slice(b"IHDR");
data.extend_from_slice(&ihdr_payload);
data.extend_from_slice(&[0x90, 0x77, 0x53, 0xde]);
let itxt_len: u32 = 18;
data.extend_from_slice(&itxt_len.to_be_bytes());
data.extend_from_slice(b"iTXt");
data.extend_from_slice(b"XML:com.adobe.xmp\0"); data.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
data.extend_from_slice(&0u32.to_be_bytes());
data.extend_from_slice(b"IEND");
data.extend_from_slice(&[0xae, 0x42, 0x60, 0x82]);
let png_io = PngIO {};
let mut stream = Cursor::new(data);
assert!(
png_io.read_xmp(&mut stream).is_none(),
"malformed iTxt must not yield an XMP string"
);
}
#[test]
fn test_read_xmp_itxt_shorter_than_key_does_not_underflow() {
let mut data: Vec<u8> = Vec::new();
data.extend_from_slice(&PNG_ID);
let ihdr_payload: [u8; 13] = [0, 0, 0, 1, 0, 0, 0, 1, 8, 2, 0, 0, 0];
data.extend_from_slice(&(ihdr_payload.len() as u32).to_be_bytes());
data.extend_from_slice(b"IHDR");
data.extend_from_slice(&ihdr_payload);
data.extend_from_slice(&[0x90, 0x77, 0x53, 0xde]);
let itxt_len: u32 = 5;
data.extend_from_slice(&itxt_len.to_be_bytes());
data.extend_from_slice(b"iTXt");
data.extend_from_slice(b"XML:c"); data.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]);
data.extend_from_slice(&0u32.to_be_bytes());
data.extend_from_slice(b"IEND");
data.extend_from_slice(&[0xae, 0x42, 0x60, 0x82]);
let png_io = PngIO {};
let mut stream = Cursor::new(data);
assert!(png_io.read_xmp(&mut stream).is_none());
}
}