use std::io::{Read, Seek};
use std::path::Path;
use toml_edit::DocumentMut;
use zip::ZipArchive;
use crate::error::{EntryKind, Error, Malformed, Result, Unsupported};
use crate::{central, metadata, name, Limits, METADATA_MEMBER, VERSION};
pub(crate) struct Entry {
raw: Vec<u8>,
kind: EntryKind,
size: u64,
crc: u32,
encrypted: bool,
unsupported_method: Option<u16>,
}
fn entries_of<R: Read + Seek>(archive: &mut ZipArchive<R>) -> Result<Vec<Entry>> {
let mut entries = Vec::with_capacity(archive.len());
for i in 0..archive.len() {
let f = archive.by_index_raw(i)?;
entries.push(Entry {
raw: f.name_raw().to_owned(),
kind: EntryKind::from_mode(f.unix_mode()),
size: f.size(),
crc: f.crc32(),
encrypted: f.encrypted(),
unsupported_method: unsupported_method(f.compression()),
});
}
Ok(entries)
}
#[allow(deprecated)]
fn unsupported_method(method: zip::CompressionMethod) -> Option<u16> {
match method {
zip::CompressionMethod::Unsupported(id) => Some(id),
_ => None,
}
}
pub(crate) enum Located {
One(usize),
None,
Several(usize),
}
pub(crate) fn locate(entries: &[Entry], names: &[central::Recorded], want: &str) -> Located {
let mut matched = names.iter().filter(|n| n.decodes_to(want));
let Some(first) = matched.next() else {
return Located::None;
};
let count = 1 + matched.count();
if count > 1 {
return Located::Several(count);
}
match entries.iter().position(|e| e.raw == first.bytes) {
Some(i) => Located::One(i),
None => Located::None,
}
}
pub struct Container<R> {
pub(crate) archive: ZipArchive<R>,
pub(crate) entries: Vec<Entry>,
pub(crate) names: Vec<central::Recorded>,
pub(crate) metadata_index: usize,
doc: DocumentMut,
bytes: Vec<u8>,
version: String,
payload_file: String,
pub(crate) payload_index: Option<usize>,
}
impl Container<std::fs::File> {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::read(std::fs::File::open(path)?)
}
pub fn open_with<P: AsRef<Path>>(path: P, limits: Limits) -> Result<Self> {
Self::read_with(std::fs::File::open(path)?, limits)
}
}
impl<R: Read + Seek> Container<R> {
pub fn read(reader: R) -> Result<Self> {
Self::read_with(reader, Limits::default())
}
pub fn read_with(mut reader: R, limits: Limits) -> Result<Self> {
let names = central::names(&mut reader)?;
reader.rewind()?;
let mut archive = ZipArchive::new(reader)?;
let entries = entries_of(&mut archive)?;
let meta_index = match locate(&entries, &names, METADATA_MEMBER) {
Located::One(i) => i,
Located::None => return Err(Malformed::NoMetadataMember.into()),
Located::Several(n) => return Err(Malformed::DuplicateMetadataMember(n).into()),
};
let bytes =
read_metadata_member(&mut archive, meta_index, entries[meta_index].size, limits)?;
let (doc, keys) = metadata::parse(&bytes)?;
let crate::metadata::Keys {
version,
payload_file,
} = keys;
let payload_index = if version == VERSION {
Some(locate_payload(&entries, &names, &payload_file)?)
} else {
None
};
Ok(Self {
archive,
entries,
names,
metadata_index: meta_index,
doc,
bytes,
version,
payload_file,
payload_index,
})
}
pub fn version(&self) -> &str {
&self.version
}
pub fn payload_name(&self) -> &str {
&self.payload_file
}
pub fn metadata(&self) -> &DocumentMut {
&self.doc
}
pub fn metadata_mut(&mut self) -> &mut DocumentMut {
&mut self.doc
}
pub fn metadata_bytes(&self) -> &[u8] {
&self.bytes
}
pub(crate) fn version_is_recognised(&self) -> bool {
self.payload_index.is_some()
}
pub fn payload_size(&self) -> Result<u64> {
let i = self
.payload_index
.ok_or_else(|| Unsupported::Version(self.version.clone()))?;
Ok(self.entries[i].size)
}
pub fn payload_crc(&self) -> Result<u32> {
let i = self
.payload_index
.ok_or_else(|| Unsupported::Version(self.version.clone()))?;
Ok(self.entries[i].crc)
}
pub fn payload_mode(&self) -> Result<Option<u32>> {
let i = self
.payload_index
.ok_or_else(|| Unsupported::Version(self.version.clone()))?;
let raw = &self.entries[i].raw;
let attributes = self
.names
.iter()
.find(|n| n.bytes == *raw)
.map_or(0, |n| n.external_attributes);
let mode = attributes >> 16;
Ok((mode != 0).then_some(mode & 0o7777))
}
pub fn check_payload_readable(&self) -> std::result::Result<(), Unsupported> {
let i = self
.payload_index
.ok_or_else(|| Unsupported::Version(self.version.clone()))?;
if self.entries[i].encrypted {
return Err(Unsupported::Encrypted);
}
if let Some(m) = self.entries[i].unsupported_method {
return Err(Unsupported::Compression(m));
}
Ok(())
}
pub fn payload(&mut self) -> Result<impl Read + '_> {
let i = self
.payload_index
.ok_or_else(|| Unsupported::Version(self.version.clone()))?;
Ok(self.archive.by_index(i)?)
}
}
pub fn metadata_of<R: Read + Seek>(reader: R) -> Result<DocumentMut> {
metadata_of_with(reader, Limits::default())
}
pub fn metadata_of_with<R: Read + Seek>(mut reader: R, limits: Limits) -> Result<DocumentMut> {
let names = central::names(&mut reader)?;
reader.rewind()?;
let mut archive = ZipArchive::new(reader)?;
let entries = entries_of(&mut archive)?;
let i = match locate(&entries, &names, METADATA_MEMBER) {
Located::One(i) => i,
Located::None => return Err(Malformed::NoMetadataMember.into()),
Located::Several(n) => return Err(Malformed::DuplicateMetadataMember(n).into()),
};
let bytes = read_metadata_member(&mut archive, i, entries[i].size, limits)?;
metadata::document(&bytes)
}
fn read_metadata_member<R: Read + Seek>(
archive: &mut ZipArchive<R>,
index: usize,
declared: u64,
limits: Limits,
) -> Result<Vec<u8>> {
let limit = limits.metadata_bytes;
let too_large = || Unsupported::MetadataTooLarge { limit, declared };
if declared > limit {
return Err(too_large().into());
}
let mut bytes = Vec::new();
let mut member = archive.by_index(index)?;
member.by_ref().take(limit + 1).read_to_end(&mut bytes)?;
if bytes.len() as u64 > limit {
return Err(too_large().into());
}
Ok(bytes)
}
pub(crate) fn locate_payload(
entries: &[Entry],
names: &[central::Recorded],
payload_file: &str,
) -> Result<usize> {
name::check_payload_name(payload_file)?;
let i = match locate(entries, names, payload_file) {
Located::One(i) => i,
Located::None => return Err(Malformed::NoPayloadMember(payload_file.to_owned()).into()),
Located::Several(count) => {
return Err(Malformed::DuplicatePayloadMember {
name: payload_file.to_owned(),
count,
}
.into())
}
};
if entries[i].kind != EntryKind::Regular {
return Err(Error::Malformed(Malformed::PayloadNotARegularFile {
name: payload_file.to_owned(),
kind: entries[i].kind,
}));
}
Ok(i)
}