piz 0.5.1

piz (a Parallel Implementation of Zip) is a ZIP archive reader designed to concurrently decompress files using a simple API.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
//! Code specific to the ZIP file format specification.
//!
//! We try to keep the nitty gritty here,
//! and higher-level stuff in the [`read`] module.
//! (This pattern, like several others, was inspired by the Zip crate.)
//!
//! Most comments quote the ZIP spec, [`APPNOTE.TXT`].
//!
//! [_Zip Files: History, Explanation and Implementation_]
//! is also a fantastic resource and a great read.
//!
//! [`read`]: ../read/index.html
//! [`APPNOTE.TXT`]: https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.6.TXT
//! [_Zip Files: History, Explanation and Implementation_]: https://www.hanshq.net/zip.html

use std::borrow::Cow;
use std::convert::TryInto;

use camino::Utf8Path;
use chrono::{NaiveDate, NaiveDateTime};
use codepage_437::*;
use memchr::memmem;

use crate::arch::usize;
use crate::read::{CompressionMethod, FileMetadata};
use crate::result::*;

// Magic numbers denoting various sections of a ZIP archive

/// End of central directory magic number
const EOCDR_MAGIC: [u8; 4] = [b'P', b'K', 5, 6];
/// Zip64 end of central directory magic number
const ZIP64_EOCDR_MAGIC: [u8; 4] = [b'P', b'K', 6, 6];
/// Zip64 end of central directory locator magic number
const ZIP64_EOCDR_LOCATOR_MAGIC: [u8; 4] = [b'P', b'K', 6, 7];
/// Central directory magic number
const CENTRAL_DIRECTORY_MAGIC: [u8; 4] = [b'P', b'K', 1, 2];
/// Local file header magic number
const LOCAL_FILE_HEADER_MAGIC: [u8; 4] = [b'P', b'K', 3, 4];

impl CompressionMethod {
    fn from_u16(u: u16) -> Self {
        match u {
            0 => CompressionMethod::None,
            8 => CompressionMethod::Deflate,
            // 12 => CompressionMethod::Bzip2,
            v => CompressionMethod::Unsupported(v),
        }
    }
}

/// The OS a file in the archive was compressed with.
/// Used to decode additional metadata like permissions
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum System {
    Dos,
    Unix,
    Other(u8),
}

#[allow(dead_code)]
impl System {
    fn from_source_version(source_version: u16) -> Self {
        // 4.4.2.1 The upper byte indicates the compatibility of the file
        // attribute information.  If the external file attributes
        // are compatible with MS-DOS and can be read by PKZIP for
        // DOS version 2.04g then this value will be zero.  If these
        // attributes are not compatible, then this value will
        // identify the host system on which the attributes are
        // compatible.  Software can use this information to determine
        // the line record format for text files etc.
        //
        // 4.4.2.2 The current mappings are:
        //
        //  0 - MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems)
        //  1 - Amiga                     2 - OpenVMS
        //  3 - UNIX                      4 - VM/CMS
        //  5 - Atari ST                  6 - OS/2 H.P.F.S.
        //  7 - Macintosh                 8 - Z-System
        //  9 - CP/M                     10 - Windows NTFS
        // 11 - MVS (OS/390 - Z/OS)      12 - VSE
        // 13 - Acorn Risc               14 - VFAT
        // 15 - alternate MVS            16 - BeOS
        // 17 - Tandem                   18 - OS/400
        // 19 - OS X (Darwin)            20 thru 255 - unused
        match source_version >> 8 {
            0 => System::Dos,
            3 => System::Unix,
            o => System::Other(o as u8),
        }
    }
}

// Straight from the Rust docs:

/// Reads a little-endian u64 from the front of the provided slice, shrinking it.
fn read_u64(input: &mut &[u8]) -> u64 {
    let (int_bytes, rest) = input.split_at(std::mem::size_of::<u64>());
    *input = rest;
    u64::from_le_bytes(int_bytes.try_into().expect("less than eight bytes for u64"))
}

