use std::io::{Cursor, Read, Write};
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipArchive, ZipWriter};
use crate::Error;
use crate::xml::{self, Element, Ns};
pub struct Part {
pub name: String,
pub data: Vec<u8>,
pub stored: bool,
pub directory: bool,
}
pub struct Package {
parts: Vec<Part>,
media_type: Option<String>,
}
const MIMETYPE: &str = "mimetype";
impl Package {
pub fn read(bytes: &[u8]) -> Result<Self, Error> {
let mut archive = ZipArchive::new(Cursor::new(bytes))?;
let mut parts = Vec::with_capacity(archive.len());
for index in 0..archive.len() {
let mut entry = archive.by_index(index)?;
let name = entry.name().to_owned();
let stored = entry.compression() == CompressionMethod::Stored;
if name.ends_with('/') {
parts.push(Part {
name,
data: Vec::new(),
stored,
directory: true,
});
continue;
}
let mut data = Vec::new();
entry
.read_to_end(&mut data)
.map_err(|e| Error::Package(e.to_string()))?;
parts.push(Part {
name,
data,
stored,
directory: false,
});
}
let mut package = Self {
parts,
media_type: None,
};
package.media_type = package.read_media_type();
Ok(package)
}
pub fn media_type(&self) -> Option<&str> {
self.media_type.as_deref()
}
fn read_media_type(&self) -> Option<String> {
if let Some(part) = self.part(MIMETYPE)
&& let Ok(text) = std::str::from_utf8(&part.data)
{
let text = text.trim();
if !text.is_empty() {
return Some(text.to_owned());
}
}
let part = self.part("META-INF/manifest.xml")?;
let root = xml::parse(&part.data, "META-INF/manifest.xml").ok()?;
root.elements()
.find(|e| {
e.is(&Ns::Manifest, "file-entry") && e.attr(&Ns::Manifest, "full-path") == Some("/")
})
.and_then(|e| e.attr(&Ns::Manifest, "media-type"))
.filter(|t| !t.is_empty())
.map(ToOwned::to_owned)
}
pub fn part(&self, name: &str) -> Option<&Part> {
self.parts.iter().find(|p| p.name == name)
}
pub fn parts(&self) -> impl Iterator<Item = &Part> {
self.parts.iter()
}
pub fn xml(&self, name: &'static str) -> Result<Element, Error> {
let part = self.part(name).ok_or(Error::MissingPart(name))?;
xml::parse(&part.data, name)
}
pub fn optional_xml(&self, name: &'static str) -> Result<Option<Element>, Error> {
match self.part(name) {
Some(part) => xml::parse(&part.data, name).map(Some),
None => Ok(None),
}
}
pub fn set_part(&mut self, name: &str, data: Vec<u8>) {
if let Some(part) = self.parts.iter_mut().find(|p| p.name == name) {
part.data = data;
return;
}
self.parts.push(Part {
name: name.to_owned(),
data,
stored: false,
directory: false,
});
}
pub fn set_xml(&mut self, name: &str, root: &Element) {
self.set_part(name, xml::serialize(root));
}
pub fn write(&self) -> Result<Vec<u8>, Error> {
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
let stored = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
let deflated = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
if let Some(part) = self.part(MIMETYPE) {
writer.start_file(MIMETYPE, stored)?;
writer
.write_all(&part.data)
.map_err(|e| Error::Package(e.to_string()))?;
}
for part in &self.parts {
if part.name == MIMETYPE {
continue;
}
if part.directory {
writer.add_directory(part.name.trim_end_matches('/'), stored)?;
continue;
}
let options = if part.stored { stored } else { deflated };
writer.start_file(part.name.as_str(), options)?;
writer
.write_all(&part.data)
.map_err(|e| Error::Package(e.to_string()))?;
}
Ok(writer.finish()?.into_inner())
}
pub fn expect_media_type(&self, wanted: &'static [&'static str]) -> Result<(), Error> {
match self.media_type() {
Some(found) if !wanted.contains(&found) => Err(Error::WrongFormat {
found: found.to_owned(),
wanted: wanted.first().copied().unwrap_or_default(),
}),
_ => Ok(()),
}
}
}