use std::io::{Cursor, Read, Seek, Write};
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipArchive, ZipWriter};
use crate::{manifest, LibError, Result, MIMETYPE};
#[derive(Debug, Clone)]
pub struct DataFile {
pub name: String,
pub mime_type: String,
pub content: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct SignatureFile {
pub name: String,
pub xml: String,
}
#[derive(Debug, Clone)]
pub struct OpenOptions {
pub require_mimetype: bool,
}
impl Default for OpenOptions {
fn default() -> Self {
Self {
require_mimetype: false,
}
}
}
#[derive(Debug, Default)]
pub struct Container {
data_files: Vec<DataFile>,
signatures: Vec<SignatureFile>,
warnings: Vec<String>,
}
impl Container {
pub fn new() -> Self {
Self::default()
}
pub fn data_files(&self) -> &[DataFile] {
&self.data_files
}
pub fn signatures(&self) -> &[SignatureFile] {
&self.signatures
}
pub fn warnings(&self) -> &[String] {
&self.warnings
}
pub fn add_file(
&mut self,
name: impl Into<String>,
mime_type: impl Into<String>,
content: Vec<u8>,
) -> Result<()> {
let name = name.into();
validate_entry_name(&name)?;
if self.data_files.iter().any(|f| f.name == name) {
return Err(LibError::Container(format!("duplicate file name: {name}")));
}
self.data_files.push(DataFile {
name,
mime_type: mime_type.into(),
content,
});
Ok(())
}
pub fn add_signature_xml(&mut self, xml: String) -> &SignatureFile {
let mut n = self.signatures.len();
let name = loop {
let candidate = format!("META-INF/signatures{n}.xml");
if !self.signatures.iter().any(|s| s.name == candidate) {
break candidate;
}
n += 1;
};
self.signatures.push(SignatureFile { name, xml });
self.signatures.last().expect("a signature should exist")
}
pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
Self::open_with(reader, &OpenOptions::default())
}
pub fn open_with<R: Read + Seek>(reader: R, opts: &OpenOptions) -> Result<Self> {
let mut zip = ZipArchive::new(reader)?;
let mut container = Container::default();
let mut names: Vec<String> = Vec::with_capacity(zip.len());
for i in 0..zip.len() {
names.push(zip.by_index(i)?.name().to_owned());
}
let mimetype_present = names.iter().any(|n| n == "mimetype");
if let Some(n) = names.first() {
if n == "mimetype" {
let entry = zip.by_index(0)?;
if entry.compression() != CompressionMethod::Stored {
container.warnings.push("'mimetype' is compressed".into());
}
} else if mimetype_present {
container
.warnings
.push("'mimetype' is not the first zip entry".into());
}
}
if mimetype_present {
let mimetype = read_entry(&mut zip, "mimetype")?;
let mimetype = String::from_utf8_lossy(&mimetype);
if mimetype.trim() != MIMETYPE {
return Err(LibError::Container(format!(
"unexpected container mime type: {}",
mimetype.trim()
)));
}
} else if opts.require_mimetype {
return Err(LibError::Container("missing mimetype entry".into()));
} else {
container
.warnings
.push("container has no mimetype entry".into());
}
let manifest_types = match read_entry(&mut zip, "META-INF/manifest.xml") {
Ok(bytes) => manifest::parse(&bytes)?,
Err(_) => {
return Err(LibError::Container(
"container has no META-INF/manifest.xml".into(),
));
}
};
for name in &names {
if name == "mimetype" || name.ends_with('/') {
continue;
}
let bytes = read_entry(&mut zip, name)?;
if let Some(rest) = name.strip_prefix("META-INF/") {
if is_signature_entry(rest) {
container.signatures.push(SignatureFile {
name: name.clone(),
xml: String::from_utf8(bytes)
.map_err(|_| LibError::Xml(format!("{name} is not valid UTF-8")))?,
});
}
continue;
}
let mime_type = manifest_types
.iter()
.find(|(n, _)| n == name)
.map(|(_, m)| m.clone())
.ok_or_else(|| {
LibError::Container(format!("missing MIME type in manifest for {name}"))
})?;
container.data_files.push(DataFile {
name: name.clone(),
mime_type,
content: bytes,
});
}
if container.data_files.is_empty() {
return Err(LibError::Container("container has no data files".into()));
}
Ok(container)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
Self::open(Cursor::new(bytes))
}
pub fn from_bytes_with(bytes: &[u8], opts: &OpenOptions) -> Result<Self> {
Self::open_with(Cursor::new(bytes), opts)
}
pub fn open_file(path: impl AsRef<std::path::Path>) -> Result<Self> {
Self::open(std::fs::File::open(path)?)
}
pub fn open_file_with(path: impl AsRef<std::path::Path>, opts: &OpenOptions) -> Result<Self> {
Self::open_with(std::fs::File::open(path)?, opts)
}
fn write_to<W: Write + Seek>(&self, writer: W) -> Result<()> {
if self.data_files.is_empty() {
return Err(LibError::Container("container has no data files".into()));
}
let mut zip = ZipWriter::new(writer);
zip.start_file(
"mimetype",
SimpleFileOptions::default().compression_method(CompressionMethod::Stored),
)?;
zip.write_all(MIMETYPE.as_bytes())?;
let deflated = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
for file in &self.data_files {
zip.start_file(&file.name, deflated)?;
zip.write_all(&file.content)?;
}
zip.start_file("META-INF/manifest.xml", deflated)?;
zip.write_all(&manifest::build(&self.data_files))?;
for sig in &self.signatures {
zip.start_file(&sig.name, deflated)?;
zip.write_all(sig.xml.as_bytes())?;
}
zip.finish()?;
Ok(())
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let mut buf = Cursor::new(Vec::new());
self.write_to(&mut buf)?;
Ok(buf.into_inner())
}
pub fn save(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
self.write_to(std::fs::File::create(path)?)
}
}
fn read_entry<R: Read + Seek>(zip: &mut ZipArchive<R>, name: &str) -> Result<Vec<u8>> {
let mut entry = zip
.by_name(name)
.map_err(|_| LibError::Container(format!("missing zip entry: {name}")))?;
let mut buf = Vec::with_capacity(entry.size() as usize);
entry.read_to_end(&mut buf)?;
Ok(buf)
}
fn validate_entry_name(name: &str) -> Result<()> {
if name.is_empty() {
return Err(LibError::Container("file name must not be empty".into()));
}
if name == "mimetype" || name.starts_with("META-INF/") {
return Err(LibError::Container(format!("reserved entry name: {name}")));
}
if name.starts_with('/') || name.split('/').any(|part| part == ".." || part.is_empty()) {
return Err(LibError::Container(format!(
"file name must be a clean relative path: {name}"
)));
}
Ok(())
}
fn is_signature_entry(meta_inf_rest: &str) -> bool {
meta_inf_rest.contains("signatures") && meta_inf_rest.ends_with(".xml")
}