/// Reads a little-endian u32 from the front of the provided slice, shrinking it.
fn read_u32(input: &mut &[u8]) -> u32 {
    let (int_bytes, rest) = input.split_at(std::mem::size_of::<u32>());
    *input = rest;
    u32::from_le_bytes(int_bytes.try_into().expect("less than four bytes for u32"))
}

/// Reads a little-endian u16 from the front of the provided slice, shrinking it.
fn read_u16(input: &mut &[u8]) -> u16 {
    let (int_bytes, rest) = input.split_at(std::mem::size_of::<u16>());
    *input = rest;
    u16::from_le_bytes(int_bytes.try_into().expect("less than two bytes for u16"))
}

/// Data from the End of central directory record
///
/// Found at the back of the ZIP archive and provides offsets for finding
/// its central directory, along with lots of stuff that stopped being relevant
/// when we stopped breaking ZIP archives onto multiple floppies.
#[derive(Debug)]
pub struct EndOfCentralDirectory<'a> {
    pub disk_number: u16,
    pub disk_with_central_directory: u16,
    pub entries_on_this_disk: u16,
    pub entries: u16,
    pub central_directory_size: u32,
    pub central_directory_offset: u32,
    pub file_comment: &'a [u8],
}

impl<'a> EndOfCentralDirectory<'a> {
    pub fn parse(mut eocdr: &'a [u8]) -> ZipResult<Self> {
        // 4.3.16  End of central directory record:
        //
        // end of central dir signature    4 bytes  (0x06054b50)
        // number of this disk             2 bytes
        // number of the disk with the
        // start of the central directory  2 bytes
        // total number of entries in
        // the central dir on this disk    2 bytes
        // total number of entries in
        // the central dir                 2 bytes
        // size of the central directory   4 bytes
        // offset of start of central
        // directory with respect to
        // the starting disk number        4 bytes
        // zipfile comment length          2 bytes

        // Assert the magic instead of checking for it
        // because the search should have found it.
        assert_eq!(eocdr[..4], EOCDR_MAGIC);
        eocdr = &eocdr[4..];
        let disk_number = read_u16(&mut eocdr);
        let disk_with_central_directory = read_u16(&mut eocdr);
        let entries_on_this_disk = read_u16(&mut eocdr);
        let entries = read_u16(&mut eocdr);
        let central_directory_size = read_u32(&mut eocdr);
        let central_directory_offset = read_u32(&mut eocdr);
        let comment_length = read_u16(&mut eocdr);
        let file_comment = &eocdr[..usize(comment_length)?];

        Ok(Self {
            disk_number,
            disk_with_central_directory,
            entries_on_this_disk,
            entries,
            central_directory_size,
            central_directory_offset,
            file_comment,
        })
    }
}

/// Searches backward through `mapping` to find the
/// End of central directory record.
///
/// It should be right at the end of the file,
/// but its variable size means we can't jump to a known offset.
pub fn find_eocdr(mapping: &[u8]) -> ZipResult<usize> {
    memmem::rfind(mapping, &EOCDR_MAGIC).ok_or(ZipError::InvalidArchive(
        "Couldn't find End Of Central Directory Record",
    ))
}

/// Data from the Zip64 end of central directory locator
///
/// This should immediately precede the End of central directory record
/// on Zip64 files and tell us where to find the Zip64 end of central directory record.
#[derive(Debug)]
pub struct Zip64EndOfCentralDirectoryLocator {
    pub disk_with_central_directory: u32,
    pub zip64_eocdr_offset: u64,
    pub disks: u32,
}

impl Zip64EndOfCentralDirectoryLocator {
    pub fn parse(mut mapping: &[u8]) -> Option<Self> {
        // 4.3.15 Zip64 end of central directory locator
        //
        // zip64 end of central dir locator
        // signature                       4 bytes  (0x07064b50)
        // number of the disk with the
        // start of the zip64 end of
        // central directory               4 bytes
        // relative offset of the zip64
        // end of central directory record 8 bytes
        // total number of disks           4 bytes
        if mapping[..4] != ZIP64_EOCDR_LOCATOR_MAGIC {
            return None;
        }
        mapping = &mapping[4..];
        let disk_with_central_directory = read_u32(&mut mapping);
        let zip64_eocdr_offset = read_u64(&mut mapping);
        let disks = read_u32(&mut mapping);

        Some(Self {
            disk_with_central_directory,
            zip64_eocdr_offset,
            disks,
        })
    }

