use std::io::{Read, Seek, Write};
use std::path::Path;
use toml_edit::{value, DocumentMut};
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipWriter};
use crate::container::{locate_payload, Container};
use crate::error::{Malformed, NameError, Result, Unsupported};
use crate::{metadata, name, METADATA_MEMBER, PAYLOAD_FILE_KEY, VERSION, VERSION_KEY};
pub fn pack_reader<M, R, W>(payload_name: &str, payload: R, metadata: M, out: W) -> Result<()>
where
M: Into<DocumentMut>,
R: Read,
W: Write,
{
name::check_payload_name(payload_name)?;
pack(payload_name, payload, metadata, out)
}
pub fn pack_file<M, P, W>(payload_path: P, metadata: M, out: W) -> Result<()>
where
M: Into<DocumentMut>,
P: AsRef<Path>,
W: Write,
{
let path = payload_path.as_ref();
let bad = |cause: NameError| Malformed::PayloadPathName {
path: path.to_owned(),
cause,
};
let name = path.file_name().ok_or_else(|| bad(NameError::Empty))?;
let name = name.to_str().ok_or_else(|| bad(NameError::NotUtf8))?;
name::check_payload_name(name).map_err(bad)?;
pack(name, std::fs::File::open(path)?, metadata, out)
}
pub fn rewrite_metadata<R, W>(source: R, metadata: &DocumentMut, out: W) -> Result<()>
where
R: Read + Seek,
W: Write,
{
rewrite_metadata_bytes(source, metadata.to_string().as_bytes(), out)
}
pub fn rewrite_metadata_bytes<R, W>(source: R, metadata: &[u8], out: W) -> Result<()>
where
R: Read + Seek,
W: Write,
{
let mut c = Container::read(source)?;
if !c.version_is_recognised() {
return Err(Unsupported::Version(c.version().to_owned()).into());
}
let (_, keys) = metadata::parse(metadata)?;
if keys.version != VERSION {
return Err(Malformed::Disagrees {
key: VERSION_KEY,
found: keys.version,
writing: VERSION.to_owned(),
}
.into());
}
locate_payload(&c.entries, &keys.payload_file)?;
let mut w = ZipWriter::new_stream(out);
for i in 0..c.entries.len() {
if i == c.metadata_index {
w.start_file(METADATA_MEMBER, options())?;
w.write_all(metadata)?;
} else {
w.raw_copy_file(c.archive.by_index_raw(i)?)?;
}
}
finish(w)
}
fn options() -> SimpleFileOptions {
SimpleFileOptions::default().compression_method(CompressionMethod::Deflated)
}
fn finish<W: Write>(w: ZipWriter<zip::write::StreamWriter<W>>) -> Result<()> {
w.finish()?.into_inner().flush()?;
Ok(())
}
fn pack<M, R, W>(payload_name: &str, mut payload: R, metadata: M, out: W) -> Result<()>
where
M: Into<DocumentMut>,
R: Read,
W: Write,
{
let doc = with_required_keys(metadata.into(), payload_name)?;
let mut w = ZipWriter::new_stream(out);
w.start_file(METADATA_MEMBER, options())?;
w.write_all(doc.to_string().as_bytes())?;
w.start_file(payload_name, options())?;
std::io::copy(&mut payload, &mut w)?;
finish(w)
}
fn with_required_keys(mut doc: DocumentMut, payload_name: &str) -> Result<DocumentMut> {
agree_or_set(&mut doc, VERSION_KEY, VERSION)?;
match doc.get("payload") {
None => {}
Some(item) if item.is_table_like() => {}
Some(_) => return Err(Malformed::PayloadNotATable.into()),
}
agree_or_set(&mut doc, PAYLOAD_FILE_KEY, payload_name)?;
Ok(doc)
}
fn agree_or_set(doc: &mut DocumentMut, key: &'static str, writing: &str) -> Result<()> {
let path: Vec<&str> = key.split('.').collect();
let existing = path
.iter()
.try_fold(doc.as_item(), |item, part| item.get(part));
match existing {
None => {
let mut at = doc.as_item_mut();
for part in &path[..path.len() - 1] {
if at.get(part).is_none() {
at[*part] = toml_edit::Item::Table(toml_edit::Table::new());
}
at = &mut at[*part];
}
at[path[path.len() - 1]] = value(writing);
Ok(())
}
Some(item) => {
let found = item.as_str().ok_or(Malformed::KeyNotAString(key))?;
if found == writing {
Ok(())
} else {
Err(Malformed::Disagrees {
key,
found: found.to_owned(),
writing: writing.to_owned(),
}
.into())
}
}
}
}