1use crate::{Error, Result};
10
11const DEFLATE_LEVEL: u8 = 9;
13
14const METHOD_STORE: u16 = 0;
15const METHOD_DEFLATE: u16 = 8;
16
17const DOS_TIME: u16 = 0;
20const DOS_DATE: u16 = 0x0021;
21
22const VERSION_MADE_BY: u16 = 20;
23const VERSION_STORE: u16 = 10;
24const VERSION_DEFLATE: u16 = 20;
25
26const SIG_LOCAL: u32 = 0x0403_4b50;
27const SIG_CENTRAL: u32 = 0x0201_4b50;
28const SIG_EOCD: u32 = 0x0605_4b50;
29
30const GPBF_UTF8: u16 = 1 << 11;
32
33const LOCAL_HEADER_LEN: usize = 30;
34const CENTRAL_HEADER_LEN: usize = 46;
35const EOCD_LEN: usize = 22;
36
37#[derive(Debug, Default)]
39pub struct ZipArchive {
40 body: Vec<u8>,
41 entries: Vec<CentralEntry>,
42}
43
44#[derive(Debug)]
45struct CentralEntry {
46 name: String,
47 method: u16,
48 version_needed: u16,
49 crc: u32,
50 compressed_size: u32,
51 uncompressed_size: u32,
52 local_offset: u32,
53 utf8: bool,
54}
55
56impl ZipArchive {
57 #[must_use]
59 pub fn new() -> Self {
60 Self::default()
61 }
62
63 pub fn store(&mut self, name: &str, data: &[u8]) -> Result<()> {
66 self.write_entry(name, data, METHOD_STORE, data, VERSION_STORE)
67 }
68
69 pub fn deflate(&mut self, name: &str, data: &[u8]) -> Result<()> {
72 let compressed = miniz_oxide::deflate::compress_to_vec(data, DEFLATE_LEVEL);
73 if compressed.len() < data.len() {
74 self.write_entry(name, data, METHOD_DEFLATE, &compressed, VERSION_DEFLATE)
75 } else {
76 self.store(name, data)
77 }
78 }
79
80 fn write_entry(
81 &mut self,
82 name: &str,
83 uncompressed: &[u8],
84 method: u16,
85 payload: &[u8],
86 version_needed: u16,
87 ) -> Result<()> {
88 let uncompressed_size =
89 u32::try_from(uncompressed.len()).map_err(|_| entry_too_large(name))?;
90 let compressed_size = u32::try_from(payload.len()).map_err(|_| entry_too_large(name))?;
91 let local_offset = u32::try_from(self.body.len()).map_err(|_| entry_too_large(name))?;
92 let name_bytes = name.as_bytes();
93 let name_len = u16::try_from(name_bytes.len()).map_err(|_| entry_too_large(name))?;
94 let utf8 = !name.is_ascii();
95 let flags = if utf8 { GPBF_UTF8 } else { 0 };
96 let crc = crc32(uncompressed);
97
98 self.body.extend_from_slice(&SIG_LOCAL.to_le_bytes());
99 self.body.extend_from_slice(&version_needed.to_le_bytes());
100 self.body.extend_from_slice(&flags.to_le_bytes());
101 self.body.extend_from_slice(&method.to_le_bytes());
102 self.body.extend_from_slice(&DOS_TIME.to_le_bytes());
103 self.body.extend_from_slice(&DOS_DATE.to_le_bytes());
104 self.body.extend_from_slice(&crc.to_le_bytes());
105 self.body.extend_from_slice(&compressed_size.to_le_bytes());
106 self.body
107 .extend_from_slice(&uncompressed_size.to_le_bytes());
108 self.body.extend_from_slice(&name_len.to_le_bytes());
109 self.body.extend_from_slice(&0u16.to_le_bytes());
110 self.body.extend_from_slice(name_bytes);
111 self.body.extend_from_slice(payload);
112
113 self.entries.push(CentralEntry {
114 name: name.to_owned(),
115 method,
116 version_needed,
117 crc,
118 compressed_size,
119 uncompressed_size,
120 local_offset,
121 utf8,
122 });
123 Ok(())
124 }
125
126 pub fn finish(self) -> Result<Vec<u8>> {
129 let mut out = self.body;
130 let central_offset = u32::try_from(out.len()).map_err(|_| archive_too_large())?;
131
132 for entry in &self.entries {
133 let name_bytes = entry.name.as_bytes();
134 let name_len = u16::try_from(name_bytes.len()).map_err(|_| archive_too_large())?;
135 let flags = if entry.utf8 { GPBF_UTF8 } else { 0 };
136 out.extend_from_slice(&SIG_CENTRAL.to_le_bytes());
137 out.extend_from_slice(&VERSION_MADE_BY.to_le_bytes());
138 out.extend_from_slice(&entry.version_needed.to_le_bytes());
139 out.extend_from_slice(&flags.to_le_bytes());
140 out.extend_from_slice(&entry.method.to_le_bytes());
141 out.extend_from_slice(&DOS_TIME.to_le_bytes());
142 out.extend_from_slice(&DOS_DATE.to_le_bytes());
143 out.extend_from_slice(&entry.crc.to_le_bytes());
144 out.extend_from_slice(&entry.compressed_size.to_le_bytes());
145 out.extend_from_slice(&entry.uncompressed_size.to_le_bytes());
146 out.extend_from_slice(&name_len.to_le_bytes());
147 out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&entry.local_offset.to_le_bytes());
153 out.extend_from_slice(name_bytes);
154 }
155
156 let central_end = u32::try_from(out.len()).map_err(|_| archive_too_large())?;
157 let central_size = central_end - central_offset;
158 let count = u16::try_from(self.entries.len()).map_err(|_| archive_too_large())?;
159
160 out.extend_from_slice(&SIG_EOCD.to_le_bytes());
161 out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&count.to_le_bytes());
164 out.extend_from_slice(&count.to_le_bytes());
165 out.extend_from_slice(¢ral_size.to_le_bytes());
166 out.extend_from_slice(¢ral_offset.to_le_bytes());
167 out.extend_from_slice(&0u16.to_le_bytes()); Ok(out)
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct ZipEntry {
175 pub name: String,
177 pub data: Vec<u8>,
179}
180
181pub fn read(archive: &[u8]) -> Result<Vec<ZipEntry>> {
187 let eocd =
188 find_eocd(archive).ok_or_else(|| corrupt("missing end-of-central-directory record"))?;
189 let count = u16_at(archive, eocd + 10)?;
190 let mut pos =
191 usize::try_from(u32_at(archive, eocd + 16)?).map_err(|_| corrupt("offset too large"))?;
192
193 let mut entries = Vec::with_capacity(usize::from(count));
194 for _ in 0..count {
195 if u32_at(archive, pos)? != SIG_CENTRAL {
196 return Err(corrupt("bad central-directory signature"));
197 }
198 let method = u16_at(archive, pos + 10)?;
199 let compressed_size =
200 usize::try_from(u32_at(archive, pos + 20)?).map_err(|_| corrupt("size too large"))?;
201 let uncompressed_size =
202 usize::try_from(u32_at(archive, pos + 24)?).map_err(|_| corrupt("size too large"))?;
203 let name_len = usize::from(u16_at(archive, pos + 28)?);
204 let extra_len = usize::from(u16_at(archive, pos + 30)?);
205 let comment_len = usize::from(u16_at(archive, pos + 32)?);
206 let local_offset =
207 usize::try_from(u32_at(archive, pos + 42)?).map_err(|_| corrupt("offset too large"))?;
208
209 let name_start = add(pos, CENTRAL_HEADER_LEN)?;
210 let name_bytes = archive
211 .get(name_start..add(name_start, name_len)?)
212 .ok_or_else(|| corrupt("truncated central-directory name"))?;
213 let name =
214 String::from_utf8(name_bytes.to_vec()).map_err(|_| corrupt("non-UTF-8 entry name"))?;
215
216 let local_name_len = usize::from(u16_at(archive, add(local_offset, 26)?)?);
217 let local_extra_len = usize::from(u16_at(archive, add(local_offset, 28)?)?);
218 let data_start = add(
219 add(add(local_offset, LOCAL_HEADER_LEN)?, local_name_len)?,
220 local_extra_len,
221 )?;
222 let raw = archive
223 .get(data_start..add(data_start, compressed_size)?)
224 .ok_or_else(|| corrupt("truncated entry data"))?;
225
226 let data = match method {
227 METHOD_STORE => raw.to_vec(),
228 METHOD_DEFLATE => {
232 let out =
233 miniz_oxide::inflate::decompress_to_vec_with_limit(raw, uncompressed_size)
234 .map_err(|_| corrupt("DEFLATE decode failed"))?;
235 if out.len() != uncompressed_size {
236 return Err(corrupt("DEFLATE size disagrees with declared size"));
237 }
238 out
239 }
240 other => return Err(corrupt(&format!("unsupported compression method {other}"))),
241 };
242 entries.push(ZipEntry { name, data });
243 pos = add(
244 add(add(add(pos, CENTRAL_HEADER_LEN)?, name_len)?, extra_len)?,
245 comment_len,
246 )?;
247 }
248 Ok(entries)
249}
250
251fn add(a: usize, b: usize) -> Result<usize> {
255 a.checked_add(b)
256 .ok_or_else(|| corrupt("archive offset out of range"))
257}
258
259fn find_eocd(buf: &[u8]) -> Option<usize> {
260 if buf.len() < EOCD_LEN {
261 return None;
262 }
263 let signature = SIG_EOCD.to_le_bytes();
264 let earliest = buf.len().saturating_sub(EOCD_LEN + usize::from(u16::MAX));
265 let mut at = buf.len() - EOCD_LEN;
266 loop {
267 if buf.get(at..at + 4) == Some(&signature[..]) {
268 return Some(at);
269 }
270 if at == earliest {
271 return None;
272 }
273 at -= 1;
274 }
275}
276
277fn u16_at(buf: &[u8], at: usize) -> Result<u16> {
278 let bytes = buf
279 .get(at..)
280 .and_then(|rest| rest.get(..2))
281 .ok_or_else(|| corrupt("truncated field"))?;
282 let array = <[u8; 2]>::try_from(bytes).map_err(|_| corrupt("truncated field"))?;
283 Ok(u16::from_le_bytes(array))
284}
285
286fn u32_at(buf: &[u8], at: usize) -> Result<u32> {
287 let bytes = buf
288 .get(at..)
289 .and_then(|rest| rest.get(..4))
290 .ok_or_else(|| corrupt("truncated field"))?;
291 let array = <[u8; 4]>::try_from(bytes).map_err(|_| corrupt("truncated field"))?;
292 Ok(u32::from_le_bytes(array))
293}
294
295fn crc32(data: &[u8]) -> u32 {
297 let mut crc: u32 = 0xFFFF_FFFF;
298 for &byte in data {
299 crc ^= u32::from(byte);
300 let mut bit = 0;
301 while bit < 8 {
302 let mask = (crc & 1).wrapping_neg();
303 crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
304 bit += 1;
305 }
306 }
307 !crc
308}
309
310fn entry_too_large(name: &str) -> Error {
311 Error::Container(format!(
312 "zip entry '{name}' exceeds 4 GiB (ZIP64 is unsupported)"
313 ))
314}
315
316fn archive_too_large() -> Error {
317 Error::Container("zip archive exceeds 4 GiB (ZIP64 is unsupported)".to_owned())
318}
319
320fn corrupt(detail: &str) -> Error {
321 Error::Container(format!("corrupt zip archive: {detail}"))
322}
323
324#[cfg(test)]
325mod tests {
326 use super::{ZipArchive, ZipEntry, crc32, read};
327
328 #[test]
329 fn crc32_matches_known_vector() {
330 assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
332 assert_eq!(crc32(b""), 0);
333 }
334
335 #[test]
336 fn stored_and_deflated_entries_round_trip() {
337 let mut archive = ZipArchive::new();
338 let text = b"hello, world; ".repeat(64);
339 archive
340 .store("mimetype", b"application/epub+zip")
341 .expect("store");
342 archive.deflate("EPUB/content.opf", &text).expect("deflate");
343 archive.deflate("small", b"x").expect("deflate small");
344 let bytes = archive.finish().expect("finish");
345
346 let entries = read(&bytes).expect("read");
347 assert_eq!(
348 entries,
349 vec![
350 ZipEntry {
351 name: "mimetype".to_owned(),
352 data: b"application/epub+zip".to_vec()
353 },
354 ZipEntry {
355 name: "EPUB/content.opf".to_owned(),
356 data: text
357 },
358 ZipEntry {
359 name: "small".to_owned(),
360 data: b"x".to_vec()
361 },
362 ]
363 );
364 }
365
366 #[test]
367 fn incompressible_data_falls_back_to_stored() {
368 let mut archive = ZipArchive::new();
370 archive.deflate("one", b"Z").expect("deflate");
371 let bytes = archive.finish().expect("finish");
372 let entries = read(&bytes).expect("read");
373 assert_eq!(entries.first().map(|e| e.data.clone()), Some(b"Z".to_vec()));
374 }
375
376 #[test]
377 fn output_is_reproducible() {
378 let build = || {
379 let mut archive = ZipArchive::new();
380 archive
381 .store("mimetype", b"application/epub+zip")
382 .expect("store");
383 archive
384 .deflate("a.txt", b"repeatable content ".repeat(16).as_slice())
385 .expect("deflate");
386 archive.finish().expect("finish")
387 };
388 assert_eq!(build(), build());
389 }
390
391 #[test]
392 fn read_rejects_truncated_archive() {
393 assert!(read(b"not a zip").is_err());
394 assert!(read(b"").is_err());
395 }
396
397 #[test]
398 fn read_rejects_deflate_exceeding_declared_size() {
399 let mut archive = ZipArchive::new();
403 archive
404 .deflate("big", b"AAAAAAAAAAAAAAAA".repeat(64).as_slice())
405 .expect("deflate");
406 let mut bytes = archive.finish().expect("finish");
407 let central = bytes
408 .windows(4)
409 .position(|window| window == [0x50, 0x4b, 0x01, 0x02])
410 .expect("central-directory header");
411 bytes
412 .get_mut(central + 24..central + 28)
413 .expect("uncompressed-size field")
414 .copy_from_slice(&4u32.to_le_bytes());
415 assert!(read(&bytes).is_err());
416 }
417}