1pub mod mem;
7pub mod seek;
8pub mod stream;
9
10pub mod cd;
11mod counting;
12pub(crate) mod io;
13
14use crate::ZipString;
15pub use crate::base::read::io::entry::WithEntry;
17pub use crate::base::read::io::entry::WithoutEntry;
18pub use crate::base::read::io::entry::ZipEntryReader;
19
20use crate::date::ZipDateTime;
21use crate::entry::{StoredZipEntry, ZipEntry};
22use crate::error::{Result, ZipError};
23use crate::file::ZipFile;
24use crate::spec::attribute::AttributeCompatibility;
25use crate::spec::consts::{CDDS_LENGTH, CDDS_SIGNATURE, CDH_LENGTH, LFH_LENGTH};
26use crate::spec::consts::{
27 CDH_SIGNATURE, LFH_SIGNATURE, NON_ZIP64_MAX_SIZE, SIGNATURE_LENGTH, ZIP64_EOCDL_LENGTH, ZIP64_EOCDR_SIGNATURE,
28};
29use crate::spec::header::InfoZipUnicodeCommentExtraField;
30use crate::spec::header::InfoZipUnicodePathExtraField;
31use crate::spec::header::{
32 CentralDirectoryRecord, EndOfCentralDirectoryHeader, ExtraField, LocalFileHeader,
33 Zip64EndOfCentralDirectoryLocator, Zip64EndOfCentralDirectoryRecord, Zip64ExtendedInformationExtraField,
34};
35use crate::spec::Compression;
36use crate::string::StringEncoding;
37
38use crate::base::read::io::CombinedCentralDirectoryRecord;
39use crate::spec::parse::parse_extra_fields;
40
41use futures_lite::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, BufReader, SeekFrom};
42
43const MAX_CD_BUFFER_SIZE: usize = 20 * 1024 * 1024;
45const MIN_CENTRAL_DIRECTORY_ENTRY_SIZE: u64 = (SIGNATURE_LENGTH + CDH_LENGTH) as u64;
46
47pub(crate) async fn file<R>(mut reader: R) -> Result<ZipFile>
48where
49 R: AsyncRead + AsyncSeek + Unpin,
50{
51 let eocdr_offset = crate::base::read::io::locator::eocdr(&mut reader).await?;
53 let eocdr_record_offset = eocdr_offset.saturating_sub(SIGNATURE_LENGTH as u64);
54
55 reader.seek(SeekFrom::Start(eocdr_offset)).await?;
56 let eocdr = EndOfCentralDirectoryHeader::from_reader(&mut reader).await?;
57
58 let comment = io::read_string(&mut reader, eocdr.file_comm_length.into(), crate::StringEncoding::Utf8).await?;
59
60 let (eocdr, zip64, central_directory_boundary) =
63 match eocdr_offset.checked_sub(ZIP64_EOCDL_LENGTH + SIGNATURE_LENGTH as u64) {
64 None => (CombinedCentralDirectoryRecord::try_from(&eocdr)?, false, eocdr_record_offset),
65 Some(offset) => {
66 reader.seek(SeekFrom::Start(offset)).await?;
67 let zip64_locator = Zip64EndOfCentralDirectoryLocator::try_from_reader(&mut reader).await?;
68
69 match zip64_locator {
70 Some(locator) => {
71 reader.seek(SeekFrom::Start(locator.relative_offset)).await?;
72 let signature = {
73 let mut buffer = [0; SIGNATURE_LENGTH];
74 reader.read_exact(&mut buffer).await?;
75 u32::from_le_bytes(buffer)
76 };
77 if signature != ZIP64_EOCDR_SIGNATURE {
78 return Err(ZipError::UnexpectedHeaderError(signature, ZIP64_EOCDR_SIGNATURE));
79 }
80 let zip64_eocdr = Zip64EndOfCentralDirectoryRecord::from_reader(&mut reader).await?;
81 validate_zip64_end_record_binding(&zip64_eocdr, locator.relative_offset, offset)?;
82 validate_zip64_entry_count(&zip64_eocdr, locator.relative_offset)?;
83 (CombinedCentralDirectoryRecord::combine(eocdr, zip64_eocdr)?, true, locator.relative_offset)
84 }
85 None => (CombinedCentralDirectoryRecord::try_from(&eocdr)?, false, eocdr_record_offset),
86 }
87 }
88 };
89
90 validate_central_directory_range(&eocdr, central_directory_boundary)?;
91
92 if eocdr.disk_number != eocdr.disk_number_start_of_cd
94 || eocdr.num_entries_in_directory != eocdr.num_entries_in_directory_on_disk
95 {
96 return Err(ZipError::FeatureNotSupported("Spanned/split files"));
97 }
98
99 reader.seek(SeekFrom::Start(eocdr.offset_of_start_of_directory)).await?;
101
102 let mut buf =
105 BufReader::with_capacity(std::cmp::min(eocdr.offset_of_start_of_directory as _, MAX_CD_BUFFER_SIZE), reader);
106 let mut entries = crate::base::read::cd(
107 &mut buf,
108 eocdr.num_entries_in_directory,
109 eocdr.offset_of_start_of_directory,
110 eocdr.directory_size,
111 zip64,
112 )
113 .await?;
114 validate_central_directory_binding(&eocdr, central_directory_boundary)?;
115 assign_entry_data_boundaries(&mut entries, eocdr.offset_of_start_of_directory);
116
117 Ok(ZipFile { entries, comment, zip64 })
118}
119
120fn validate_zip64_entry_count(zip64_eocdr: &Zip64EndOfCentralDirectoryRecord, zip64_eocdr_offset: u64) -> Result<()> {
121 let minimum_central_directory_end = zip64_eocdr
122 .num_entries_in_directory
123 .saturating_mul(MIN_CENTRAL_DIRECTORY_ENTRY_SIZE)
124 .saturating_add(zip64_eocdr.offset_of_start_of_directory);
125
126 if zip64_eocdr_offset < minimum_central_directory_end {
127 return Err(ZipError::InvalidCentralDirectoryEntryCount { entries: zip64_eocdr.num_entries_in_directory });
128 }
129
130 Ok(())
131}
132
133fn validate_zip64_end_record_binding(
138 zip64_eocdr: &Zip64EndOfCentralDirectoryRecord,
139 zip64_eocdr_offset: u64,
140 zip64_locator_offset: u64,
141) -> Result<()> {
142 let expected_locator_offset = zip64_eocdr_offset
143 .checked_add(SIGNATURE_LENGTH as u64 + 8)
144 .and_then(|offset| offset.checked_add(zip64_eocdr.size_of_zip64_end_of_cd_record))
145 .ok_or(ZipError::InvalidZip64EndOfCentralDirectoryLocatorOffset(u64::MAX, zip64_locator_offset))?;
146
147 if expected_locator_offset != zip64_locator_offset {
148 return Err(ZipError::InvalidZip64EndOfCentralDirectoryLocatorOffset(
149 expected_locator_offset,
150 zip64_locator_offset,
151 ));
152 }
153
154 Ok(())
155}
156
157fn validate_central_directory_binding(eocdr: &CombinedCentralDirectoryRecord, boundary: u64) -> Result<()> {
161 let end = eocdr
162 .offset_of_start_of_directory
163 .checked_add(eocdr.directory_size)
164 .ok_or(ZipError::InvalidCentralDirectoryBinding { directory_end: u64::MAX, end_record: boundary })?;
165
166 if end != boundary {
167 return Err(ZipError::InvalidCentralDirectoryBinding { directory_end: end, end_record: boundary });
168 }
169
170 Ok(())
171}
172
173fn validate_central_directory_range(eocdr: &CombinedCentralDirectoryRecord, boundary: u64) -> Result<()> {
175 let start = eocdr.offset_of_start_of_directory;
176 let end = start.checked_add(eocdr.directory_size).ok_or(ZipError::InvalidCentralDirectoryRange {
177 start,
178 end: u64::MAX,
179 boundary,
180 })?;
181
182 if end > boundary {
183 return Err(ZipError::InvalidCentralDirectoryRange { start, end, boundary });
184 }
185
186 Ok(())
187}
188
189fn cd_entry_capacity(num_of_entries: usize, directory_start: u64) -> Result<usize> {
190 let directory_start = usize::try_from(directory_start).unwrap_or(usize::MAX);
191 let capacity = if num_of_entries > directory_start { 0 } else { num_of_entries };
192
193 if capacity.saturating_mul(std::mem::size_of::<StoredZipEntry>()) > isize::MAX as usize {
194 return Err(ZipError::FeatureNotSupported("Oversized central directory"));
195 }
196
197 Ok(capacity)
198}
199
200fn assign_entry_data_boundaries(entries: &mut [StoredZipEntry], directory_start: u64) {
201 let mut local_headers: Vec<_> = entries.iter().map(|entry| entry.file_offset).collect();
202 local_headers.sort_unstable();
203
204 for entry in entries {
205 entry.data_end_boundary = local_headers
206 .get(local_headers.partition_point(|offset| *offset <= entry.file_offset))
207 .copied()
208 .unwrap_or(directory_start)
209 .min(directory_start);
210 }
211}
212
213pub(crate) async fn cd<R>(
217 reader: R,
218 num_of_entries: u64,
219 directory_start: u64,
220 directory_size: u64,
221 zip64: bool,
222) -> Result<Vec<StoredZipEntry>>
223where
224 R: AsyncRead + Unpin,
225{
226 let claimed_entries = num_of_entries;
227 let num_of_entries: usize = num_of_entries.try_into().map_err(|_| ZipError::TargetZip64NotSupported)?;
228 let mut entries = Vec::with_capacity(cd_entry_capacity(num_of_entries, directory_start)?);
229 let mut remaining_directory_size = directory_size;
230 let mut reader = counting::Counting::new(reader);
231
232 for _ in 0..num_of_entries {
233 let entry = cd_record(&mut reader, zip64, &mut remaining_directory_size, claimed_entries).await?;
234 entries.push(entry);
235 }
236
237 consume_central_directory_digital_signature(&mut reader, directory_size).await?;
238
239 let actual = reader.bytes_read();
240 if actual != directory_size {
241 return Err(ZipError::InvalidCentralDirectorySize { expected: directory_size, actual });
242 }
243
244 Ok(entries)
245}
246
247async fn consume_central_directory_digital_signature<R>(
252 reader: &mut counting::Counting<R>,
253 directory_size: u64,
254) -> Result<()>
255where
256 R: AsyncRead + Unpin,
257{
258 let actual = reader.bytes_read();
259 if actual >= directory_size {
260 return Ok(());
261 }
262
263 let fixed_length = (SIGNATURE_LENGTH + CDDS_LENGTH) as u64;
264 if directory_size - actual < fixed_length {
265 return Err(ZipError::InvalidCentralDirectorySize { expected: directory_size, actual });
266 }
267
268 let mut signature = [0; SIGNATURE_LENGTH];
269 reader.read_exact(&mut signature).await?;
270 if u32::from_le_bytes(signature) != CDDS_SIGNATURE {
271 return Err(ZipError::InvalidCentralDirectorySize { expected: directory_size, actual });
272 }
273
274 let mut length = [0; CDDS_LENGTH];
275 reader.read_exact(&mut length).await?;
276 let signature_length = u16::from_le_bytes(length) as u64;
277 let Some(record_end) =
278 actual.checked_add(fixed_length).and_then(|record_end| record_end.checked_add(signature_length))
279 else {
280 return Err(ZipError::InvalidCentralDirectorySize { expected: directory_size, actual: u64::MAX });
281 };
282 if record_end != directory_size {
283 return Err(ZipError::InvalidCentralDirectorySize { expected: directory_size, actual: record_end });
284 }
285
286 io::skip_bytes(reader, signature_length).await?;
287 Ok(())
288}
289
290pub(crate) fn get_zip64_extra_field(extra_fields: &[ExtraField]) -> Option<&Zip64ExtendedInformationExtraField> {
291 for field in extra_fields {
292 if let ExtraField::Zip64ExtendedInformation(zip64field) = field {
293 return Some(zip64field);
294 }
295 }
296 None
297}
298
299pub(crate) fn get_zip64_extra_field_mut(
300 extra_fields: &mut [ExtraField],
301) -> Option<&mut Zip64ExtendedInformationExtraField> {
302 for field in extra_fields {
303 if let ExtraField::Zip64ExtendedInformation(zip64field) = field {
304 return Some(zip64field);
305 }
306 }
307 None
308}
309
310pub(crate) fn get_combined_sizes(
311 uncompressed_size: u32,
312 compressed_size: u32,
313 extra_field: &Option<&Zip64ExtendedInformationExtraField>,
314) -> Result<(u64, u64)> {
315 let mut uncompressed_size = uncompressed_size as u64;
316 let mut compressed_size = compressed_size as u64;
317
318 if uncompressed_size == NON_ZIP64_MAX_SIZE as u64 {
319 uncompressed_size =
320 extra_field.and_then(|field| field.uncompressed_size).ok_or(ZipError::Zip64ExtendedFieldIncomplete)?;
321 }
322 if compressed_size == NON_ZIP64_MAX_SIZE as u64 {
323 compressed_size =
324 extra_field.and_then(|field| field.compressed_size).ok_or(ZipError::Zip64ExtendedFieldIncomplete)?;
325 }
326
327 Ok((uncompressed_size, compressed_size))
328}
329
330pub(crate) async fn cd_record<R>(
331 mut reader: R,
332 _zip64: bool,
333 remaining_directory_size: &mut u64,
334 claimed_entries: u64,
335) -> Result<StoredZipEntry>
336where
337 R: AsyncRead + Unpin,
338{
339 if *remaining_directory_size < MIN_CENTRAL_DIRECTORY_ENTRY_SIZE {
340 return Err(ZipError::InvalidCentralDirectoryEntryCount { entries: claimed_entries });
341 }
342
343 crate::utils::assert_signature(&mut reader, CDH_SIGNATURE).await?;
344
345 let header = CentralDirectoryRecord::from_reader(&mut reader).await?;
346 let central_directory_entry_size = MIN_CENTRAL_DIRECTORY_ENTRY_SIZE
347 + header.file_name_length as u64
348 + header.extra_field_length as u64
349 + header.file_comment_length as u64;
350 if *remaining_directory_size < central_directory_entry_size {
351 return Err(ZipError::InvalidCentralDirectoryEntryCount { entries: claimed_entries });
352 }
353 *remaining_directory_size -= central_directory_entry_size;
354
355 let header_size = (SIGNATURE_LENGTH + LFH_LENGTH) as u64;
356 let trailing_size = header.file_name_length as u64 + header.extra_field_length as u64;
357 let filename_basic = io::read_bytes(&mut reader, header.file_name_length.into()).await?;
358 let compression = Compression::try_from(header.compression)?;
359 let extra_field = io::read_bytes(&mut reader, header.extra_field_length.into()).await?;
360 let extra_fields = parse_extra_fields(
361 extra_field,
362 header.uncompressed_size,
363 header.compressed_size,
364 Some(header.lh_offset),
365 Some(header.disk_start),
366 )?;
367 let comment_basic = io::read_bytes(reader, header.file_comment_length.into()).await?;
368
369 let zip64_extra_field = get_zip64_extra_field(&extra_fields);
370 let (uncompressed_size, compressed_size) =
371 get_combined_sizes(header.uncompressed_size, header.compressed_size, &zip64_extra_field)?;
372
373 let mut file_offset = header.lh_offset as u64;
374 if let Some(zip64_extra_field) = zip64_extra_field {
375 if file_offset == NON_ZIP64_MAX_SIZE as u64 {
376 if let Some(offset) = zip64_extra_field.relative_header_offset {
377 file_offset = offset;
378 }
379 }
380 }
381
382 let filename = detect_filename(filename_basic, header.flags.filename_unicode, extra_fields.as_ref())?;
383 let comment = detect_comment(comment_basic, header.flags.filename_unicode, extra_fields.as_ref());
384
385 let entry = ZipEntry {
386 filename,
387 compression,
388 #[cfg(any(
389 feature = "deflate",
390 feature = "bzip2",
391 feature = "zstd",
392 feature = "lzma",
393 feature = "xz",
394 feature = "deflate64"
395 ))]
396 compression_level: async_compression::Level::Default,
397 attribute_compatibility: AttributeCompatibility::Unix,
398 crc32: header.crc,
400 uncompressed_size,
401 compressed_size,
402 last_modification_date: ZipDateTime { date: header.mod_date, time: header.mod_time },
403 internal_file_attribute: header.inter_attr,
404 external_file_attribute: header.exter_attr,
405 extra_fields,
406 comment,
407 data_descriptor: header.flags.data_descriptor,
408 file_offset,
409 };
410
411 Ok(StoredZipEntry { entry, file_offset, header_size: header_size + trailing_size, data_end_boundary: u64::MAX })
412}
413
414pub(crate) async fn lfh<R>(mut reader: R, file_offset: u64) -> Result<Option<ZipEntry>>
415where
416 R: AsyncRead + Unpin,
417{
418 let signature = {
419 let mut buffer = [0; 4];
420 reader.read_exact(&mut buffer).await?;
421 u32::from_le_bytes(buffer)
422 };
423 match signature {
424 actual if actual == LFH_SIGNATURE => (),
425 actual if actual == CDH_SIGNATURE => return Ok(None),
426 actual => return Err(ZipError::UnexpectedHeaderError(actual, LFH_SIGNATURE)),
427 };
428
429 let header = LocalFileHeader::from_reader(&mut reader).await?;
430 let filename_basic = io::read_bytes(&mut reader, header.file_name_length.into()).await?;
431 let compression = Compression::try_from(header.compression)?;
432 let extra_field = io::read_bytes(&mut reader, header.extra_field_length.into()).await?;
433 let extra_fields = parse_extra_fields(extra_field, header.uncompressed_size, header.compressed_size, None, None)?;
434
435 let zip64_extra_field = get_zip64_extra_field(&extra_fields);
436 let (uncompressed_size, compressed_size) =
437 get_combined_sizes(header.uncompressed_size, header.compressed_size, &zip64_extra_field)?;
438
439 if header.flags.data_descriptor && compression == Compression::Stored {
440 return Err(ZipError::FeatureNotSupported(
441 "stream reading entries with data descriptors & Stored compression mode",
442 ));
443 }
444 let filename = detect_filename(filename_basic, header.flags.filename_unicode, extra_fields.as_ref())?;
445
446 let entry = ZipEntry {
447 filename,
448 compression,
449 #[cfg(any(
450 feature = "deflate",
451 feature = "bzip2",
452 feature = "zstd",
453 feature = "lzma",
454 feature = "xz",
455 feature = "deflate64"
456 ))]
457 compression_level: async_compression::Level::Default,
458 attribute_compatibility: AttributeCompatibility::Unix,
459 crc32: header.crc,
461 uncompressed_size,
462 compressed_size,
463 last_modification_date: ZipDateTime { date: header.mod_date, time: header.mod_time },
464 internal_file_attribute: 0,
465 external_file_attribute: 0,
466 extra_fields,
467 comment: String::new().into(),
468 data_descriptor: header.flags.data_descriptor,
469 file_offset,
470 };
471
472 Ok(Some(entry))
473}
474
475fn detect_comment(basic: Vec<u8>, basic_is_utf8: bool, extra_fields: &[ExtraField]) -> ZipString {
476 if basic_is_utf8 {
477 ZipString::new(basic, StringEncoding::Utf8)
478 } else {
479 let unicode_extra = extra_fields.iter().find_map(|field| match field {
480 ExtraField::InfoZipUnicodeComment(InfoZipUnicodeCommentExtraField::V1 { crc32, unicode }) => {
481 if *crc32 == crc32fast::hash(&basic) {
482 Some(std::string::String::from_utf8(unicode.clone()))
483 } else {
484 None
485 }
486 }
487 _ => None,
488 });
489 if let Some(Ok(s)) = unicode_extra {
490 ZipString::new_with_alternative(s, basic)
491 } else {
492 if basic.is_ascii() {
495 unsafe { std::string::String::from_utf8_unchecked(basic).into() }
498 } else {
499 ZipString::new(basic, StringEncoding::Raw)
500 }
501 }
502 }
503}
504
505fn detect_filename(basic: Vec<u8>, basic_is_utf8: bool, extra_fields: &[ExtraField]) -> Result<ZipString> {
506 let unicode_extra = extra_fields.iter().find_map(|field| match field {
507 ExtraField::InfoZipUnicodePath(InfoZipUnicodePathExtraField::V1 { crc32, unicode }) => {
508 if !unicode.is_empty() && *crc32 == crc32fast::hash(&basic) {
509 Some(std::string::String::from_utf8(unicode.clone()))
510 } else {
511 None
512 }
513 }
514 _ => None,
515 });
516 if basic.contains(&0) {
517 return Err(ZipError::FileNameContainsNul { filename: basic });
518 }
519 if let Some(Ok(filename)) = unicode_extra.as_ref() {
520 if filename.contains('\0') {
521 return Err(ZipError::FileNameContainsNul { filename: filename.as_bytes().to_vec() });
522 }
523 }
524 if let Some(unicode_extra) = unicode_extra {
525 let unicode = unicode_extra.map_err(|_| ZipError::InfoZipUnicodePathFieldInvalidUtf8)?;
526 Ok(ZipString::new_with_alternative(unicode, basic))
527 } else if basic_is_utf8 {
528 Ok(ZipString::new(basic, StringEncoding::Utf8))
529 } else {
530 if basic.is_ascii() {
533 Ok(unsafe { std::string::String::from_utf8_unchecked(basic).into() })
536 } else {
537 Ok(ZipString::new(basic, StringEncoding::Raw))
538 }
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn invalid_matching_unicode_path_extra_field_is_rejected() {
548 let basic = b"basic.txt".to_vec();
549 let fields = [ExtraField::InfoZipUnicodePath(InfoZipUnicodePathExtraField::V1 {
550 crc32: crc32fast::hash(&basic),
551 unicode: vec![0xFF],
552 })];
553
554 assert!(matches!(detect_filename(basic, false, &fields), Err(ZipError::InfoZipUnicodePathFieldInvalidUtf8)));
555 }
556
557 #[test]
558 fn invalid_non_matching_unicode_path_extra_field_is_ignored() {
559 let basic = b"basic.txt".to_vec();
560 let fields =
561 [ExtraField::InfoZipUnicodePath(InfoZipUnicodePathExtraField::V1 { crc32: 0, unicode: vec![0xFF] })];
562
563 assert_eq!(detect_filename(basic, false, &fields).unwrap().as_str().unwrap(), "basic.txt");
564 }
565}