    pub fn size_in_file() -> usize {
        20
    }
}

/// Data from the Zip64 end of central directory record
///
/// This should immediately precede the "End of central directory" record
/// on Zip64 files and tell us where to find the Zip64 end of central directory record.
#[derive(Debug)]
pub struct Zip64EndOfCentralDirectory<'a> {
    pub source_version: u16,
    pub minimum_extract_version: u16,
    pub disk_number: u32,
    pub disk_with_central_directory: u32,
    pub entries_on_this_disk: u64,
    pub entries: u64,
    pub central_directory_size: u64,
    pub central_directory_offset: u64,
    pub extensible_data: &'a [u8],
}

impl<'a> Zip64EndOfCentralDirectory<'a> {
    pub fn parse(mut eocdr: &'a [u8]) -> ZipResult<Self> {
        // 4.3.14  Zip64 end of central directory record
        //
        // zip64 end of central dir
        // signature                       4 bytes  (0x06064b50)
        // size of zip64 end of central
        // directory record                8 bytes
        // version made by                 2 bytes
        // version needed to extract       2 bytes
        // number of this disk             4 bytes
        // number of the disk with the
        // start of the central directory  4 bytes
        // total number of entries in the
        // central directory on this disk  8 bytes
        // total number of entries in the
        // central directory               8 bytes
        // size of the central directory   8 bytes
        // offset of start of central
        // directory with respect to
        // the starting disk number        8 bytes
        // zip64 extensible data sector    (variable size)

        // Assert the magic instead of checking for it
        // because the search should have found it.
        assert_eq!(eocdr[..4], ZIP64_EOCDR_MAGIC);
        eocdr = &eocdr[4..];
        let eocdr_size = read_u64(&mut eocdr);
        let source_version = read_u16(&mut eocdr);
        let minimum_extract_version = read_u16(&mut eocdr);
        let disk_number = read_u32(&mut eocdr);
        let disk_with_central_directory = read_u32(&mut eocdr);
        let entries_on_this_disk = read_u64(&mut eocdr);
        let entries = read_u64(&mut eocdr);
        let central_directory_size = read_u64(&mut eocdr);
        let central_directory_offset = read_u64(&mut eocdr);

        // 4.3.14.1 The value stored into the "size of zip64 end of central
        // directory record" SHOULD be the size of the remaining
        // record and SHOULD NOT include the leading 12 bytes.
        //
        // Size = SizeOfFixedFields + SizeOfVariableData - 12.
        // (SizeOfVariableData = Size - SizeOfFixedFields + 12)

        // Check for underflow:
        let eocdr_size = usize(eocdr_size)?;
        if (eocdr_size + 12) < Self::fixed_size_in_file() {
            return Err(ZipError::InvalidArchive(
                "Invalid extensible data length in Zip64 End Of Central Directory Record",
            ));
        }
        // We should be left with just the extensible data:
        let extensible_data_length = eocdr_size + 12 - Self::fixed_size_in_file();
        if eocdr.len() != extensible_data_length {
            return Err(ZipError::InvalidArchive(
                "Invalid extensible data length in Zip64 End Of Central Directory Record",
            ));
        }
        let extensible_data = eocdr;

        Ok(Self {
            source_version,
            minimum_extract_version,
            disk_number,
            disk_with_central_directory,
            entries,
            entries_on_this_disk,
            central_directory_size,
            central_directory_offset,
            extensible_data,
        })
    }

    fn fixed_size_in_file() -> usize {
        56
    }
}

/// Finds the Zip64 end of central directory record in the given slice.
///
/// The slice should start at the Zip64 EOCDR's nominal location,
/// but we might have to do some searching since ZIP archives can have
/// arbitrary junk up front.
pub fn find_zip64_eocdr(mapping: &[u8]) -> ZipResult<usize> {
    memmem::find(mapping, &ZIP64_EOCDR_MAGIC).ok_or(ZipError::InvalidArchive(
        "Couldn't find zip64 End Of Central Directory Record",
    ))
}

