use std::io::{Cursor, Read, Result as IoResult, Seek, Write};
use zip::result::ZipError;
use zip::write::FileOptions;
use zip::ZipWriter;
use crate::{VariableType, Writer as VarWriter};
pub enum Kind {
B83,
B84,
}
impl Kind {
pub fn file_extension(&self) -> &'static str {
match self {
Kind::B83 => "b83",
Kind::B84 => "b84",
}
}
fn metadata_device_name(&self) -> &'static str {
match self {
Kind::B83 => "83CE",
Kind::B84 => "84CE",
}
}
}
pub struct Writer<W>
where
W: Write + Seek,
{
kind: Kind,
zip: ZipWriter<W>,
crc_sum: u32,
active_var: Option<(VarWriter<Cursor<Vec<u8>>>, String)>,
}
impl<W> Writer<W>
where
W: Write + Seek,
{
pub fn new(kind: Kind, writer: W) -> Self {
Writer {
kind,
zip: ZipWriter::new(writer),
crc_sum: 0,
active_var: None,
}
}
pub fn start_var(&mut self, ty: VariableType, name: &str, archived: bool) -> IoResult<()> {
self.close_var()?;
self.active_var = Some((
VarWriter::new(Cursor::new(Vec::new()), ty, name, archived)?,
format!("{}.{}", name, ty.file_extension()),
));
Ok(())
}
fn update_crc(&mut self, data: &[u8]) {
self.crc_sum = self.crc_sum.wrapping_add(crc32fast::hash(data));
}
fn close_var(&mut self) -> IoResult<()> {
let (w, name) = match self.active_var.take() {
Some(x) => x,
None => return Ok(()),
};
let buf = w.close()?.into_inner();
self.update_crc(&buf);
self.zip.start_file(name, FileOptions::default())?;
self.zip.write_all(&buf)
}
pub fn close(mut self) -> IoResult<W> {
self.close_var()?;
self.zip.start_file("METADATA", FileOptions::default())?;
let metadata_contents =
format!(
"bundle_identifier:TI Bundle\n\
bundle_format_version:1\n\
bundle_target_device:{}\n\
bundle_target_type:CUSTOM\n\
bundle_comments:Generated by tifiles-rs::bundle::Writer\n",
self.kind.metadata_device_name()
);
self.update_crc(metadata_contents.as_bytes());
self.zip.write_all(metadata_contents.as_bytes())?;
self.zip.start_file("_CHECKSUM", FileOptions::default())?;
write!(self.zip, "{:x}", self.crc_sum)?;
match self.zip.finish() {
Err(ZipError::Io(e)) => Err(e),
Err(o) => unreachable!("zip.finish() can only return IO errors, but got {:?}", o),
Ok(w) => Ok(w),
}
}
}
impl<W> Write for Writer<W>
where
W: Write + Seek,
{
fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
match self.active_var {
None => {
panic!("start_var must be called on a bundle writer before data can be written")
}
Some((ref mut v, _)) => v.write(buf),
}
}
fn flush(&mut self) -> IoResult<()> {
match self.active_var {
None => {
panic!("start_var must be called on a bundle writer before data can be flushed")
}
Some((ref mut v, _)) => v.flush(),
}
}
}
#[test]
fn crc_matches_metafile() {
let mut w = Writer::new(Kind::B83, Cursor::new(Vec::new()));
w.start_var(VariableType::AppVar, "A", false).unwrap();
write!(w, "var one data").unwrap();
w.start_var(VariableType::AppVar, "B", false).unwrap();
write!(w, "var two data").unwrap();
let data = w.close().unwrap().into_inner();
let mut zip = zip::ZipArchive::new(Cursor::new(data)).unwrap();
let mut actual_crc = 0u32;
for i in 0..zip.len() - 1 {
let file = zip.by_index(i).unwrap();
assert_ne!(file.name(), "_CHECKSUM", "checksum file should be last");
actual_crc = actual_crc.wrapping_add(file.crc32());
}
let mut checksum_file = zip.by_name("_CHECKSUM").unwrap();
let mut checksum_string = String::new();
checksum_file.read_to_string(&mut checksum_string).unwrap();
assert_eq!(
u32::from_str_radix(&checksum_string, 16).unwrap(),
actual_crc,
"Actual zip CRCs did not match CHECKSUM file"
);
}