use std::{
collections::HashMap,
fs::{self, File},
io::{self, Read, Seek, SeekFrom},
path::{Path, PathBuf},
};
use tempfile::Builder;
use zip::{
result::ZipResult, write::SimpleFileOptions, CompressionMethod, HasZipMetadata, ZipArchive,
ZipWriter,
};
use crate::{
asset_io::{
self, AssetIO, CAIRead, CAIReadWrapper, CAIReadWrite, CAIReadWriteWrapper, CAIReader,
CAIWriter, HashObjectPositions,
},
error::Result,
Error, HashRange,
};
const MANIFEST_PATH: &str = "META-INF/content_credential.c2pa";
const CENTRAL_DIRECTORY_CRC_OFFSET: u64 = 16;
const CRC_LEN: u64 = 4;
const DATA_DESCRIPTOR_SIGNATURE: [u8; 4] = [0x50, 0x4b, 0x07, 0x08];
const LOCAL_FILE_HEADER_SIGNATURE: [u8; 4] = [0x50, 0x4b, 0x03, 0x04];
const CENTRAL_DIRECTORY_HEADER_SIGNATURE: [u8; 4] = [0x50, 0x4b, 0x01, 0x02];
pub struct ZipIO {}
impl CAIWriter for ZipIO {
fn write_cai(
&self,
input_stream: &mut dyn CAIRead,
output_stream: &mut dyn CAIReadWrite,
mut store_bytes: &[u8],
) -> Result<()> {
let mut writer = self
.writer(input_stream, output_stream)
.map_err(ZipError::Write)?;
match writer.add_directory("META-INF", SimpleFileOptions::DEFAULT) {
Err(zip::result::ZipError::InvalidArchive(err))
if err.starts_with("Duplicate filename") => {}
Err(source) => return Err(ZipError::Write(source).into()),
_ => {}
}
match writer.start_file_from_path(
Path::new(MANIFEST_PATH),
SimpleFileOptions::DEFAULT.compression_method(CompressionMethod::Stored),
) {
Err(zip::result::ZipError::InvalidArchive(err))
if err.starts_with("Duplicate filename") =>
{
writer.abort_file().map_err(ZipError::Write)?;
writer
.start_file_from_path(
Path::new(MANIFEST_PATH),
SimpleFileOptions::DEFAULT.compression_method(CompressionMethod::Stored),
)
.map_err(ZipError::Write)?;
}
Err(source) => return Err(ZipError::Write(source).into()),
_ => {}
}
io::copy(&mut store_bytes, &mut writer)?;
writer.finish().map_err(ZipError::Write)?;
Ok(())
}
fn get_object_locations_from_stream(
&self,
_input_stream: &mut dyn CAIRead,
) -> Result<Vec<HashObjectPositions>> {
Err(ZipError::ObjectLocationsUnsupported.into())
}
fn remove_cai_store_from_stream(
&self,
input_stream: &mut dyn CAIRead,
output_stream: &mut dyn CAIReadWrite,
) -> Result<()> {
let mut writer = self
.writer(input_stream, output_stream)
.map_err(ZipError::Remove)?;
match writer.start_file_from_path(Path::new(MANIFEST_PATH), SimpleFileOptions::default()) {
Err(zip::result::ZipError::InvalidArchive(err))
if err.starts_with("Duplicate filename") => {}
Err(source) => return Err(ZipError::Remove(source).into()),
_ => {}
}
writer.abort_file().map_err(ZipError::Remove)?;
writer.finish().map_err(ZipError::Remove)?;
Ok(())
}
}
impl CAIReader for ZipIO {
fn read_cai(&self, asset_reader: &mut dyn CAIRead) -> Result<Vec<u8>> {
let mut reader = self.reader(asset_reader).map_err(ZipError::Read)?;
let index = reader
.index_for_path(Path::new(MANIFEST_PATH))
.ok_or(Error::JumbfNotFound)?;
let mut file = reader.by_index(index).map_err(ZipError::Read)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
fn read_xmp(&self, _asset_reader: &mut dyn CAIRead) -> Option<String> {
None
}
}
impl AssetIO for ZipIO {
fn new(_asset_type: &str) -> Self
where
Self: Sized,
{
ZipIO {}
}
fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> {
Box::new(ZipIO::new(asset_type))
}
fn get_reader(&self) -> &dyn CAIReader {
self
}
fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> {
Some(Box::new(ZipIO::new(asset_type)))
}
fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> {
let mut f = File::open(asset_path)?;
self.read_cai(&mut f)
}
fn save_cai_store(&self, asset_path: &Path, store_bytes: &[u8]) -> Result<()> {
let mut stream = fs::OpenOptions::new()
.read(true)
.open(asset_path)
.map_err(Error::IoError)?;
let mut temp_file = Builder::new()
.prefix("c2pa_temp")
.rand_bytes(5)
.tempfile()?;
self.write_cai(&mut stream, &mut temp_file, store_bytes)?;
asset_io::rename_or_move(temp_file, asset_path)
}
fn get_object_locations(&self, _asset_path: &Path) -> Result<Vec<HashObjectPositions>> {
Err(ZipError::ObjectLocationsUnsupported.into())
}
fn remove_cai_store(&self, asset_path: &Path) -> Result<()> {
let mut stream = fs::OpenOptions::new()
.read(true)
.open(asset_path)
.map_err(Error::IoError)?;
let mut temp_file = Builder::new()
.prefix("c2pa_temp")
.rand_bytes(5)
.tempfile()?;
self.remove_cai_store_from_stream(&mut stream, &mut temp_file)?;
asset_io::rename_or_move(temp_file, asset_path)
}
fn supported_types(&self) -> &[&str] {
&[
"zip",
"application/x-zip",
"epub",
"application/epub+zip",
"docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"pptx",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"docm",
"application/vnd.ms-word.document.macroenabled.12",
"xlsm",
"application/vnd.ms-excel.sheet.macroenabled.12",
"pptm",
"application/vnd.ms-powerpoint.presentation.macroenabled.12",
"odt",
"application/vnd.oasis.opendocument.text",
"ods",
"application/vnd.oasis.opendocument.spreadsheet",
"odp",
"application/vnd.oasis.opendocument.presentation",
"odg",
"application/vnd.oasis.opendocument.graphics",
"ott",
"application/vnd.oasis.opendocument.text-template",
"ots",
"application/vnd.oasis.opendocument.spreadsheet-template",
"otp",
"application/vnd.oasis.opendocument.presentation-template",
"otg",
"application/vnd.oasis.opendocument.graphics-template",
"oxps",
"application/oxps",
]
}
}
impl ZipIO {
fn reader<'a>(
&self,
input_stream: &'a mut dyn CAIRead,
) -> ZipResult<ZipArchive<CAIReadWrapper<'a>>> {
ZipArchive::new(CAIReadWrapper {
reader: input_stream,
})
}
fn writer<'a>(
&self,
input_stream: &'a mut dyn CAIRead,
output_stream: &'a mut dyn CAIReadWrite,
) -> ZipResult<ZipWriter<CAIReadWriteWrapper<'a>>> {
input_stream.rewind()?;
io::copy(input_stream, output_stream)?;
ZipWriter::new_append(CAIReadWriteWrapper {
reader_writer: output_stream,
})
}
}
pub(crate) fn zip_central_directory_range<R>(reader: &mut R) -> Result<Vec<HashRange>>
where
R: Read + Seek + ?Sized,
{
let length = reader.seek(SeekFrom::End(0))?;
let mut reader = ZipArchive::new(reader).map_err(ZipError::Read)?;
let start = reader.central_directory_start();
let range = match reader.index_for_path(Path::new(MANIFEST_PATH)) {
Some(index) => {
let file = reader.by_index(index).map_err(ZipError::Read)?;
let crc_start = file.central_header_start() + CENTRAL_DIRECTORY_CRC_OFFSET;
vec![
HashRange::new(start, crc_start - start),
HashRange::new(crc_start + CRC_LEN, length - (crc_start + CRC_LEN)),
]
}
None => vec![HashRange::new(start, length - start)],
};
Ok(range)
}
pub(crate) fn zip_uri_ranges<R>(stream: &mut R) -> Result<HashMap<PathBuf, HashRange>>
where
R: Read + Seek + ?Sized,
{
let mut ranges = HashMap::new();
for entry in zip_uri_entries(stream)? {
if entry.path == Path::new(MANIFEST_PATH) {
continue;
}
let mut end = entry.data_end;
if entry.using_data_descriptor {
stream.seek(SeekFrom::Start(entry.data_end))?;
let mut signature = [0; DATA_DESCRIPTOR_SIGNATURE.len()];
stream.read_exact(&mut signature)?;
if signature != LOCAL_FILE_HEADER_SIGNATURE
&& signature != CENTRAL_DIRECTORY_HEADER_SIGNATURE
{
let signature_len: u64 = if signature == DATA_DESCRIPTOR_SIGNATURE {
DATA_DESCRIPTOR_SIGNATURE.len() as u64
} else {
0
};
let size_field_len: u64 = if entry.large_file { 8 } else { 4 };
end += signature_len + CRC_LEN + (2 * size_field_len);
}
}
ranges.insert(
entry.path,
HashRange::new(entry.header_start, end - entry.header_start),
);
}
Ok(ranges)
}
struct ZipUriEntry {
path: PathBuf,
header_start: u64,
data_end: u64,
using_data_descriptor: bool,
large_file: bool,
}
fn zip_uri_entries<R>(stream: &mut R) -> Result<Vec<ZipUriEntry>>
where
R: Read + Seek + ?Sized,
{
let mut reader = ZipArchive::new(&mut *stream).map_err(ZipError::Read)?;
let file_names: Vec<String> = reader.file_names().map(|name| name.to_owned()).collect();
let mut entries = Vec::new();
for file_name in file_names {
let file = reader.by_name(&file_name).map_err(ZipError::Read)?;
let path = match file.enclosed_name() {
Some(path) => path,
None => return Err(ZipError::InvalidPath(file_name).into()),
};
let header_start = file.header_start();
let data_start = file
.data_start()
.ok_or_else(|| ZipError::MissingDataStart(file_name.clone()))?;
let metadata = file.get_metadata();
entries.push(ZipUriEntry {
path,
header_start,
data_end: data_start + file.compressed_size(),
using_data_descriptor: metadata.using_data_descriptor,
large_file: metadata.large_file,
});
}
Ok(entries)
}
#[derive(Debug, thiserror::Error)]
pub enum ZipError {
#[error("could not read the ZIP")]
Read(#[source] zip::result::ZipError),
#[error("could not embed the C2PA manifest into the ZIP")]
Write(#[source] zip::result::ZipError),
#[error("could not remove the C2PA manifest from the ZIP")]
Remove(#[source] zip::result::ZipError),
#[error("data hashing is not supported for ZIP, use a collection hash instead")]
ObjectLocationsUnsupported,
#[error("invalid stored path `{0}` in the ZIP")]
InvalidPath(String),
#[error("could not locate the data start for `{0}` in the ZIP")]
MissingDataStart(String),
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use std::io::Write;
use io::{Cursor, Seek};
use super::*;
const SAMPLES: [&[u8]; 3] = [
include_bytes!("../../tests/fixtures/sample1.zip"),
include_bytes!("../../tests/fixtures/sample1.docx"),
include_bytes!("../../tests/fixtures/sample1.odt"),
];
#[test]
fn test_write_bytes() {
for sample in SAMPLES {
let mut stream = Cursor::new(sample);
let zip_io = ZipIO {};
assert!(matches!(
zip_io.read_cai(&mut stream),
Err(Error::JumbfNotFound)
));
let mut output_stream = Cursor::new(Vec::with_capacity(sample.len() + 7));
let random_bytes = [1, 2, 3, 4, 3, 2, 1];
zip_io
.write_cai(&mut stream, &mut output_stream, &random_bytes)
.unwrap();
let data_written = zip_io.read_cai(&mut output_stream).unwrap();
assert_eq!(data_written, random_bytes);
}
}
#[test]
fn test_write_bytes_replace() {
for sample in SAMPLES {
let mut stream = Cursor::new(sample);
let zip_io = ZipIO {};
assert!(matches!(
zip_io.read_cai(&mut stream),
Err(Error::JumbfNotFound)
));
let mut output_stream1 = Cursor::new(Vec::with_capacity(sample.len() + 7));
let random_bytes = [1, 2, 3, 4, 3, 2, 1];
zip_io
.write_cai(&mut stream, &mut output_stream1, &random_bytes)
.unwrap();
let data_written = zip_io.read_cai(&mut output_stream1).unwrap();
assert_eq!(data_written, random_bytes);
let mut output_stream2 = Cursor::new(Vec::with_capacity(sample.len() + 5));
let random_bytes = [3, 2, 1, 2, 3];
zip_io
.write_cai(&mut output_stream1, &mut output_stream2, &random_bytes)
.unwrap();
let data_written = zip_io.read_cai(&mut output_stream2).unwrap();
assert_eq!(data_written, random_bytes);
let mut bytes = Vec::new();
stream.rewind().unwrap();
stream.read_to_end(&mut bytes).unwrap();
assert_eq!(sample, bytes);
}
}
#[test]
fn test_remove_cai_store() {
for sample in SAMPLES {
let zip_io = ZipIO {};
let mut input = Cursor::new(sample);
let mut with_manifest = Cursor::new(Vec::new());
zip_io
.write_cai(&mut input, &mut with_manifest, &[1, 2, 3])
.unwrap();
assert_eq!(zip_io.read_cai(&mut with_manifest).unwrap(), [1, 2, 3]);
let mut removed = Cursor::new(Vec::new());
zip_io
.remove_cai_store_from_stream(&mut with_manifest, &mut removed)
.unwrap();
assert!(matches!(
zip_io.read_cai(&mut removed),
Err(Error::JumbfNotFound)
));
}
}
#[test]
fn test_read_cai_invalid_zip() {
let zip_io = ZipIO {};
let mut not_a_zip = Cursor::new(b"i am a zip".to_vec());
assert!(matches!(
zip_io.read_cai(&mut not_a_zip),
Err(Error::ZipError(ZipError::Read(_)))
));
}
#[test]
fn test_object_locations_unsupported() {
let zip_io = ZipIO {};
let mut stream = Cursor::new(SAMPLES[0]);
assert!(matches!(
zip_io.get_object_locations_from_stream(&mut stream),
Err(Error::ZipError(ZipError::ObjectLocationsUnsupported))
));
}
#[test]
fn test_zip_central_directory_range_no_manifest() {
let mut stream = Cursor::new(SAMPLES[0]);
assert_eq!(
zip_central_directory_range(&mut stream).unwrap(),
vec![HashRange::new(369, 727)]
);
}
#[test]
fn test_zip_uri_ranges1() {
let mut stream = Cursor::new(SAMPLES[0]);
let ranges = zip_uri_ranges(&mut stream).unwrap();
assert_eq!(ranges.len(), 7);
assert_eq!(
ranges.get(Path::new("sample1/test1.txt")),
Some(&HashRange::new(44, 47))
);
assert_eq!(
ranges.get(Path::new("sample1/test2.txt")),
Some(&HashRange::new(313, 56))
);
}
#[test]
fn test_zip_uri_ranges_data_descriptor_length() {
let mut writer = ZipWriter::new_stream(Vec::new());
writer
.start_file("only.txt", SimpleFileOptions::default())
.unwrap();
writer.write_all(b"hello").unwrap();
let bytes = writer.finish().unwrap().into_inner();
let mut stream = Cursor::new(bytes);
let (header_start, data_end) = {
let mut archive = ZipArchive::new(&mut stream).unwrap();
let file = archive.by_name("only.txt").unwrap();
(
file.header_start(),
file.data_start().unwrap() + file.compressed_size(),
)
};
let data_descriptor_len = 16;
let ranges = zip_uri_ranges(&mut stream).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(
ranges.get(Path::new("only.txt")),
Some(&HashRange::new(
header_start,
(data_end + data_descriptor_len) - header_start
))
);
}
#[test]
fn test_zip_uri_ranges_includes_directory_local_header() {
let mut writer = ZipWriter::new_stream(Vec::new());
writer
.add_directory("mydir", SimpleFileOptions::default())
.unwrap();
writer
.start_file("mydir/file.txt", SimpleFileOptions::default())
.unwrap();
writer.write_all(b"hello").unwrap();
let bytes = writer.finish().unwrap().into_inner();
let ranges = zip_uri_ranges(&mut Cursor::new(bytes)).unwrap();
assert_eq!(ranges.len(), 2);
let dir_range = ranges.get(Path::new("mydir/")).unwrap();
let file_range = ranges.get(Path::new("mydir/file.txt")).unwrap();
assert_eq!(dir_range.start(), 0);
assert_eq!(dir_range.start() + dir_range.length(), file_range.start());
}
#[test]
fn test_central_directory_range_skips_manifest_crc() {
let zip_io = ZipIO {};
let mut input = Cursor::new(SAMPLES[0]);
let mut with_manifest = Cursor::new(Vec::new());
zip_io
.write_cai(&mut input, &mut with_manifest, &[1, 2, 3])
.unwrap();
let ranges = zip_central_directory_range(&mut with_manifest).unwrap();
assert_eq!(ranges.len(), 2);
let uri_ranges = zip_uri_ranges(&mut with_manifest).unwrap();
assert!(!uri_ranges.contains_key(Path::new(MANIFEST_PATH)));
}
#[test]
fn test_write_preserves_existing_entry_bytes() {
fn read_range<R: Read + Seek>(stream: &mut R, range: &HashRange) -> Vec<u8> {
stream.seek(SeekFrom::Start(range.start())).unwrap();
let mut bytes = vec![0; range.length() as usize];
stream.read_exact(&mut bytes).unwrap();
bytes
}
let zip_io = ZipIO {};
let mut src = Cursor::new(SAMPLES[0].to_vec());
let input_ranges = zip_uri_ranges(&mut src).unwrap();
let mut src_with_manifest = Cursor::new(Vec::new());
zip_io
.write_cai(&mut src, &mut src_with_manifest, &[1, 2, 3])
.unwrap();
let output_ranges = zip_uri_ranges(&mut src_with_manifest).unwrap();
assert_eq!(output_ranges.len(), input_ranges.len() + 1);
for (path, input_range) in input_ranges {
let output_range = output_ranges.get(&path).unwrap();
assert_eq!(
read_range(&mut src, &input_range),
read_range(&mut src_with_manifest, output_range),
"entry `{}` bytes changed after embedding the manifest",
path.display()
);
}
}
}