/// Data from a central directory entry
///
/// Each of these records contians information about a file or folder
/// stored in the ZIP archive.
#[derive(Debug)]
pub struct CentralDirectoryEntry<'a> {
    pub source_version: u16,
    pub minimum_extract_version: u16,
    pub flags: u16,
    pub compression_method: u16,
    pub last_modified_time: u16,
    pub last_modified_date: u16,
    pub crc32: u32,
    pub compressed_size: u32,
    pub uncompressed_size: u32,
    pub disk_number: u16,
    pub internal_file_attributes: u16,
    pub external_file_attributes: u32,
    pub header_offset: u32,
    pub path: &'a [u8],
    pub extra_field: &'a [u8],
    pub file_comment: &'a [u8],
}

impl<'a> CentralDirectoryEntry<'a> {
    pub fn parse_and_consume(entry: &mut &'a [u8]) -> ZipResult<Self> {
        // 4.3.12  Central directory structure:
        //
        // [central directory header 1]
        // .
        // .
        // .
        // [central directory header n]
        // [digital signature]
        //
        // File header:
        //
        //   central file header signature   4 bytes  (0x02014b50)
        //   version made by                 2 bytes
        //   version needed to extract       2 bytes
        //   general purpose bit flag        2 bytes
        //   compression method              2 bytes
        //   last mod file time              2 bytes
        //   last mod file date              2 bytes
        //   crc-32                          4 bytes
        //   compressed size                 4 bytes
        //   uncompressed size               4 bytes
        //   file name length                2 bytes
        //   extra field length              2 bytes
        //   file comment length             2 bytes
        //   disk number start               2 bytes
        //   internal file attributes        2 bytes
        //   external file attributes        4 bytes
        //   relative offset of local header 4 bytes
        //
        //   file name (variable size)
        //   extra field (variable size)
        //   file comment (variable size)
        if entry[..4] != CENTRAL_DIRECTORY_MAGIC {
            return Err(ZipError::InvalidArchive("Invalid central directory entry"));
        }
        *entry = &entry[4..];
        let source_version = read_u16(entry);
        let minimum_extract_version = read_u16(entry);
        let flags = read_u16(entry);
        let compression_method = read_u16(entry);
        let last_modified_time = read_u16(entry);
        let last_modified_date = read_u16(entry);
        let crc32 = read_u32(entry);
        let compressed_size = read_u32(entry);
        let uncompressed_size = read_u32(entry);
        let path_length = usize(read_u16(entry))?;
        let extra_field_length = usize(read_u16(entry))?;
        let file_comment_length = usize(read_u16(entry))?;
        let disk_number = read_u16(entry);
        let internal_file_attributes = read_u16(entry);
        let external_file_attributes = read_u32(entry);
        let header_offset = read_u32(entry);
        let (path, remaining) = entry.split_at(path_length);
        let (extra_field, remaining) = remaining.split_at(extra_field_length);
        let (file_comment, remaining) = remaining.split_at(file_comment_length);
        *entry = remaining;

        Ok(Self {
            source_version,
            minimum_extract_version,
            flags,
            compression_method,
            last_modified_time,
            last_modified_date,
            crc32,
            compressed_size,
            uncompressed_size,
            disk_number,
            internal_file_attributes,
            external_file_attributes,
            header_offset,
            path,
            extra_field,
            file_comment,
        })
    }
}

/// Extracts the "is this text UTF-8?" bit from the 16-bit flags field.
///
/// If false, text is assumped to be CP437.
fn is_utf8(flags: u16) -> bool {
    // Bit 11: Language encoding flag (EFS).  If this bit is set,
    //         the filename and comment fields for this file
    //         MUST be encoded using UTF-8. (see APPENDIX D)
    flags & (1 << 11) != 0
}

/// Extracts the "is this file encrypted?" bit from the 16-bit flags field.
fn is_encrypted(flags: u16) -> bool {
    // Bit 0: If set, indicates that the file is encrypted
    flags & 1 != 0
}

