diff --git a/src/format/aes.rs b/src/format/aes.rs new file mode 100644 index 000000000..5db1ebe8e --- /dev/null +++ b/src/format/aes.rs @@ -0,0 +1,117 @@ +//! AES related specifications + +use core::fmt::Display; + +/// The encryption specification used to encrypt a file with AES. +/// +/// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2 +/// does not make use of the CRC check. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u16)] +pub enum AesVendorVersion { + Ae1 = 0x0001, + Ae2 = 0x0002, +} + +impl AesVendorVersion { + /// As u16 + #[must_use] + pub const fn as_u16(self) -> u16 { + self as u16 + } + + /// Returns `true` if the data is encrypted using AE2. + #[cfg(feature = "aes-crypto")] + pub const fn is_ae2_encrypted(&self) -> bool { + matches!(self, AesVendorVersion::Ae2) + } + + /// `false` since the feature `aes-crypto` is not enabled + #[cfg(not(feature = "aes-crypto"))] + pub const fn is_ae2_encrypted(&self) -> bool { + false + } +} + +impl TryFrom for AesVendorVersion { + type Error = &'static str; + + fn try_from(value: u16) -> Result { + let aes_vendor_version = match value { + 0x0001 => AesVendorVersion::Ae1, + 0x0002 => AesVendorVersion::Ae2, + _ => return Err("Invalid AES vendor version"), + }; + Ok(aes_vendor_version) + } +} + +impl From for u16 { + fn from(value: AesVendorVersion) -> Self { + value.as_u16() + } +} + +/// AES variant used. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "_arbitrary", derive(arbitrary::Arbitrary))] +#[repr(u8)] +pub enum AesMode { + /// 128-bit AES encryption. + Aes128 = 0x01, + /// 192-bit AES encryption. + Aes192 = 0x02, + /// 256-bit AES encryption. + Aes256 = 0x03, +} + +impl AesMode { + /// As u8 + #[must_use] + pub const fn as_u8(self) -> u8 { + self as u8 + } +} + +impl Display for AesMode { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Aes128 => write!(f, "AES-128"), + Self::Aes192 => write!(f, "AES-192"), + Self::Aes256 => write!(f, "AES-256"), + } + } +} + +impl TryFrom for AesMode { + type Error = &'static str; + + fn try_from(value: u8) -> Result { + let mode = match value { + 0x01 => AesMode::Aes128, + 0x02 => AesMode::Aes192, + 0x03 => AesMode::Aes256, + _ => return Err("Invalid AES encryption strength"), + }; + Ok(mode) + } +} + +#[cfg(feature = "aes-crypto")] +impl AesMode { + /// Length of the salt for the given AES mode. + #[must_use] + pub const fn salt_length(&self) -> usize { + self.key_length() / 2 + } + + /// Length of the key for the given AES mode. + #[must_use] + pub const fn key_length(&self) -> usize { + match self { + Self::Aes128 => 16, + Self::Aes192 => 24, + Self::Aes256 => 32, + } + } +} diff --git a/src/format/flags.rs b/src/format/flags.rs new file mode 100644 index 000000000..ccbc3c464 --- /dev/null +++ b/src/format/flags.rs @@ -0,0 +1,168 @@ +//! Flags of zip + +/// System inside `version made by` (upper byte) +/// Reference: 4.4.2.2 +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +#[allow(clippy::upper_case_acronyms)] +#[repr(u8)] +pub enum System { + /// `MS-DOS` and `OS/2` (`FAT` / `VFAT` / `FAT32` file systems; default on Windows) + Dos = 0, + /// `Amiga` + Amiga = 1, + /// `OpenVMS` + OpenVMS = 2, + /// Default on Unix; default for symlinks on all platforms + Unix = 3, + /// `VM/CMS` + VmCms = 4, + /// `Atari ST` + AtariSt = 5, + /// `OS/2 H.P.F.S.` + Os2 = 6, + /// Legacy `Mac OS`, pre `OS X` + Macintosh = 7, + /// `Z-System` + ZSystem = 8, + /// `CP/M` + CPM = 9, + /// Windows NTFS (with extra attributes; not used by default) + WindowsNTFS = 10, + /// `MVS (OS/390 - Z/OS)` + MVS = 11, + /// `VSE` + VSE = 12, + /// `Acorn Risc` + AcornRisc = 13, + /// `VFAT` + VFAT = 14, + /// alternate MVS + AlternateMVS = 15, + /// `BeOS` + BeOS = 16, + /// `Tandem` + Tandem = 17, + /// `OS/400` + Os400 = 18, + /// `OS X` (Darwin) (with extra attributes; not used by default) + OsDarwin = 19, + /// unused + #[default] + Unknown = 255, +} + +impl System { + /// Parse `version_made_by` block in local entry block. + #[must_use] + pub fn from_version_made_by(version_made_by: u16) -> Self { + // Extract upper byte from little-endian representation + let upper_byte = version_made_by.to_le_bytes()[1]; + System::from(upper_byte) // from u8 + } + + /// Extract the system and version from a `version_made_by` field. + /// The first byte (lower) is the version, and the second byte (upper) is the system. + pub(crate) fn extract_bytes(version_made_by: u16) -> (u8, Self) { + let bytes = version_made_by.to_le_bytes(); + (bytes[0], Self::from(bytes[1])) + } +} + +impl From for System { + fn from(system: u8) -> Self { + match system { + 0 => System::Dos, + 1 => System::Amiga, + 2 => System::OpenVMS, + 3 => System::Unix, + 4 => System::VmCms, + 5 => System::AtariSt, + 6 => System::Os2, + 7 => System::Macintosh, + 8 => System::ZSystem, + 9 => System::CPM, + 10 => System::WindowsNTFS, + 11 => System::MVS, + 12 => System::VSE, + 13 => System::AcornRisc, + 14 => System::VFAT, + 15 => System::AlternateMVS, + 16 => System::BeOS, + 17 => System::Tandem, + 18 => System::Os400, + 19 => System::OsDarwin, + _ => System::Unknown, + } + } +} + +impl From for u8 { + fn from(system: System) -> Self { + system as u8 + } +} + +/// Zip flags +/// Stored as Little endian +#[rustfmt::skip] +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub(crate) enum ZipFlags { + /// If set, indicates that the file is encrypted. + Encrypted = 0b0000_0000_0000_0001, + #[allow(unused)] + CompressionSetting = 0b0000_0000_0000_0010, + #[allow(unused)] + CompressionSetting2 = 0b0000_0000_0000_0100, + /// If this bit is set, the fields crc-32, compressed size and uncompressed size are set to zero in the local header. + /// The correct values are put in the data descriptor immediately following the compressed data. + UsingDataDescriptor = 0b0000_0000_0000_1000, + /// Reserved for use with method 8, for enhanced deflating. + #[allow(unused)] + ReservedEnhancedDeflating = 0b0000_0000_0001_0000, + /// If this bit is set, this indicates that the file is compressed patched data. + #[allow(unused)] + CompressedPatchedData = 0b0000_0000_0010_0000, + /// Strong encryption. + /// If this bit is set, you MUST set the version needed to extract value to at least 50 and you MUST also set bit 0. + /// If AES encryption is used, the version needed to extract value MUST be at least 51. + #[allow(unused)] + StrongEncryption = 0b0000_0000_0100_0000, + // bit 7 Currently unused = 0b0000_0000_1000_0000; + // bit 8 Currently unused = 0b0000_0001_0000_0000; + // bit 9 Currently unused = 0b0000_0010_0000_0000; + // bit 10 Currently unused = 0b0000_0100_0000_0000; + + /// Language encoding flag (EFS). + /// If this bit is set, the filename and comment fields for this file MUST be encoded using UTF-8. + LanguageEncoding = 0b0000_1000_0000_0000, + /// Reserved by PKWARE for enhanced compression. + #[allow(unused)] + ReservedEnhancedCompression = 0b0001_0000_0000_0000, + /// Set when encrypting the Central Directory to indicate selected data values in the Local Header are masked to hide their actual values. + #[allow(unused)] + Masked = 0b0010_0000_0000_0000, + /// Reserved by PKWARE for alternate streams. + #[allow(unused)] + ReservedAlternateStream = 0b0100_0000_0000_0000, + /// Reserved by PKWARE. + #[allow(unused)] + Reserved = 0b1000_0000_0000_0000, +} + +impl ZipFlags { + pub(crate) fn matching(flags: u16, matching_flag: Self) -> bool { + flags & u16::from(matching_flag) != 0 + } + + pub(crate) const fn as_u16(self) -> u16 { + self as u16 + } +} + +impl From for u16 { + fn from(value: ZipFlags) -> u16 { + value.as_u16() + } +} diff --git a/src/format/mod.rs b/src/format/mod.rs new file mode 100644 index 000000000..32749d91c --- /dev/null +++ b/src/format/mod.rs @@ -0,0 +1,4 @@ +//! Zip format + +pub(crate) mod aes; +pub(crate) mod flags; diff --git a/src/lib.rs b/src/lib.rs index dd1e6eb2a..e5f4dce6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,10 +16,11 @@ #![allow(clippy::multiple_crate_versions)] // https://github.com/rust-lang/rust-clippy/issues/16440 pub use crate::compression::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS}; pub use crate::datetime::DateTime; +pub use crate::format::aes::AesMode; +pub use crate::format::flags::System; pub use crate::read::HasZipMetadata; pub use crate::read::{ZipArchive, ZipReadOptions}; pub use crate::spec::{ZIP64_BYTES_THR, ZIP64_ENTRY_THR}; -pub use crate::types::{AesMode, System}; pub use crate::write::ZipWriter; #[cfg(feature = "aes-crypto")] @@ -33,6 +34,7 @@ mod cp437; mod crc32; mod datetime; pub mod extra_fields; +mod format; mod path; pub mod read; pub mod result; diff --git a/src/read/mod.rs b/src/read/mod.rs index 52d3c890d..e5f65099b 100644 --- a/src/read/mod.rs +++ b/src/read/mod.rs @@ -7,10 +7,9 @@ use crate::extra_fields::AexEncryption; use crate::extra_fields::UnicodeExtraField; use crate::extra_fields::Zip64ExtendedInformation; use crate::extra_fields::{ExtendedTimestamp, ExtraField, Ntfs, UsedExtraField}; +use crate::format::flags::ZipFlags; use crate::result::{ZipError, ZipResult, invalid}; -use crate::spec::{ - CentralDirectoryEndInfo, DataAndPosition, FixedSizeBlock, ZipCentralEntryBlock, ZipFlags, -}; +use crate::spec::{CentralDirectoryEndInfo, DataAndPosition, FixedSizeBlock, ZipCentralEntryBlock}; use crate::types::{System, ZipFileData}; use crate::unstable::LittleEndianReadExt; use indexmap::IndexMap; diff --git a/src/read/stream.rs b/src/read/stream.rs index f6d9fa77c..db482893d 100644 --- a/src/read/stream.rs +++ b/src/read/stream.rs @@ -338,7 +338,6 @@ pub fn read_zipfile_from_stream_with_options<'a, R: io::Read>( #[cfg(test)] mod tests { - use crate::read::ZipFile; use crate::read::stream::{ZipStreamFileMetadata, ZipStreamReader, ZipStreamVisitor}; use crate::result::ZipResult; diff --git a/src/spec.rs b/src/spec.rs index 988cf39cf..cb1d8ae59 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -53,63 +53,6 @@ impl Magic { pub const DATA_DESCRIPTOR_SIGNATURE: Self = Self::literal(0x0807_4b50); } -/// Zip flags -/// Stored as Little endian -#[allow(unused)] -#[rustfmt::skip] -#[repr(u16)] -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub(crate) enum ZipFlags { - /// If set, indicates that the file is encrypted. - Encrypted = 0b0000_0000_0000_0001, - CompressionSetting = 0b0000_0000_0000_0010, - CompressionSetting2 = 0b0000_0000_0000_0100, - /// If this bit is set, the fields crc-32, compressed size and uncompressed size are set to zero in the local header. - /// The correct values are put in the data descriptor immediately following the compressed data. - UsingDataDescriptor = 0b0000_0000_0000_1000, - /// Reserved for use with method 8, for enhanced deflating. - ReservedEnhancedDeflating = 0b0000_0000_0001_0000, - /// If this bit is set, this indicates that the file is compressed patched data. - CompressedPatchedData = 0b0000_0000_0010_0000, - /// Strong encryption. - /// If this bit is set, you MUST set the version needed to extract value to at least 50 and you MUST also set bit 0. - /// If AES encryption is used, the version needed to extract value MUST be at least 51. - StrongEncryption = 0b0000_0000_0100_0000, - // bit 7 Currently unused = 0b0000_0000_1000_0000; - // bit 8 Currently unused = 0b0000_0001_0000_0000; - // bit 9 Currently unused = 0b0000_0010_0000_0000; - // bit 10 Currently unused = 0b0000_0100_0000_0000; - - /// Language encoding flag (EFS). - /// If this bit is set, the filename and comment fields for this file MUST be encoded using UTF-8. - LanguageEncoding = 0b0000_1000_0000_0000, - /// Reserved by PKWARE for enhanced compression. - ReservedEnhancedCompression = 0b0001_0000_0000_0000, - /// Set when encrypting the Central Directory to indicate selected data values in the Local Header are masked to hide their actual values. - Masked = 0b0010_0000_0000_0000, - /// Reserved by PKWARE for alternate streams. - ReservedAlternateStream = 0b0100_0000_0000_0000, - /// Reserved by PKWARE. - Reserved = 0b1000_0000_0000_0000, -} - -impl ZipFlags { - pub(crate) fn matching(flags: u16, matching_flag: Self) -> bool { - flags & u16::from(matching_flag) != 0 - } - - pub(crate) const fn as_u16(self) -> u16 { - self as u16 - } -} - -impl From for u16 { - fn from(value: ZipFlags) -> u16 { - value.as_u16() - } -} - /// The file size at which a ZIP64 record becomes necessary. /// /// If a file larger than this threshold attempts to be written, compressed or uncompressed, and diff --git a/src/types.rs b/src/types.rs index f4e4e00d8..2dedda4e6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,18 +4,17 @@ use crate::CompressionMethod; use crate::cp437::FromCp437; use crate::datetime::DateTime; use crate::extra_fields::ExtraField; +use crate::format::flags::ZipFlags; use crate::path::{enclosed_name, file_name_sanitized}; use crate::read::readers::SeekableTake; use crate::result::{ZipError, ZipResult, invalid}; use crate::spec::is_dir; use crate::spec::{ self, FixedSizeBlock, Magic, Zip64DataDescriptorBlock, ZipCentralEntryBlock, - ZipDataDescriptorBlock, ZipFlags, ZipLocalEntryBlock, + ZipDataDescriptorBlock, ZipLocalEntryBlock, }; use crate::write::FileOptionExtension; use crate::zipcrypto::ZipCryptoKeys; -use core::fmt::Debug; -use core::fmt::Display; use core::marker::PhantomData; use std::borrow::Cow; use std::ffi::OsStr; @@ -23,6 +22,9 @@ use std::io::{Read, Seek, SeekFrom, Take}; use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; +pub(crate) use crate::format::aes::{AesMode, AesVendorVersion}; +pub(crate) use crate::format::flags::System; + pub(crate) mod ffi { pub const S_IFDIR: u32 = 0o0_040_000; pub const S_IFREG: u32 = 0o0_100_000; @@ -35,108 +37,6 @@ pub(crate) struct ZipRawValues { pub(crate) uncompressed_size: u64, } -/// System inside `version made by` (upper byte) -/// Reference: 4.4.2.2 -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] -#[allow(clippy::upper_case_acronyms)] -#[repr(u8)] -pub enum System { - /// `MS-DOS` and `OS/2` (`FAT` / `VFAT` / `FAT32` file systems; default on Windows) - Dos = 0, - /// `Amiga` - Amiga = 1, - /// `OpenVMS` - OpenVMS = 2, - /// Default on Unix; default for symlinks on all platforms - Unix = 3, - /// `VM/CMS` - VmCms = 4, - /// `Atari ST` - AtariSt = 5, - /// `OS/2 H.P.F.S.` - Os2 = 6, - /// Legacy `Mac OS`, pre `OS X` - Macintosh = 7, - /// `Z-System` - ZSystem = 8, - /// `CP/M` - CPM = 9, - /// Windows NTFS (with extra attributes; not used by default) - WindowsNTFS = 10, - /// `MVS (OS/390 - Z/OS)` - MVS = 11, - /// `VSE` - VSE = 12, - /// `Acorn Risc` - AcornRisc = 13, - /// `VFAT` - VFAT = 14, - /// alternate MVS - AlternateMVS = 15, - /// `BeOS` - BeOS = 16, - /// `Tandem` - Tandem = 17, - /// `OS/400` - Os400 = 18, - /// `OS X` (Darwin) (with extra attributes; not used by default) - OsDarwin = 19, - /// unused - #[default] - Unknown = 255, -} - -impl System { - /// Parse `version_made_by` block in local entry block. - #[must_use] - pub fn from_version_made_by(version_made_by: u16) -> Self { - // Extract upper byte from little-endian representation - let upper_byte = version_made_by.to_le_bytes()[1]; - System::from(upper_byte) // from u8 - } - - /// Extract the system and version from a `version_made_by` field. - /// The first byte (lower) is the version, and the second byte (upper) is the system. - pub(crate) fn extract_bytes(version_made_by: u16) -> (u8, Self) { - let bytes = version_made_by.to_le_bytes(); - (bytes[0], Self::from(bytes[1])) - } -} - -impl From for System { - fn from(system: u8) -> Self { - match system { - 0 => System::Dos, - 1 => System::Amiga, - 2 => System::OpenVMS, - 3 => System::Unix, - 4 => System::VmCms, - 5 => System::AtariSt, - 6 => System::Os2, - 7 => System::Macintosh, - 8 => System::ZSystem, - 9 => System::CPM, - 10 => System::WindowsNTFS, - 11 => System::MVS, - 12 => System::VSE, - 13 => System::AcornRisc, - 14 => System::VFAT, - 15 => System::AlternateMVS, - 16 => System::BeOS, - 17 => System::Tandem, - 18 => System::Os400, - 19 => System::OsDarwin, - _ => System::Unknown, - } - } -} - -impl From for u8 { - fn from(system: System) -> Self { - system as u8 - } -} - #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(crate) enum EncryptWith<'k> { #[cfg(feature = "aes-crypto")] @@ -766,120 +666,6 @@ impl ZipFileData { } } -/// The encryption specification used to encrypt a file with AES. -/// -/// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2 -/// does not make use of the CRC check. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -#[repr(u16)] -pub enum AesVendorVersion { - Ae1 = 0x0001, - Ae2 = 0x0002, -} - -impl AesVendorVersion { - /// As u16 - #[must_use] - pub const fn as_u16(self) -> u16 { - self as u16 - } - - /// Returns `true` if the data is encrypted using AE2. - #[cfg(feature = "aes-crypto")] - pub const fn is_ae2_encrypted(&self) -> bool { - matches!(self, AesVendorVersion::Ae2) - } - - /// `false` since the feature `aes-crypto` is not enabled - #[cfg(not(feature = "aes-crypto"))] - pub const fn is_ae2_encrypted(&self) -> bool { - false - } -} - -impl TryFrom for AesVendorVersion { - type Error = &'static str; - - fn try_from(value: u16) -> Result { - let aes_vendor_version = match value { - 0x0001 => AesVendorVersion::Ae1, - 0x0002 => AesVendorVersion::Ae2, - _ => return Err("Invalid AES vendor version"), - }; - Ok(aes_vendor_version) - } -} - -impl From for u16 { - fn from(value: AesVendorVersion) -> Self { - value as u16 - } -} - -/// AES variant used. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "_arbitrary", derive(arbitrary::Arbitrary))] -#[repr(u8)] -pub enum AesMode { - /// 128-bit AES encryption. - Aes128 = 0x01, - /// 192-bit AES encryption. - Aes192 = 0x02, - /// 256-bit AES encryption. - Aes256 = 0x03, -} - -impl AesMode { - /// As u8 - #[must_use] - pub const fn as_u8(self) -> u8 { - self as u8 - } -} - -impl Display for AesMode { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Aes128 => write!(f, "AES-128"), - Self::Aes192 => write!(f, "AES-192"), - Self::Aes256 => write!(f, "AES-256"), - } - } -} - -impl TryFrom for AesMode { - type Error = &'static str; - - fn try_from(value: u8) -> Result { - let mode = match value { - 0x01 => AesMode::Aes128, - 0x02 => AesMode::Aes192, - 0x03 => AesMode::Aes256, - _ => return Err("Invalid AES encryption strength"), - }; - Ok(mode) - } -} - -#[cfg(feature = "aes-crypto")] -impl AesMode { - /// Length of the salt for the given AES mode. - #[must_use] - pub const fn salt_length(&self) -> usize { - self.key_length() / 2 - } - - /// Length of the key for the given AES mode. - #[must_use] - pub const fn key_length(&self) -> usize { - match self { - Self::Aes128 => 16, - Self::Aes192 => 24, - Self::Aes256 => 32, - } - } -} - #[cfg(test)] mod tests { #[test] diff --git a/src/unstable.rs b/src/unstable.rs index 5c53eb53c..3c6d4ecbf 100644 --- a/src/unstable.rs +++ b/src/unstable.rs @@ -60,7 +60,6 @@ pub trait LittleEndianWriteExt: Write { fn write_u16_le(&mut self, input: u16) -> io::Result<()> { self.write_all(&input.to_le_bytes()) } - /// Write a u32 as little endian fn write_u32_le(&mut self, input: u32) -> io::Result<()> { self.write_all(&input.to_le_bytes()) diff --git a/src/write.rs b/src/write.rs index d32d3db32..f6a71ef17 100644 --- a/src/write.rs +++ b/src/write.rs @@ -5,9 +5,9 @@ use crate::datetime::DateTime; use crate::extra_fields::AexEncryption; use crate::extra_fields::UsedExtraField; use crate::extra_fields::Zip64ExtendedInformation; +use crate::format::flags::ZipFlags; use crate::read::{Config, ZipArchive, ZipFile, parse_single_extra_field}; use crate::result::{ZipError, ZipResult, invalid}; -use crate::spec::ZipFlags; use crate::spec::{self, FixedSizeBlock, Magic, Zip32CDEBlock, ZipLocalEntryBlock}; use crate::types::EncryptWith; use crate::types::{AesVendorVersion, MIN_VERSION, System, ZipFileData, ZipRawValues, ffi}; diff --git a/tests/aes_encryption.rs b/tests/aes_encryption.rs index 82bc8e67b..729188798 100644 --- a/tests/aes_encryption.rs +++ b/tests/aes_encryption.rs @@ -331,7 +331,7 @@ fn test_update_aes_version_on_threshold() { let mut data = Vec::new(); let mut zip = ZipWriter::new(io::Cursor::new(&mut data)); zip.start_file("test.txt", options).unwrap(); - zip.write(b"HELLO").unwrap(); + zip.write_all(b"HELLO").unwrap(); zip.finish().unwrap(); // checksum is empty because we use version 2 @@ -365,7 +365,7 @@ fn test_update_aes_version_on_threshold() { let mut data = Vec::new(); let mut zip = ZipWriter::new(io::Cursor::new(&mut data)); zip.start_file("test.txt", options).unwrap(); - zip.write(b"LONG FILE MORE THAN 20 BYTES").unwrap(); + zip.write_all(b"LONG FILE MORE THAN 20 BYTES").unwrap(); zip.finish().unwrap(); assert_eq!(data[14..18], [0xb7, 0x81, 0xef, 0xad]); // checksum