1use pith_digest::{Error, Result, crc32};
20use pith_inflate::{Limits, inflate_raw};
21
22const SIG_LOCAL: u32 = 0x0403_4b50;
24const SIG_CENTRAL: u32 = 0x0201_4b50;
26const SIG_EOCD: u32 = 0x0605_4b50;
29const SIG_DESCRIPTOR: u32 = 0x0807_4b50;
32
33const METHOD_STORED: u16 = 0;
35const METHOD_DEFLATE: u16 = 8;
37
38const EOCD_LEN: usize = 22;
40const CENTRAL_LEN: usize = 46;
42const LOCAL_LEN: usize = 30;
44const EOCD_MAX_COMMENT: usize = 65_535;
47
48const FLAG_ENCRYPTED: u16 = 1 << 0;
52const FLAG_DESCRIPTOR: u16 = 1 << 3;
55const FLAG_STRONG_ENCRYPTED: u16 = 1 << 6;
58const FLAG_MASKED_LOCAL: u16 = 1 << 13;
61
62fn le16(data: &[u8], off: usize) -> Option<u16> {
64 let b = data.get(off..off + 2)?;
65 Some(u16::from_le_bytes([b[0], b[1]]))
66}
67
68fn le32(data: &[u8], off: usize) -> Option<u32> {
70 let b = data.get(off..off + 4)?;
71 Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
72}
73
74fn take<'a>(data: &'a [u8], off: usize, len: usize, what: &'static str) -> Result<&'a [u8]> {
76 let end = off
77 .checked_add(len)
78 .ok_or_else(|| Error::truncated(what, usize::MAX, data.len()))?;
79 data.get(off..end)
80 .ok_or_else(|| Error::truncated(what, end, data.len()))
81}
82
83struct Eocd {
85 entries: u16,
87 cd_size: u32,
89 cd_offset: u32,
91 offset: usize,
93}
94
95fn find_eocd(data: &[u8]) -> Result<Eocd> {
104 let start = data.len().saturating_sub(EOCD_LEN + EOCD_MAX_COMMENT);
105 let mut off = data.len().saturating_sub(EOCD_LEN);
109 loop {
110 if le32(data, off) == Some(SIG_EOCD) {
111 let comment_len = le16(data, off + 20).ok_or_else(|| {
112 Error::truncated("end of central directory", off + EOCD_LEN, data.len())
113 })? as usize;
114 if off + EOCD_LEN + comment_len == data.len() {
115 return parse_eocd(data, off);
116 }
117 }
118 if off == start {
119 return Err(Error::InvalidMagic {
120 what: "zip end of central directory",
121 });
122 }
123 off -= 1;
124 }
125}
126
127fn parse_eocd(data: &[u8], off: usize) -> Result<Eocd> {
130 let fields = take(data, off + 4, EOCD_LEN - 4, "end of central directory")?;
131 let disk = u16::from_le_bytes([fields[0], fields[1]]);
139 let cd_disk = u16::from_le_bytes([fields[2], fields[3]]);
140 let entries_disk = u16::from_le_bytes([fields[4], fields[5]]);
141 let entries = u16::from_le_bytes([fields[6], fields[7]]);
142 let cd_size = u32::from_le_bytes([fields[8], fields[9], fields[10], fields[11]]);
143 let cd_offset = u32::from_le_bytes([fields[12], fields[13], fields[14], fields[15]]);
144
145 if disk != 0 || cd_disk != 0 || entries_disk != entries {
146 return Err(Error::Unsupported("multi-disk zip archive"));
147 }
148 if entries == 0xFFFF || cd_size == 0xFFFF_FFFF || cd_offset == 0xFFFF_FFFF {
149 return Err(Error::Unsupported("zip64 archive"));
150 }
151 Ok(Eocd {
152 entries,
153 cd_size,
154 cd_offset,
155 offset: off,
156 })
157}
158
159#[derive(Copy, Clone, Debug)]
162pub struct ZipEntry<'a> {
163 name: &'a str,
164 flags: u16,
165 method: u16,
166 crc32: u32,
168 compressed_size: u32,
169 uncompressed_size: u32,
170 local_offset: u32,
172}
173
174impl<'a> ZipEntry<'a> {
175 pub fn name(&self) -> &'a str {
179 self.name
180 }
181
182 pub fn method(&self) -> u16 {
186 self.method
187 }
188
189 pub fn compressed_size(&self) -> u64 {
191 u64::from(self.compressed_size)
192 }
193
194 pub fn uncompressed_size(&self) -> u64 {
197 u64::from(self.uncompressed_size)
198 }
199
200 pub fn crc32(&self) -> u32 {
202 self.crc32
203 }
204
205 pub fn uses_data_descriptor(&self) -> bool {
209 self.flags & FLAG_DESCRIPTOR != 0
210 }
211}
212
213#[derive(Clone, Debug)]
217pub struct ZipArchive<'a> {
218 data: &'a [u8],
219 entries: Vec<ZipEntry<'a>>,
220}
221
222impl<'a> ZipArchive<'a> {
223 pub fn new(data: &'a [u8]) -> Result<Self> {
231 let eocd = find_eocd(data)?;
232
233 let cd_offset = usize::try_from(eocd.cd_offset).unwrap_or(usize::MAX);
234 let cd_size = usize::try_from(eocd.cd_size).unwrap_or(usize::MAX);
235 let cd_end = cd_offset
236 .checked_add(cd_size)
237 .ok_or_else(|| Error::truncated("zip central directory", usize::MAX, data.len()))?;
238 if cd_end > eocd.offset {
242 return Err(Error::truncated(
243 "zip central directory",
244 cd_end,
245 eocd.offset,
246 ));
247 }
248 let dir = take(data, cd_offset, cd_size, "zip central directory")?;
249
250 let mut entries = Vec::with_capacity(usize::from(eocd.entries));
251 let mut pos = 0usize;
252 for _ in 0..eocd.entries {
253 let fixed = take(dir, pos, CENTRAL_LEN, "zip central directory entry")?;
254 let magic = u32::from_le_bytes([fixed[0], fixed[1], fixed[2], fixed[3]]);
255 if magic != SIG_CENTRAL {
256 return Err(Error::InvalidMagic {
257 what: "zip central directory entry",
258 });
259 }
260 let flags = u16::from_le_bytes([fixed[8], fixed[9]]);
261 let method = u16::from_le_bytes([fixed[10], fixed[11]]);
262 let crc32 = u32::from_le_bytes([fixed[16], fixed[17], fixed[18], fixed[19]]);
263 let compressed_size = u32::from_le_bytes([fixed[20], fixed[21], fixed[22], fixed[23]]);
264 let uncompressed_size =
265 u32::from_le_bytes([fixed[24], fixed[25], fixed[26], fixed[27]]);
266 let name_len = usize::from(u16::from_le_bytes([fixed[28], fixed[29]]));
267 let extra_len = usize::from(u16::from_le_bytes([fixed[30], fixed[31]]));
268 let comment_len = usize::from(u16::from_le_bytes([fixed[32], fixed[33]]));
269 let local_offset = u32::from_le_bytes([fixed[42], fixed[43], fixed[44], fixed[45]]);
270
271 if compressed_size == 0xFFFF_FFFF
272 || uncompressed_size == 0xFFFF_FFFF
273 || local_offset == 0xFFFF_FFFF
274 {
275 return Err(Error::Unsupported("zip64 central directory entry"));
276 }
277
278 let name_bytes = take(
279 dir,
280 pos + CENTRAL_LEN,
281 name_len,
282 "zip entry name in central directory",
283 )?;
284 let name = core::str::from_utf8(name_bytes)
285 .map_err(|_| Error::BadValue("zip entry name is not UTF-8"))?;
286
287 let entry_len = CENTRAL_LEN
288 .checked_add(name_len)
289 .and_then(|n| n.checked_add(extra_len))
290 .and_then(|n| n.checked_add(comment_len))
291 .ok_or(Error::BadValue("zip central directory entry length"))?;
292 if entry_len > dir.len() - pos {
293 return Err(Error::truncated(
294 "zip central directory entry",
295 pos + entry_len,
296 dir.len(),
297 ));
298 }
299 entries.push(ZipEntry {
300 name,
301 flags,
302 method,
303 crc32,
304 compressed_size,
305 uncompressed_size,
306 local_offset,
307 });
308 pos += entry_len;
309 }
310 if pos != dir.len() {
313 return Err(Error::BadValue("zip central directory size"));
314 }
315 Ok(ZipArchive { data, entries })
316 }
317
318 pub fn entries(&self) -> &[ZipEntry<'a>] {
320 &self.entries
321 }
322
323 pub fn len(&self) -> usize {
325 self.entries.len()
326 }
327
328 pub fn is_empty(&self) -> bool {
330 self.entries.is_empty()
331 }
332
333 pub fn by_name(&self, name: &str) -> Option<&ZipEntry<'a>> {
336 self.entries.iter().find(|e| e.name == name)
337 }
338
339 pub fn extract(&self, entry: &ZipEntry<'a>) -> Result<Vec<u8>> {
350 self.extract_with(entry, &Limits::default())
351 }
352
353 pub fn extract_by_name(&self, name: &str) -> Result<Vec<u8>> {
357 let entry = self
358 .by_name(name)
359 .ok_or(Error::BadValue("no such zip entry"))?;
360 self.extract(entry)
361 }
362
363 pub fn extract_with(&self, entry: &ZipEntry<'a>, limits: &Limits) -> Result<Vec<u8>> {
368 let base = usize::try_from(entry.local_offset)
369 .map_err(|_| Error::BadValue("zip local header offset"))?;
370 let hdr = take(self.data, base, LOCAL_LEN, "zip local file header")?;
371 let magic = u32::from_le_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]);
372 if magic != SIG_LOCAL {
373 return Err(Error::InvalidMagic {
374 what: "zip local file header",
375 });
376 }
377
378 let local_flags = u16::from_le_bytes([hdr[6], hdr[7]]);
382 let local_method = u16::from_le_bytes([hdr[8], hdr[9]]);
383 if local_flags != entry.flags {
384 return Err(Error::BadValue("zip flags differ local vs central"));
385 }
386 if local_method != entry.method {
387 return Err(Error::BadValue("zip method differs local vs central"));
388 }
389
390 let name_len = usize::from(u16::from_le_bytes([hdr[26], hdr[27]]));
391 let extra_len = usize::from(u16::from_le_bytes([hdr[28], hdr[29]]));
392 let data_start = base
393 .checked_add(LOCAL_LEN)
394 .and_then(|n| n.checked_add(name_len))
395 .and_then(|n| n.checked_add(extra_len))
396 .ok_or(Error::BadValue("zip local header offset"))?;
397
398 if entry.flags & (FLAG_ENCRYPTED | FLAG_STRONG_ENCRYPTED | FLAG_MASKED_LOCAL) != 0 {
399 return Err(Error::Unsupported("encrypted zip entry"));
400 }
401
402 let cs = usize::try_from(entry.compressed_size).unwrap_or(usize::MAX);
403 let payload = take(self.data, data_start, cs, "zip entry payload")?;
404
405 if entry.flags & FLAG_DESCRIPTOR != 0 {
406 let dd_at = data_start
407 .checked_add(cs)
408 .ok_or(Error::BadValue("zip data descriptor offset"))?;
409 self.verify_descriptor(entry, dd_at)?;
410 }
411
412 let out = match entry.method {
413 METHOD_STORED => {
414 if entry.uncompressed_size != entry.compressed_size {
415 return Err(Error::BadValue("zip stored entry size"));
416 }
417 if cs > limits.max_output {
418 return Err(Error::too_large("zip entry", limits.max_output));
419 }
420 payload.to_vec()
421 }
422 METHOD_DEFLATE => inflate_raw(payload, limits)?,
423 _ => return Err(Error::Unsupported("zip compression method")),
424 };
425
426 if out.len() as u64 != u64::from(entry.uncompressed_size) {
427 return Err(Error::BadValue("zip entry size mismatch"));
428 }
429 if crc32(&out) != entry.crc32 {
430 return Err(Error::BadValue("zip entry crc32 mismatch"));
431 }
432 Ok(out)
433 }
434
435 fn verify_descriptor(&self, entry: &ZipEntry<'a>, at: usize) -> Result<()> {
440 let base = if le32(self.data, at) == Some(SIG_DESCRIPTOR) {
441 at + 4
442 } else {
443 at
444 };
445 let dd = take(self.data, base, 12, "zip data descriptor")?;
446 let dd_crc = u32::from_le_bytes([dd[0], dd[1], dd[2], dd[3]]);
447 let dd_cs = u32::from_le_bytes([dd[4], dd[5], dd[6], dd[7]]);
448 let dd_us = u32::from_le_bytes([dd[8], dd[9], dd[10], dd[11]]);
449 if dd_crc == entry.crc32
450 && dd_cs == entry.compressed_size
451 && dd_us == entry.uncompressed_size
452 {
453 Ok(())
454 } else {
455 Err(Error::BadValue("zip data descriptor mismatch"))
456 }
457 }
458}