use std::io::{Read, Seek, Write};
use std::path::Path;
use toml_edit::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 name = name_from_path(path)?;
pack(name, std::fs::File::open(path)?, metadata, out)
}
pub struct Repack<'a, R> {
source: R,
metadata: Option<NewMetadata<'a>>,
payload: Option<(String, Box<dyn Read + 'a>)>,
}
enum NewMetadata<'a> {
Document(&'a DocumentMut),
Bytes(&'a [u8]),
}
impl<'a, R: Read + Seek> Repack<'a, R> {
#[must_use]
pub fn new(source: R) -> Self {
Self {
source,
metadata: None,
payload: None,
}
}
#[must_use]
pub fn metadata(mut self, document: &'a DocumentMut) -> Self {
self.metadata = Some(NewMetadata::Document(document));
self
}
#[must_use]
pub fn metadata_bytes(mut self, bytes: &'a [u8]) -> Self {
self.metadata = Some(NewMetadata::Bytes(bytes));
self
}
#[must_use]
pub fn payload(mut self, name: &str, payload: impl Read + 'a) -> Self {
self.payload = Some((name.to_owned(), Box::new(payload)));
self
}
pub fn payload_file<P: AsRef<Path>>(mut self, path: P) -> Result<Self> {
let path = path.as_ref();
let name = name_from_path(path)?.to_owned();
self.payload = Some((name, Box::new(std::fs::File::open(path)?)));
Ok(self)
}
pub fn write<W: Write + Seek>(self, out: W) -> Result<()> {
let Self {
source,
metadata,
mut payload,
} = self;
let mut c = Container::read(source)?;
if !c.version_is_recognised() {
return Err(Unsupported::Version(c.version().to_owned()).into());
}
if let Some((name, _)) = &payload {
name::check_payload_name(name)?;
let carried = c.names.iter().filter(|n| n.decodes_to(name)).count();
if carried > usize::from(name == c.payload_name()) {
return Err(Malformed::PayloadNameTaken(name.clone()).into());
}
}
let payload_name = payload.as_ref().map(|(n, _)| n.as_str());
let new_metadata: Option<Vec<u8>> = match (metadata, payload_name) {
(Some(NewMetadata::Bytes(b)), _) => Some(b.to_vec()),
(Some(NewMetadata::Document(d)), None) => Some(d.to_string().into_bytes()),
(Some(NewMetadata::Document(d)), Some(n)) => Some(repointed(d, n)?),
(None, Some(n)) if n != c.payload_name() => Some(repointed(c.metadata(), n)?),
(None, _) => None,
};
if let Some(bytes) = &new_metadata {
let (_, keys) = metadata::parse(bytes)?;
if keys.version != VERSION {
return Err(Malformed::Disagrees {
key: VERSION_KEY,
found: keys.version,
writing: VERSION.to_owned(),
}
.into());
}
match payload_name {
Some(n) if keys.payload_file != n => {
return Err(Malformed::Disagrees {
key: PAYLOAD_FILE_KEY,
found: keys.payload_file,
writing: n.to_owned(),
}
.into())
}
Some(_) => {}
None => {
locate_payload(&c.entries, &c.names, &keys.payload_file)?;
}
}
}
let payload_at = if payload.is_some() {
c.payload_index
} else {
None
};
let mut w = ZipWriter::new(out);
for i in 0..c.entries.len() {
if i == c.metadata_index {
match &new_metadata {
Some(bytes) => {
w.start_file(METADATA_MEMBER, options())?;
w.write_all(bytes)?;
}
None => w.raw_copy_file(c.archive.by_index_raw(i)?)?,
}
} else if let Some((name, data)) = payload.as_mut().filter(|_| Some(i) == payload_at) {
w.start_file(name.as_str(), options())?;
std::io::copy(data, &mut w)?;
} else {
w.raw_copy_file(c.archive.by_index_raw(i)?)?;
}
}
w.finish()?.flush()?;
Ok(())
}
}
pub fn rewrite_metadata<R, W>(source: R, metadata: &DocumentMut, out: W) -> Result<()>
where
R: Read + Seek,
W: Write + Seek,
{
Repack::new(source).metadata(metadata).write(out)
}
pub fn rewrite_metadata_bytes<R, W>(source: R, metadata: &[u8], out: W) -> Result<()>
where
R: Read + Seek,
W: Write + Seek,
{
Repack::new(source).metadata_bytes(metadata).write(out)
}
fn repointed(doc: &DocumentMut, payload_name: &str) -> Result<Vec<u8>> {
let mut doc = doc.clone();
metadata::set(&mut doc, PAYLOAD_FILE_KEY, payload_name)?;
Ok(doc.to_string().into_bytes())
}
fn name_from_path(path: &Path) -> Result<&str> {
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)?;
Ok(name)
}
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)?;
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 Some(item) = metadata::lookup(doc, key) else {
return metadata::set(doc, key, writing);
};
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())
}
}