1use crate::error::Error;
19use crate::zip::{self, ZIP_MANIFEST_PATH};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Member {
27 pub name: String,
28 pub content: std::ops::Range<usize>,
29}
30
31pub fn collection_members(zip: &[u8]) -> Result<Vec<Member>, Error> {
36 Ok(zip::member_ranges(zip)?
37 .into_iter()
38 .filter(|(name, _)| name != ZIP_MANIFEST_PATH)
39 .map(|(name, content)| Member { name, content })
40 .collect())
41}
42
43pub fn central_directory_range(zip: &[u8]) -> Result<std::ops::Range<usize>, Error> {
51 zip::central_directory_range(zip)
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use crate::writer::embed_manifest;
58 use crate::zip::tests::build_zip;
59
60 const MANIFEST: &[u8] = b"pretend-manifest-store";
61
62 fn fixture() -> Vec<u8> {
63 build_zip(&[
64 ("mimetype", b"application/epub+zip"),
65 ("META-INF/container.xml", b"<container/>"),
66 ("OEBPS/content.opf", b"<package/>"),
67 ])
68 }
69
70 #[test]
71 fn members_exclude_the_manifest_entry() {
72 let signed = embed_manifest(&fixture(), MANIFEST).unwrap();
73 let members = collection_members(&signed).unwrap();
74 let names: Vec<&str> = members.iter().map(|m| m.name.as_str()).collect();
75 assert_eq!(
76 names,
77 ["mimetype", "META-INF/container.xml", "OEBPS/content.opf"]
78 );
79 assert!(!names.contains(&ZIP_MANIFEST_PATH));
80 }
81
82 #[test]
83 fn member_ranges_address_the_stored_content() {
84 let zip = fixture();
85 let members = collection_members(&zip).unwrap();
86 let mimetype = members.iter().find(|m| m.name == "mimetype").unwrap();
87 assert_eq!(&zip[mimetype.content.clone()], b"application/epub+zip");
88 }
89
90 #[test]
91 fn central_directory_range_reaches_the_end_of_the_archive() {
92 let zip = fixture();
93 let range = central_directory_range(&zip).unwrap();
94 assert_eq!(range.end, zip.len(), "the EOCD record is the last thing");
95 assert_eq!(&zip[range.start..range.start + 4], b"PK\x01\x02");
97 assert!(zip[range.clone()].windows(4).any(|w| w == b"PK\x05\x06"));
99 }
100
101 #[test]
102 fn adding_an_entry_changes_the_central_directory_coverage() {
103 let zip = fixture();
104 let before = &zip[central_directory_range(&zip).unwrap()].to_vec();
105 let signed = embed_manifest(&zip, MANIFEST).unwrap();
106 let after = &signed[central_directory_range(&signed).unwrap()].to_vec();
107 assert_ne!(before, after);
110 }
111
112 #[test]
113 fn every_member_range_is_inside_the_archive_and_before_the_directory() {
114 let signed = embed_manifest(&fixture(), MANIFEST).unwrap();
115 let cd = central_directory_range(&signed).unwrap();
116 for m in collection_members(&signed).unwrap() {
117 assert!(m.content.end <= signed.len(), "{} past end", m.name);
118 assert!(m.content.start <= m.content.end, "{} inverted", m.name);
119 assert!(
120 m.content.end <= cd.start,
121 "{} overlaps the directory",
122 m.name
123 );
124 }
125 }
126
127 #[test]
128 fn a_truncated_archive_is_rejected_rather_than_panicking() {
129 let zip = fixture();
130 for cut in [0usize, 1, 8, zip.len() / 2, zip.len() - 1] {
131 let truncated = &zip[..cut];
132 assert!(collection_members(truncated).is_err() || truncated.is_empty());
133 assert!(central_directory_range(truncated).is_err() || truncated.is_empty());
134 }
135 }
136}