impl<'a> FileMetadata<'a> {
    /// Extracts `FileMetadata` from a central directory entry
    pub(crate) fn from_cde(cde: &CentralDirectoryEntry<'a>) -> ZipResult<Self> {
        let is_utf8 = is_utf8(cde.flags);

        let path: Cow<Utf8Path> = if is_utf8 {
            let utf8 = std::str::from_utf8(cde.path).map_err(ZipError::Encoding)?;
            Cow::Borrowed(Utf8Path::new(utf8))
        } else {
            let str_cow: Cow<str> = Cow::borrow_from_cp437(cde.path, &CP437_CONTROL);
            // Annoying: doesn't seem to be any Cow<str> -> Cow<Path>
            match str_cow {
                Cow::Borrowed(s) => Cow::Borrowed(Utf8Path::new(s)),
                Cow::Owned(s) => Cow::Owned(s.into()),
            }
        };

        if cde.disk_number != 0 {
            return Err(ZipError::UnsupportedArchive(format!(
                "No support for multi-disk archives: file {path} claims to be on disk {}",
                cde.disk_number,
            )));
        }

        let encrypted = is_encrypted(cde.flags);
        /* When we try to read; don't bomb if the archive has _any_ encrypted file
        if encrypted {
            return Err(ZipError::UnsupportedArchive(format!(
                "No support for encrypted files, as {} claims to be",
                path
            )));
        }
        */

        let compression_method = CompressionMethod::from_u16(cde.compression_method);

        // https://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute
        // There's a _lot_ to unpack here - see unzip's zipinfo.c.
        let unix_mode = match System::from_source_version(cde.source_version) {
            // I know this!
            System::Unix => Some((cde.external_file_attributes >> 16) as u16),
            _ => None,
        };

        let mut metadata = Self {
            size: usize(cde.uncompressed_size)?,
            compressed_size: usize(cde.compressed_size)?,
            compression_method,
            crc32: cde.crc32,
            encrypted,
            path,
            last_modified: parse_msdos(cde.last_modified_time, cde.last_modified_date),
            unix_mode,
            header_offset: usize(cde.header_offset)?,
        };

        parse_extra_field(&mut metadata, cde.extra_field)?;

        Ok(metadata)
    }

    /// Extract metadata from a local file header.
    ///
    /// Since the local header doesn't contain the offset
    /// (we're at it already if we're reading the thing),
    /// take the CDE-provided offset.
    /// Ditto for other things the local header lacks (file perms, etc.)
    pub(crate) fn from_local_header(
        local: &LocalFileHeader<'a>,
        cde_header: &Self,
    ) -> ZipResult<Self> {
        let is_utf8 = is_utf8(local.flags);

        let path: Cow<Utf8Path> = if is_utf8 {
            let utf8 = std::str::from_utf8(local.path).map_err(ZipError::Encoding)?;
            Cow::Borrowed(Utf8Path::new(utf8))
        } else {
            let str_cow: Cow<str> = Cow::borrow_from_cp437(local.path, &CP437_CONTROL);
            // Annoying: doesn't seem to be any Cow<str> -> Cow<Path>
            match str_cow {
                Cow::Borrowed(s) => Cow::Borrowed(Utf8Path::new(s)),
                Cow::Owned(s) => Cow::Owned(s.into()),
            }
        };

        let encrypted = is_encrypted(local.flags);

        let compression_method = CompressionMethod::from_u16(local.compression_method);

        let mut metadata = Self {
            size: usize(local.uncompressed_size)?,
            compressed_size: usize(local.compressed_size)?,
            compression_method,
            crc32: local.crc32,
            encrypted,
            path,
            last_modified: parse_msdos(local.last_modified_time, local.last_modified_date),
            ..*cde_header
        };

        parse_extra_field(&mut metadata, local.extra_field)?;

        Ok(metadata)
    }
}

fn parse_msdos(time: u16, date: u16) -> NaiveDateTime {
    let seconds = (0b0000_0000_0001_1111 & time) as u32 * 2; // MSDOS uses 2-second precision
    let minutes = (0b0000_0111_1110_0000 & time) as u32 >> 5;
    let hours = (0b1111_1000_0000_0000 & time) as u32 >> 11;

    let days = (0b0000_0000_0001_1111 & date) as u32;
    let months = (0b0000_0001_1110_0000 & date) as u32 >> 5;
    // MSDOS uses years since 1980; Always interpreted as a positive value
    let years = ((0b1111_1110_0000_0000 & date) >> 9) as i32 + 1980;

    NaiveDate::from_ymd(years, months, days).and_hms(hours, minutes, seconds)
}

