1use std::{
4 borrow::Cow,
5 io::{self, BufRead, Read, Seek},
6};
7
8use crate::{CompressionMethod, Decompressor, FileType, Timestamp, types, utils};
9
10mod extra_field;
11mod raw;
12
13use extra_field::{ExtraField, ExtraFields};
14
15#[cold]
16fn invalid(msg: &str) -> io::Error {
17 io::Error::new(io::ErrorKind::InvalidData, msg)
18}
19
20#[cold]
21fn encrypted_file() -> io::Error {
22 io::Error::new(io::ErrorKind::Unsupported, "encrypted file")
23}
24
25#[cold]
26fn compressed() -> io::Error {
27 io::Error::new(io::ErrorKind::Unsupported, "compressed file")
28}
29
30trait ReadSeek: Read + Seek {}
31impl<R: Read + Seek> ReadSeek for R {}
32
33trait BufReadSeek: BufRead + Seek {}
34impl<R: BufRead + Seek> BufReadSeek for R {}
35
36#[derive(Debug, Clone, Copy)]
44#[non_exhaustive]
45pub enum EncryptionMethod {
46 ZipCrypto,
48 StrongEncrytion,
50 Aes {
55 key_size: u16,
57 check_crc32: bool,
61 },
62}
63
64pub struct RawArchive {
66 entries: Vec<Metadata>,
67 names: utils::NameTable<usize>,
68 comment: Box<[u8]>,
69}
70
71impl std::fmt::Debug for RawArchive {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 f.debug_struct("RawArchive")
74 .field("entries", &self.entries)
75 .field("comment", &String::from_utf8_lossy(&self.comment))
76 .finish()
77 }
78}
79
80impl RawArchive {
81 #[inline]
85 pub fn new<R: Read + Seek>(reader: &mut R) -> io::Result<Self> {
86 raw::read_archive(reader)
87 }
88
89 #[inline]
91 pub fn entries(&self) -> &[Metadata] {
92 &self.entries
93 }
94
95 #[inline]
97 pub fn comment(&self) -> &[u8] {
98 &self.comment
99 }
100
101 #[inline]
103 pub fn get_by_name(&mut self, name: &str) -> Option<&Metadata> {
104 let index = self.index_of(name)?;
105 self.entries.get(index)
106 }
107
108 pub fn index_of(&self, name: &str) -> Option<usize> {
110 let stripped_name = name.strip_suffix('/');
111 let name = stripped_name.unwrap_or(name);
112
113 let index = *self
114 .names
115 .get(name.as_bytes(), |i| self.entries[*i].stripped_name())?;
116
117 if self.entries[index].file_type.is_directory() != stripped_name.is_some() {
121 return None;
122 }
123
124 Some(index)
125 }
126
127 pub fn extract<R: BufRead + Seek>(
131 &self,
132 reader: &mut R,
133 at: &std::path::Path,
134 ) -> io::Result<()> {
135 match std::fs::create_dir(at) {
136 Ok(()) => (),
137 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
138 Err(err) => return Err(err),
139 };
140
141 for entry in &self.entries {
142 entry.extract(reader, at)?;
143 }
144
145 Ok(())
146 }
147
148 #[cfg(feature = "parallel")]
155 pub fn parallel_extract<R: sync_file::ReadAt + sync_file::Size + Sync>(
156 &self,
157 reader: &R,
158 at: &std::path::Path,
159 ) -> io::Result<()> {
160 use rayon::prelude::*;
161
162 match std::fs::create_dir(at) {
163 Ok(()) => (),
164 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
165 Err(err) => return Err(err),
166 };
167
168 self.entries.par_iter().try_for_each_init(
169 || io::BufReader::new(sync_file::Adapter::new(reader)),
170 |reader, entry| entry.extract(reader, at),
171 )?;
172
173 Ok(())
174 }
175}
176
177impl FileType {
178 fn test(attr: u32, name: &str) -> Option<Self> {
179 let dos_attr = attr as u16;
180 let unix_mode = (attr >> 16) as u16;
181 let unix_kind = unix_mode >> 12;
182
183 let is_file = (dos_attr & (1 << 5)) != 0 || unix_kind == 8;
184 let is_dir = (dos_attr & (1 << 4)) != 0 || unix_kind == 4;
185 let is_symlink = unix_kind == 10;
186 let trailing_slash = name.ends_with('/');
187
188 match (is_file, is_dir, trailing_slash, is_symlink) {
189 (_, false, false, false) => Some(FileType::File),
190 (false, _, true, false) => Some(FileType::Directory),
191 (false, false, false, true) => Some(FileType::Symlink),
192 _ => None,
193 }
194 }
195}
196
197fn convert_string(raw: &[u8], force_unicode: bool) -> Option<(Cow<'_, str>, Option<u32>)> {
198 if let Ok(name) = str::from_utf8(raw) {
202 return Some((Cow::Borrowed(name), None));
203 }
204
205 if force_unicode {
207 None
208 } else {
209 let name = utils::cp437::convert(raw);
210 Some((Cow::Owned(name), Some(crc32fast::hash(raw))))
211 }
212}
213
214#[derive(Debug)]
216pub struct Metadata {
217 header_offset: u64,
218 pub data_offset: u64,
219
220 pub compressed_size: u64,
221 pub uncompressed_size: u64,
222 pub compression_method: CompressionMethod,
223 pub crc32: u32,
224 pub file_type: FileType,
225
226 pub modification_time: Option<Timestamp>,
227 pub access_time: Option<Timestamp>,
228 pub creation_time: Option<Timestamp>,
229
230 pub encryption: Option<EncryptionMethod>,
231
232 name: Box<str>,
233 comment: Box<str>,
234
235 is_streaming: bool,
236 is_zip64: bool,
237 flags: u16,
238}
239
240impl Metadata {
241 fn from_local_header(
242 header: types::LocalFileHeader,
243 file_name: &[u8],
244 extra_fields: &[u8],
245 ) -> Option<Self> {
246 let flags = header.flags.get();
247 let is_encrypted = flags & (1 << 0) != 0;
248 let is_streaming = flags & (1 << 3) != 0;
249 let strong_encryption = flags & (1 << 6) != 0;
250 let is_unicode = flags & (1 << 11) != 0;
251
252 if { header.signature } != types::LocalFileHeader::SIGNATURE {
253 return None;
254 }
255
256 let (name, name_crc) = convert_string(file_name, is_unicode)?;
257 let name = utils::validate_name(&name)?;
258
259 let encryption = match (is_encrypted, strong_encryption) {
260 (false, false) => None,
261 (false, true) => return None,
262 (true, false) => Some(EncryptionMethod::ZipCrypto),
263 (true, true) => Some(EncryptionMethod::StrongEncrytion),
264 };
265
266 let mut meta = Self {
267 crc32: header.crc32.get(),
268 encryption,
269 header_offset: 0,
270 data_offset: 0,
271
272 compressed_size: header.compressed_size.get() as u64,
273 uncompressed_size: header.uncompressed_size.get() as u64,
274 compression_method: CompressionMethod(header.compression_method.get()),
275 file_type: FileType::File,
276
277 modification_time: None,
278 access_time: None,
279 creation_time: None,
280
281 name,
282 comment: Box::default(),
283
284 is_streaming,
285 is_zip64: false,
286 flags,
287 };
288
289 meta.parse_extra_fields(ExtraFields(extra_fields), name_crc, None)?;
290
291 Some(meta)
292 }
293
294 fn from_central_header(
295 header: types::CentralFileHeader,
296 file_name: &[u8],
297 extra_fields: &[u8],
298 comment: &[u8],
299 ) -> Option<Self> {
300 let flags = header.flags.get();
301 let is_encrypted = flags & (1 << 0) != 0;
302 let is_streaming = flags & (1 << 3) != 0;
303 let strong_encryption = flags & (1 << 6) != 0;
304 let is_unicode = flags & (1 << 11) != 0;
305
306 if { header.signature } != types::CentralFileHeader::SIGNATURE
307 || header.disk_number.get() != 0
308 {
309 return None;
310 }
311
312 let (comment, comment_crc) = convert_string(comment, is_unicode)?;
313 let comment = comment.into_owned().into_boxed_str();
314 let (name, name_crc) = convert_string(file_name, is_unicode)?;
315 let name = utils::validate_name(&name)?;
316 let file_type = FileType::test(header.external_attributes.get(), &name)?;
317
318 let encryption = match (is_encrypted, strong_encryption) {
319 (false, false) => None,
320 (false, true) => return None,
321 (true, false) => Some(EncryptionMethod::ZipCrypto),
322 (true, true) => Some(EncryptionMethod::StrongEncrytion),
323 };
324
325 let mut meta = Self {
326 crc32: header.crc32.get(),
327 encryption,
328 header_offset: header.local_header_offset.get() as u64,
329 data_offset: 0,
330
331 compressed_size: header.compressed_size.get() as u64,
332 uncompressed_size: header.uncompressed_size.get() as u64,
333 compression_method: CompressionMethod(header.compression_method.get()),
334 file_type,
335
336 modification_time: None,
337 access_time: None,
338 creation_time: None,
339
340 name,
341 comment,
342
343 is_streaming,
344 is_zip64: false,
345 flags,
346 };
347
348 meta.parse_extra_fields(ExtraFields(extra_fields), name_crc, comment_crc)?;
349
350 Some(meta)
351 }
352
353 fn parse_extra_fields(
354 &mut self,
355 extra_fields: ExtraFields,
356 name_crc: Option<u32>,
357 comment_crc: Option<u32>,
358 ) -> Option<()> {
359 for field in extra_fields.iter() {
360 match field {
361 ExtraField::Zip64ExtendedInformation(mut info) => {
362 if self.uncompressed_size == 0xffff_ffff {
363 self.uncompressed_size = info.next()?;
364 }
365 if self.compressed_size == 0xffff_ffff {
366 self.compressed_size = info.next()?;
367 }
368 if self.header_offset == 0xffff_ffff {
369 self.header_offset = info.next()?;
370 }
371 info.end()?;
373 self.is_zip64 = true;
374 }
375 ExtraField::UnicodeComment(unicode) => {
376 if Some(unicode.header_comment_crc32) != comment_crc {
377 return None;
378 }
379 self.comment = unicode.comment.into();
380 }
381
382 ExtraField::UnicodeName(unicode) => {
383 if Some(unicode.header_name_crc32) != name_crc {
384 return None;
385 }
386 self.name = utils::validate_name(unicode.name)?;
387 }
388
389 ExtraField::Ntfs(ntfs) => {
390 self.modification_time = ntfs.times.mtime;
391 self.access_time = ntfs.times.atime;
392 self.creation_time = ntfs.times.ctime;
393 }
394
395 ExtraField::ExtendedTimestamp(ts) => {
396 self.modification_time = ts.modification_time;
397 self.access_time = ts.access_time;
398 self.creation_time = ts.creation_time;
399 }
400
401 ExtraField::Aes(aes) => {
402 if self.compression_method != CompressionMethod::AES
403 || (!aes.check_crc32 && self.crc32 != 0)
404 {
405 return None;
406 }
407 let Some(enc @ EncryptionMethod::ZipCrypto) = &mut self.encryption else {
408 return None;
409 };
410
411 *enc = EncryptionMethod::Aes {
412 key_size: aes.key_size,
413 check_crc32: aes.check_crc32,
414 };
415 self.compression_method = aes.compression;
416 }
417
418 ExtraField::Invalid => return None,
419
420 _ => (),
421 }
422 }
423
424 if self.compression_method == CompressionMethod::AES {
425 return None;
426 }
427
428 Some(())
429 }
430
431 #[inline]
433 pub fn is_encrypted(&self) -> bool {
434 self.encryption.is_some()
435 }
436
437 #[inline]
439 pub fn name(&self) -> &str {
440 &self.name
441 }
442
443 fn stripped_name(&self) -> &[u8] {
444 self.name.strip_suffix('/').unwrap_or(&self.name).as_bytes()
445 }
446
447 #[inline]
449 pub fn comment(&self) -> &str {
450 &self.comment
451 }
452
453 pub fn read<R: BufRead + Seek>(&self, reader: R) -> io::Result<impl Read + use<R>> {
457 if self.encryption.is_some() {
458 return Err(encrypted_file());
459 }
460
461 let reader = Decompressor::new(self.read_raw(reader)?, self.compression_method)?;
462 Ok(self.content_checker(reader))
463 }
464
465 pub fn read_stored<R: Read + Seek>(&self, mut reader: R) -> io::Result<io::Take<R>> {
473 if self.encryption.is_some() {
474 return Err(encrypted_file());
475 }
476 if self.compression_method != CompressionMethod::STORE {
477 return Err(compressed());
478 }
479
480 let mut checker = utils::Crc32Checker::new(self.read_raw(&mut reader)?, self.crc32);
482 std::io::copy(&mut checker, &mut io::sink())?;
483
484 self.read_raw(reader)
485 }
486
487 pub fn read_raw<R: Read + Seek>(&self, mut reader: R) -> io::Result<io::Take<R>> {
491 reader.seek(io::SeekFrom::Start(self.data_offset))?;
492 Ok(reader.take(self.compressed_size))
493 }
494
495 #[inline]
499 pub fn content_checker<R: Read>(&self, reader: R) -> impl Read + use<R> {
500 utils::Crc32Checker::new(
501 utils::LengthChecker::new(reader, self.uncompressed_size),
502 self.crc32,
503 )
504 }
505
506 #[inline]
508 pub fn extract<R: BufRead + Seek>(
509 &self,
510 reader: &mut R,
511 root: impl AsRef<std::path::Path>,
512 ) -> io::Result<()> {
513 self._extract(reader, root.as_ref())
514 }
515
516 fn _extract(&self, reader: &mut dyn BufReadSeek, at: &std::path::Path) -> io::Result<()> {
517 if !std::fs::metadata(at)?.is_dir() {
518 return Err(io::Error::from(io::ErrorKind::NotFound));
519 }
520
521 let path = at.join(&*self.name);
522 std::fs::create_dir_all(path.parent().unwrap())?;
523
524 match self.file_type {
525 FileType::File => {
526 let mut f = std::fs::File::create_new(&path)?;
527 io::copy(&mut self.read(reader)?, &mut f)?;
528
529 if let Some(mod_time) = self.modification_time {
530 f.set_times(std::fs::FileTimes::new().set_modified(mod_time.to_std()))?;
531 }
532 }
533 FileType::Directory => {
534 std::fs::create_dir(path)?;
535 }
536 FileType::Symlink => {
537 let target = io::read_to_string(self.read(reader)?)?;
538 if !utils::validate_symlink(&self.name, &target) {
539 return Err(invalid("invalid symlink target"));
540 }
541
542 #[cfg(unix)]
543 std::os::unix::fs::symlink(target, path)?;
544
545 #[cfg(windows)]
546 if target.ends_with('/') {
547 std::os::windows::fs::symlink_dir(target, path)?;
548 } else {
549 std::os::windows::fs::symlink_file(target, path)?;
550 }
551
552 #[cfg(not(any(unix, windows)))]
553 std::fs::write(path, target.as_bytes())?;
554 }
555 }
556
557 Ok(())
558 }
559}
560
561#[derive(Debug)]
584pub struct ArchiveReader<R> {
585 inner: RawArchive,
586 reader: R,
587}
588
589impl ArchiveReader<io::BufReader<std::fs::File>> {
590 #[inline]
592 pub fn open(path: impl AsRef<std::path::Path>) -> io::Result<Self> {
593 Self::_open(path.as_ref())
594 }
595
596 fn _open(path: &std::path::Path) -> io::Result<Self> {
597 Self::new(io::BufReader::new(std::fs::File::open(path)?))
598 }
599}
600
601#[cfg(feature = "parallel")]
602impl ArchiveReader<io::BufReader<sync_file::SyncFile>> {
603 #[inline]
605 pub fn open_parallel(path: impl AsRef<std::path::Path>) -> io::Result<Self> {
606 Self::_open(path.as_ref())
607 }
608
609 fn _open(path: &std::path::Path) -> io::Result<Self> {
610 Self::new(io::BufReader::new(sync_file::SyncFile::open(path)?))
611 }
612}
613
614impl<R: Read + Seek> ArchiveReader<R> {
615 pub fn new(mut reader: R) -> io::Result<Self> {
627 let inner = RawArchive::new(&mut reader)?;
628 Ok(Self { inner, reader })
629 }
630
631 #[inline]
633 pub fn entries(&self) -> &[Metadata] {
634 &self.inner.entries
635 }
636
637 #[inline]
639 pub fn get_by_index(&mut self, index: usize) -> Option<File<'_, R>> {
640 let metadata = self.inner.entries().get(index)?;
641 Some(File {
642 metadata,
643 reader: &mut self.reader,
644 })
645 }
646
647 pub fn get_by_name(&mut self, name: &str) -> Option<File<'_, R>> {
649 let index = self.index_of(name)?;
650 self.get_by_index(index)
651 }
652
653 pub fn index_of(&self, name: &str) -> Option<usize> {
655 self.inner.index_of(name)
656 }
657
658 #[inline]
660 pub fn commment(&self) -> &[u8] {
661 &self.inner.comment
662 }
663
664 #[inline]
668 pub fn extract(&mut self, at: impl AsRef<std::path::Path>) -> io::Result<()>
669 where
670 R: BufRead,
671 {
672 self.inner.extract(&mut self.reader, at.as_ref())
673 }
674
675 #[cfg(feature = "parallel")]
682 #[inline]
683 pub fn parallel_extract(&self, at: impl AsRef<std::path::Path>) -> io::Result<()>
684 where
685 R: sync_file::ReadAt + sync_file::Size + Sync,
686 {
687 self.inner.parallel_extract(&self.reader, at.as_ref())
688 }
689
690 #[inline]
692 pub fn get_ref(&self) -> &R {
693 &self.reader
694 }
695
696 #[inline]
698 pub fn get_mut(&mut self) -> &mut R {
699 &mut self.reader
700 }
701}
702
703pub type Archive<R> = ArchiveReader<R>;
705
706#[derive(Debug)]
708pub struct File<'a, R> {
709 metadata: &'a Metadata,
710 reader: &'a mut R,
711}
712
713impl<'a, R: Read + Seek> File<'a, R> {
714 #[inline]
719 pub fn metadata(&self) -> &'a Metadata {
720 self.metadata
721 }
722
723 #[inline]
727 pub fn read(&mut self) -> io::Result<impl Read + '_>
728 where
729 R: BufRead,
730 {
731 self.metadata.read(&mut *self.reader)
732 }
733
734 pub fn read_stored(self) -> io::Result<io::Take<&'a mut R>> {
742 self.metadata.read_stored(self.reader)
743 }
744
745 #[inline]
749 pub fn read_raw(&mut self) -> io::Result<io::Take<&mut R>> {
750 self.metadata.read_raw(self.reader)
751 }
752
753 pub fn into_reader(self) -> &'a mut R {
759 self.reader
760 }
761}