commit c7eb0b0dbdf695f1a0b7b5341ffe9a9fc42f1b12
parent ad1075c2d1b5dce02ec234487f82326e60f56303
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date: Thu, 22 Sep 2022 19:01:53 -0400
(MINOR) Add support for manifest removal (#123)
* Support for manifest removal.
* Unit test changes
* Address PR comments
Diffstat:
7 files changed, 297 insertions(+), 46 deletions(-)
diff --git a/sdk/src/asset_handlers/bmff_io.rs b/sdk/src/asset_handlers/bmff_io.rs
@@ -509,7 +509,7 @@ fn adjust_stco_and_co64<W: Write + CAIRead>(
for stco_token in stco_list {
let stco_box_info = &bmff_tree[*stco_token].data;
if stco_box_info.box_type != BoxType::StcoBox {
- return Err(Error::BadParam("Bad BMFF".to_string()));
+ return Err(Error::InvalidAsset("Bad BMFF".to_string()));
}
// read stco box and patch
@@ -517,9 +517,9 @@ fn adjust_stco_and_co64<W: Write + CAIRead>(
// read header
let header = BoxHeaderLite::read(output)
- .map_err(|_err| Error::BadParam("Bad BMFF".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("Bad BMFF".to_string()))?;
if header.name != BoxType::StcoBox {
- return Err(Error::BadParam("Bad BMFF".to_string()));
+ return Err(Error::InvalidAsset("Bad BMFF".to_string()));
}
// read extended header
@@ -536,12 +536,12 @@ fn adjust_stco_and_co64<W: Write + CAIRead>(
let new_offset = if adjust < 0 {
offset
- u32::try_from(adjust.abs()).map_err(|_| {
- Error::BadParam("Bad BMFF offset adjustment".to_string())
+ Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
})?
} else {
offset
+ u32::try_from(adjust).map_err(|_| {
- Error::BadParam("Bad BMFF offset adjustment".to_string())
+ Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
})?
};
entries.push(new_offset);
@@ -560,7 +560,7 @@ fn adjust_stco_and_co64<W: Write + CAIRead>(
for co64_token in co64_list {
let co64_box_info = &bmff_tree[*co64_token].data;
if co64_box_info.box_type != BoxType::Co64Box {
- return Err(Error::BadParam("Bad BMFF".to_string()));
+ return Err(Error::InvalidAsset("Bad BMFF".to_string()));
}
// read co64 box and patch
@@ -568,9 +568,9 @@ fn adjust_stco_and_co64<W: Write + CAIRead>(
// read header
let header = BoxHeaderLite::read(output)
- .map_err(|_err| Error::BadParam("Bad BMFF".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("Bad BMFF".to_string()))?;
if header.name != BoxType::Co64Box {
- return Err(Error::BadParam("Bad BMFF".to_string()));
+ return Err(Error::InvalidAsset("Bad BMFF".to_string()));
}
// read extended header
@@ -587,12 +587,12 @@ fn adjust_stco_and_co64<W: Write + CAIRead>(
let new_offset = if adjust < 0 {
offset
- u64::try_from(adjust.abs()).map_err(|_| {
- Error::BadParam("Bad BMFF offset adjustment".to_string())
+ Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
})?
} else {
offset
+ u64::try_from(adjust).map_err(|_| {
- Error::BadParam("Bad BMFF offset adjustment".to_string())
+ Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
})?
};
entries.push(new_offset);
@@ -625,8 +625,8 @@ pub(crate) fn build_bmff_tree(
let mut current = start;
while current < end {
// Get box header.
- let header =
- BoxHeaderLite::read(reader).map_err(|_err| Error::BadParam("Bad BMFF".to_string()))?;
+ let header = BoxHeaderLite::read(reader)
+ .map_err(|_err| Error::InvalidAsset("Bad BMFF".to_string()))?;
// Break if size zero BoxHeader
let s = header.size;
@@ -710,7 +710,7 @@ pub(crate) fn build_bmff_tree(
let new_token = bmff_tree.new_node(b);
current_node
.append_node(bmff_tree, new_token)
- .map_err(|_err| Error::BadParam("Bad BMFF Graph".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("Bad BMFF Graph".to_string()))?;
let path = path_from_token(bmff_tree, &new_token)?;
add_token_to_cache(bmff_path_map, path, new_token);
@@ -965,16 +965,16 @@ impl AssetIO for BmffIO {
let (start, end) = if let Some(c2pa_length) = c2pa_length {
let start = usize::value_from(c2pa_start)
- .map_err(|_err| Error::BadParam("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label
let end = usize::value_from(c2pa_start + c2pa_length)
- .map_err(|_err| Error::BadParam("value out of range".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
(start, end)
} else {
// insert new c2pa
let end = usize::value_from(c2pa_start)
- .map_err(|_err| Error::BadParam("value out of range".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
(end, end)
};
@@ -992,7 +992,7 @@ impl AssetIO for BmffIO {
let offset_adjust: i32 = if end == 0 {
new_c2pa_box_size as i32
} else {
- // value could be negative is box is truncated
+ // value could be negative if box is truncated
let existing_c2pa_box_size = end - start;
let pad_size: i32 = new_c2pa_box_size as i32 - existing_c2pa_box_size as i32;
pad_size
@@ -1041,7 +1041,7 @@ impl AssetIO for BmffIO {
&mut output_bmff_map,
)?;
- // adjust based on current layyout
+ // adjust based on current layout
adjust_stco_and_co64(
&mut temp_file,
&output_bmff_tree,
@@ -1066,6 +1066,135 @@ impl AssetIO for BmffIO {
let vec: Vec<HashObjectPositions> = Vec::new();
Ok(vec)
}
+
+ fn remove_cai_store(&self, asset_path: &Path) -> Result<()> {
+ let mut input = File::open(asset_path)?;
+ let size = input.seek(SeekFrom::End(0))?;
+ input.seek(SeekFrom::Start(0))?;
+
+ // create root node
+ let root_box = BoxInfo {
+ path: "".to_string(),
+ offset: 0,
+ size: size as u64,
+ box_type: BoxType::Empty,
+ parent: None,
+ user_type: None,
+ version: None,
+ flags: None,
+ };
+
+ let (mut bmff_tree, root_token) = Arena::with_data(root_box);
+ let mut bmff_map: HashMap<String, Vec<Token>> = HashMap::new();
+
+ // build layout of the BMFF structure
+ build_bmff_tree(
+ &mut input,
+ size as u64,
+ &mut bmff_tree,
+ &root_token,
+ &mut bmff_map,
+ )?;
+
+ // get position of c2pa manifest
+ let (c2pa_start, c2pa_length) =
+ if let Some(c2pa_token) = get_manifest_token(&bmff_tree, &bmff_map) {
+ let uuid_info = &bmff_tree[c2pa_token].data;
+
+ (uuid_info.offset, Some(uuid_info.size))
+ } else {
+ return Ok(()); // no box to remove
+ };
+
+ let mut temp_file = Builder::new()
+ .prefix("c2pa_temp")
+ .rand_bytes(5)
+ .tempfile()?;
+
+ let (start, end) = if let Some(c2pa_length) = c2pa_length {
+ let start = usize::value_from(c2pa_start)
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label
+
+ let end = usize::value_from(c2pa_start + c2pa_length)
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
+
+ (start, end)
+ } else {
+ return Err(Error::InvalidAsset("value out of range".to_string()));
+ };
+
+ // write content before ContentProvenanceBox
+ input.seek(SeekFrom::Start(0))?;
+ let mut b = vec![0u8; start];
+ input.read_exact(&mut b)?;
+ temp_file.write_all(&b)?;
+
+ // calc offset adjustments
+ // value will be negative since the box is truncated
+ let new_c2pa_box_size: i32 = 0;
+ let existing_c2pa_box_size = end - start;
+ let offset_adjust = new_c2pa_box_size - existing_c2pa_box_size as i32;
+
+ // write content after ContentProvenanceBox
+ input.seek(SeekFrom::Start(end as u64))?;
+ let mut chunk = vec![0u8; 1024 * 1024];
+ loop {
+ let len = match input.read(&mut chunk) {
+ Ok(0) => break,
+ Ok(len) => len,
+ Err(e) => return Err(Error::IoError(e)),
+ };
+
+ temp_file.write_all(&chunk[0..len])?;
+ }
+ temp_file.flush()?;
+
+ // Manipulating the UUID box means we may need some patch offsets if they are file absolute offsets.
+ match self.bmff_format.as_ref() {
+ "m4a" | "mp4" | "mov" => {
+ // create root node
+ let root_box = BoxInfo {
+ path: "".to_string(),
+ offset: 0,
+ size: size as u64,
+ box_type: BoxType::Empty,
+ parent: None,
+ user_type: None,
+ version: None,
+ flags: None,
+ };
+
+ // rebuild box layout for output file
+ let (mut output_bmff_tree, root_token) = Arena::with_data(root_box);
+ let mut output_bmff_map: HashMap<String, Vec<Token>> = HashMap::new();
+
+ let size = temp_file.seek(SeekFrom::End(0))?;
+ temp_file.seek(SeekFrom::Start(0))?;
+ build_bmff_tree(
+ &mut temp_file,
+ size as u64,
+ &mut output_bmff_tree,
+ &root_token,
+ &mut output_bmff_map,
+ )?;
+
+ // adjust based on current layout
+ adjust_stco_and_co64(
+ &mut temp_file,
+ &output_bmff_tree,
+ &output_bmff_map,
+ offset_adjust,
+ )?;
+ }
+ _ => (), // todo: handle more patching cases as necessary
+ }
+
+ // copy temp file to asset
+ std::fs::rename(&temp_file.path(), asset_path)
+ // if rename fails, try to copy in case we are on different volumes
+ .or_else(|_| std::fs::copy(&temp_file.path(), asset_path).and(Ok(())))
+ .map_err(Error::IoError)
+ }
}
impl AssetPatch for BmffIO {
@@ -1120,7 +1249,7 @@ impl AssetPatch for BmffIO {
(0, None)
}
} else {
- return Err(Error::BadParam(
+ return Err(Error::InvalidAsset(
"patch_cai_store found no manifest store to patch.".to_string(),
));
};
@@ -1136,12 +1265,12 @@ impl AssetPatch for BmffIO {
asset.write_all(&new_c2pa_box)?;
Ok(())
} else {
- Err(Error::BadParam(
+ Err(Error::InvalidAsset(
"patch_cai_store store size mismatch.".to_string(),
))
}
} else {
- Err(Error::BadParam(
+ Err(Error::InvalidAsset(
"patch_cai_store store size mismatch.".to_string(),
))
}
@@ -1257,4 +1386,23 @@ pub mod tests {
}
assert!(success)
}
+
+ #[test]
+ fn test_remove_c2pa() {
+ let source = fixture_path("video1.mp4");
+
+ let temp_dir = tempdir().unwrap();
+ let output = temp_dir_path(&temp_dir, "mp4_test.mp4");
+
+ std::fs::copy(&source, &output).unwrap();
+ let bmff_io = BmffIO::new("mp4");
+
+ bmff_io.remove_cai_store(&output).unwrap();
+
+ // read back in asset, JumbfNotFound is expected since it was removed
+ match bmff_io.read_cai_store(&output) {
+ Err(Error::JumbfNotFound) => (),
+ _ => unreachable!(),
+ }
+ }
}
diff --git a/sdk/src/asset_handlers/c2pa_io.rs b/sdk/src/asset_handlers/c2pa_io.rs
@@ -60,6 +60,10 @@ impl AssetIO for C2paIO {
Ok(vec![hop])
}
+
+ fn remove_cai_store(&self, _asset_path: &Path) -> Result<()> {
+ Ok(())
+ }
}
#[cfg(test)]
diff --git a/sdk/src/asset_handlers/jpeg_io.rs b/sdk/src/asset_handlers/jpeg_io.rs
@@ -70,7 +70,7 @@ fn xmp_from_bytes(asset_bytes: &[u8]) -> Option<String> {
fn add_required_segs(asset_path: &std::path::Path) -> Result<()> {
let buf = read(asset_path)?;
let dimg_opt = DynImage::from_bytes(buf.into())
- .map_err(|_err| Error::BadParam("Could not parse input image".to_owned()))?;
+ .map_err(|_err| Error::InvalidAsset("Could not parse input JPEG".to_owned()))?;
if let Some(DynImage::Jpeg(jpeg)) = dimg_opt {
// check for JUMBF Seg
@@ -83,9 +83,7 @@ fn add_required_segs(asset_path: &std::path::Path) -> Result<()> {
aio.save_cai_store(asset_path, &no_bytes)?;
}
} else {
- return Err(Error::BadParam(
- "Image type not supported by handler".to_owned(),
- ));
+ return Err(Error::UnsupportedType);
}
Ok(())
@@ -154,7 +152,7 @@ impl CAILoader for JpegIO {
asset_reader.read_to_end(&mut buf).map_err(Error::IoError)?;
let dimg_opt = DynImage::from_bytes(buf.into())
- .map_err(|_err| Error::BadParam("Could not parse input image".to_owned()))?;
+ .map_err(|_err| Error::InvalidAsset("Could not parse input JPEG".to_owned()))?;
if let Some(dimg) = dimg_opt {
match dimg {
@@ -206,12 +204,10 @@ impl CAILoader for JpegIO {
}
}
}
- _ => return Err(Error::BadParam("Unknown image format".to_owned())),
+ _ => return Err(Error::InvalidAsset("Unknown image format".to_owned())),
};
} else {
- return Err(Error::BadParam(
- "Image type not supported by handler".to_owned(),
- ));
+ return Err(Error::UnsupportedType);
}
if buffer.is_empty() {
@@ -300,7 +296,7 @@ impl AssetIO for JpegIO {
jpeg.encoder()
.write_to(output)
- .map_err(|_err| Error::BadParam("JPEG write error".to_owned()))?;
+ .map_err(|_err| Error::InvalidAsset("JPEG write error".to_owned()))?;
Ok(())
}
@@ -399,11 +395,34 @@ impl AssetIO for JpegIO {
curr_offset += seg.len_with_entropy();
}
}
- _ => return Err(Error::BadParam("Unknown image format".to_owned())),
+ _ => return Err(Error::InvalidAsset("Unknown image format".to_owned())),
}
Ok(positions)
}
+
+ fn remove_cai_store(&self, asset_path: &std::path::Path) -> Result<()> {
+ let input = read(asset_path).map_err(wrap_io_err)?;
+
+ let mut jpeg = Jpeg::from_bytes(input.into()).map_err(|_err| Error::EmbeddingError)?;
+
+ // remove existing CAI segments
+ delete_cai_segments(&mut jpeg)?;
+
+ // save updated file
+ let output = std::fs::OpenOptions::new()
+ .read(true)
+ .write(true)
+ .truncate(true)
+ .open(asset_path)
+ .map_err(Error::IoError)?;
+
+ jpeg.encoder()
+ .write_to(output)
+ .map_err(|_err| Error::InvalidAsset("JPEG write error".to_owned()))?;
+
+ Ok(())
+ }
}
#[cfg(test)]
@@ -431,4 +450,23 @@ pub mod tests {
let result = extract_xmp(&seg);
assert_eq!(result, None);
}
+
+ #[test]
+ fn test_remove_c2pa() {
+ let source = crate::utils::test::fixture_path("CA.jpg");
+
+ let temp_dir = tempfile::tempdir().unwrap();
+ let output = crate::utils::test::temp_dir_path(&temp_dir, "CA_test.jpg");
+
+ std::fs::copy(&source, &output).unwrap();
+ let jpeg_io = JpegIO {};
+
+ jpeg_io.remove_cai_store(&output).unwrap();
+
+ // read back in asset, JumbfNotFound is expected since it was removed
+ match jpeg_io.read_cai_store(&output) {
+ Err(Error::JumbfNotFound) => (),
+ _ => unreachable!(),
+ }
+ }
}
diff --git a/sdk/src/asset_handlers/png_io.rs b/sdk/src/asset_handlers/png_io.rs
@@ -59,9 +59,9 @@ fn get_png_chunk_positions(f: &mut dyn CAIRead) -> Result<Vec<PngChunkPos>> {
// check PNG signature
f.read_exact(&mut hdr)
- .map_err(|_err| Error::BadParam("PNG invalid".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("PNG invalid".to_string()))?;
if hdr != PNG_ID {
- return Err(Error::BadParam("PNG invalid".to_string()));
+ return Err(Error::InvalidAsset("PNG invalid".to_string()));
}
loop {
@@ -70,23 +70,23 @@ fn get_png_chunk_positions(f: &mut dyn CAIRead) -> Result<Vec<PngChunkPos>> {
// read the chunk length
let length = f
.read_u32::<BigEndian>()
- .map_err(|_err| Error::BadParam("PNG out of range".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?;
// read the chunk type
f.read_exact(&mut buf4)
- .map_err(|_err| Error::BadParam("PNG out of range".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?;
let name = buf4;
// seek past data
f.seek(SeekFrom::Current(length as i64))
- .map_err(|_err| Error::BadParam("PNG out of range".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?;
// read crc
f.read_exact(&mut buf4)
- .map_err(|_err| Error::BadParam("PNG out of range".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?;
let chunk_name = String::from_utf8(name.to_vec())
- .map_err(|_err| Error::BadParam("PNG bad chunk name".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("PNG bad chunk name".to_string()))?;
let pcp = PngChunkPos {
start: current_pos,
@@ -131,7 +131,7 @@ fn get_cai_data(f: &mut dyn CAIRead) -> Result<Vec<u8>> {
let mut data: Vec<u8> = vec![0; length];
f.read_exact(&mut data[..])
- .map_err(|_err| Error::BadParam("PNG out of range".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("PNG out of range".to_string()))?;
Ok(data)
}
@@ -303,7 +303,7 @@ impl AssetIO for PngIO {
/* splice in new chunk. Each PNG chunk has the following format:
chunk data length (4 bytes big endian)
chunk identifier (4 byte character sequence)
- chunk data (0 - n bytes of chunck data)
+ chunk data (0 - n bytes of chunk data)
chunk crc (4 bytes in crc in format defined in PNG spec)
*/
@@ -313,10 +313,10 @@ impl AssetIO for PngIO {
if let Some(existing_cai) = iter.find(|pcp| pcp.name == CAI_CHUNK) {
// replace existing CAI
let start = usize::value_from(existing_cai.start)
- .map_err(|_err| Error::BadParam("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label
let end = usize::value_from(existing_cai.end())
- .map_err(|_err| Error::BadParam("value out of range".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
png_buf.splice(start..end, empty_buf.iter().cloned());
}
@@ -330,7 +330,7 @@ impl AssetIO for PngIO {
// add new cai data after image header chunk
if let Some(img_hdr) = iter.find(|pcp| pcp.name == IMG_HDR) {
let end = usize::value_from(img_hdr.end())
- .map_err(|_err| Error::BadParam("value out of range".to_string()))?;
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
png_buf.splice(end..end, cai_data.iter().cloned());
} else {
@@ -338,8 +338,7 @@ impl AssetIO for PngIO {
}
// save png data
- std::fs::write(asset_path, png_buf)
- .map_err(|_err| Error::BadParam("PNG write error".to_owned()))?;
+ std::fs::write(asset_path, png_buf)?;
Ok(())
}
@@ -384,6 +383,43 @@ impl AssetIO for PngIO {
Ok(positions)
}
+
+ fn remove_cai_store(&self, asset_path: &Path) -> Result<()> {
+ // get png byte
+ let mut png_buf = std::fs::read(asset_path).map_err(|_err| Error::EmbeddingError)?;
+
+ let mut cursor = Cursor::new(png_buf);
+ let ps = get_png_chunk_positions(&mut cursor)?;
+
+ // get back buffer
+ png_buf = cursor.into_inner();
+
+ /* splice in new chunk. Each PNG chunk has the following format:
+ chunk data length (4 bytes big endian)
+ chunk identifier (4 byte character sequence)
+ chunk data (0 - n bytes of chunk data)
+ chunk crc (4 bytes in crc in format defined in PNG spec)
+ */
+
+ // erase existing
+ let empty_buf = Vec::new();
+ let mut iter = ps.into_iter();
+ if let Some(existing_cai) = iter.find(|pcp| pcp.name == CAI_CHUNK) {
+ // replace existing CAI
+ let start = usize::value_from(existing_cai.start)
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label
+
+ let end = usize::value_from(existing_cai.end())
+ .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;
+
+ png_buf.splice(start..end, empty_buf.iter().cloned());
+ }
+
+ // save png data
+ std::fs::write(asset_path, png_buf)?;
+
+ Ok(())
+ }
}
#[cfg(test)]
@@ -433,4 +469,23 @@ pub mod tests {
}
}
}
+
+ #[test]
+ fn test_remove_c2pa() {
+ let source = crate::utils::test::fixture_path("exp-test1.png");
+
+ let temp_dir = tempfile::tempdir().unwrap();
+ let output = crate::utils::test::temp_dir_path(&temp_dir, "exp-test1_tmp.png");
+
+ std::fs::copy(&source, &output).unwrap();
+ let png_io = PngIO {};
+
+ png_io.remove_cai_store(&output).unwrap();
+
+ // read back in asset, JumbfNotFound is expected since it was removed
+ match png_io.read_cai_store(&output) {
+ Err(Error::JumbfNotFound) => (),
+ _ => unreachable!(),
+ }
+ }
}
diff --git a/sdk/src/asset_io.rs b/sdk/src/asset_io.rs
@@ -70,6 +70,9 @@ pub trait AssetIO {
fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> {
None
}
+
+ // Remove entire CAI block from asset
+ fn remove_cai_store(&self, asset_path: &Path) -> Result<()>;
}
// `AssetPatch` optimizes output generation for asset_io handlers that
diff --git a/sdk/src/error.rs b/sdk/src/error.rs
@@ -216,6 +216,9 @@ pub enum Error {
#[error(transparent)]
InvalidClaim(#[from] crate::store::InvalidClaimError),
+ #[error("asset could not be parsed: {0}")]
+ InvalidAsset(String),
+
#[error(transparent)]
JumbfParseError(#[from] crate::jumbf::boxes::JumbfParseError),
diff --git a/sdk/tests/fixtures/exp-test1.png b/sdk/tests/fixtures/exp-test1.png
Binary files differ.