/// Parses the "extra fields" found in central directory entries
/// and local file headers.
///
/// Currently we just look for Zip64 info (64-bit values for files > 2^32 in size)
fn parse_extra_field(metadata: &mut FileMetadata, mut extra_field: &[u8]) -> ZipResult<()> {
    // 4.5.1 In order to allow different programs and different types
    // of information to be stored in the 'extra' field in .ZIP
    // files, the following structure MUST be used for all
    // programs storing data in this field:

    //     header1+data1 + header2+data2 . . .

    // Each header MUST consist of:

    //     Header ID - 2 bytes
    //     Data Size - 2 bytes
    while !extra_field.is_empty() {
        let kind = read_u16(&mut extra_field);
        let field_len = read_u16(&mut extra_field);

        let mut amount_left = field_len as i16;
        // Zip64 extended information extra field
        if kind == 0x0001 {
            if metadata.size == u32::MAX as usize {
                metadata.size = usize(read_u64(&mut extra_field))?;
                amount_left -= 8;
            }
            if metadata.compressed_size == u32::MAX as usize {
                metadata.compressed_size = usize(read_u64(&mut extra_field))?;
                amount_left -= 8;
            }
            if metadata.header_offset == u32::MAX as usize {
                metadata.header_offset = usize(read_u64(&mut extra_field))?;
                amount_left -= 8;
            }
            // We already checked many times that this isn't a multi-disk archive.
            if amount_left != 0 {
                return Err(ZipError::InvalidArchive(
                    "Extra data field contains disk number",
                ));
            }
        }
        extra_field = &extra_field[amount_left as usize..];
    }
    Ok(())
}

/// Data from a local file header
///
/// Each files' actual contents is preceded by this header.
/// These headers alllow for "streaming" decompression without
/// the use of the central directory,
/// but we don't make use of this feature.
#[derive(Debug)]
pub struct LocalFileHeader<'a> {
    pub minimum_extract_version: u16,
    pub flags: u16,
    pub compression_method: u16,
    pub last_modified_time: u16,
    pub last_modified_date: u16,
    pub crc32: u32,
    pub compressed_size: u32,
    pub uncompressed_size: u32,
    pub path: &'a [u8],
    pub extra_field: &'a [u8],
}

impl<'a> LocalFileHeader<'a> {
    pub fn parse_and_consume(header: &mut &'a [u8]) -> ZipResult<Self> {
        // 4.3.7  Local file header:
        //
        // local file header signature     4 bytes  (0x04034b50)
        // version needed to extract       2 bytes
        // general purpose bit flag        2 bytes
        // compression method              2 bytes
        // last mod file time              2 bytes
        // last mod file date              2 bytes
        // crc-32                          4 bytes
        // compressed size                 4 bytes
        // uncompressed size               4 bytes
        // file name length                2 bytes
        // extra field length              2 bytes
        //
        // file name (variable size)
        // extra field (variable size)
        assert_eq!(header[..4], LOCAL_FILE_HEADER_MAGIC);
        *header = &header[4..];
        let minimum_extract_version = read_u16(header);
        let flags = read_u16(header);
        let compression_method = read_u16(header);
        let last_modified_time = read_u16(header);
        let last_modified_date = read_u16(header);
        let crc32 = read_u32(header);
        let compressed_size = read_u32(header);
        let uncompressed_size = read_u32(header);
        let path_length = usize(read_u16(header))?;
        let extra_field_length = usize(read_u16(header))?;
        let (path, remaining) = header.split_at(path_length);
        let (extra_field, remaining) = remaining.split_at(extra_field_length);
        *header = remaining;

        Ok(Self {
            minimum_extract_version,
            flags,
            compression_method,
            last_modified_time,
            last_modified_date,
            crc32,
            compressed_size,
            uncompressed_size,
            path,
            extra_field,
        })
    }
}