use crate::error::Error;
use crate::zip::{self, ZIP_MANIFEST_PATH};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Member {
pub name: String,
pub content: std::ops::Range<usize>,
}
pub fn collection_members(zip: &[u8]) -> Result<Vec<Member>, Error> {
Ok(zip::member_ranges(zip)?
.into_iter()
.filter(|(name, _)| name != ZIP_MANIFEST_PATH)
.map(|(name, content)| Member { name, content })
.collect())
}
pub fn central_directory_range(zip: &[u8]) -> Result<std::ops::Range<usize>, Error> {
zip::central_directory_range(zip)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::writer::embed_manifest;
use crate::zip::tests::build_zip;
const MANIFEST: &[u8] = b"pretend-manifest-store";
fn fixture() -> Vec<u8> {
build_zip(&[
("mimetype", b"application/epub+zip"),
("META-INF/container.xml", b"<container/>"),
("OEBPS/content.opf", b"<package/>"),
])
}
#[test]
fn members_exclude_the_manifest_entry() {
let signed = embed_manifest(&fixture(), MANIFEST).unwrap();
let members = collection_members(&signed).unwrap();
let names: Vec<&str> = members.iter().map(|m| m.name.as_str()).collect();
assert_eq!(
names,
["mimetype", "META-INF/container.xml", "OEBPS/content.opf"]
);
assert!(!names.contains(&ZIP_MANIFEST_PATH));
}
#[test]
fn member_ranges_address_the_stored_content() {
let zip = fixture();
let members = collection_members(&zip).unwrap();
let mimetype = members.iter().find(|m| m.name == "mimetype").unwrap();
assert_eq!(&zip[mimetype.content.clone()], b"application/epub+zip");
}
#[test]
fn central_directory_range_reaches_the_end_of_the_archive() {
let zip = fixture();
let range = central_directory_range(&zip).unwrap();
assert_eq!(range.end, zip.len(), "the EOCD record is the last thing");
assert_eq!(&zip[range.start..range.start + 4], b"PK\x01\x02");
assert!(zip[range.clone()].windows(4).any(|w| w == b"PK\x05\x06"));
}
#[test]
fn adding_an_entry_changes_the_central_directory_coverage() {
let zip = fixture();
let before = &zip[central_directory_range(&zip).unwrap()].to_vec();
let signed = embed_manifest(&zip, MANIFEST).unwrap();
let after = &signed[central_directory_range(&signed).unwrap()].to_vec();
assert_ne!(before, after);
}
#[test]
fn every_member_range_is_inside_the_archive_and_before_the_directory() {
let signed = embed_manifest(&fixture(), MANIFEST).unwrap();
let cd = central_directory_range(&signed).unwrap();
for m in collection_members(&signed).unwrap() {
assert!(m.content.end <= signed.len(), "{} past end", m.name);
assert!(m.content.start <= m.content.end, "{} inverted", m.name);
assert!(
m.content.end <= cd.start,
"{} overlaps the directory",
m.name
);
}
}
#[test]
fn a_truncated_archive_is_rejected_rather_than_panicking() {
let zip = fixture();
for cut in [0usize, 1, 8, zip.len() / 2, zip.len() - 1] {
let truncated = &zip[..cut];
assert!(collection_members(truncated).is_err() || truncated.is_empty());
assert!(central_directory_range(truncated).is_err() || truncated.is_empty());
}
}
}