Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions src/format/aes.rs
Original file line number Diff line number Diff line change
@@ -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
}
Comment on lines +19 to +21
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method seems unnecessary. Can't it be either inlined or replaced with the From<AesVendorVersion> for u16 impl below?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am now using as_u16() inside the From

Copy link
Copy Markdown
Member

@Pr0methean Pr0methean May 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use as u16 inside the From and wherever else it's currently used, so AesVendorVersion can have one less method?


/// 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<u16> for AesVendorVersion {
Comment thread
Its-Just-Nans marked this conversation as resolved.
type Error = &'static str;

fn try_from(value: u16) -> Result<Self, Self::Error> {
let aes_vendor_version = match value {
0x0001 => AesVendorVersion::Ae1,
0x0002 => AesVendorVersion::Ae2,
_ => return Err("Invalid AES vendor version"),
};
Ok(aes_vendor_version)
}
}

impl From<AesVendorVersion> 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<u8> for AesMode {
type Error = &'static str;

fn try_from(value: u8) -> Result<Self, Self::Error> {
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,
}
}
}
160 changes: 160 additions & 0 deletions src/format/flags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//! 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<u8> 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<System> for u8 {
fn from(system: System) -> Self {
system as u8
}
}

/// Zip flags
/// Stored as Little endian
#[allow(unused)]
Comment thread
Its-Just-Nans marked this conversation as resolved.
Outdated
#[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<ZipFlags> for u16 {
fn from(value: ZipFlags) -> u16 {
value.as_u16()
}
}
4 changes: 4 additions & 0 deletions src/format/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Zip format

pub(crate) mod aes;
pub(crate) mod flags;
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -33,6 +34,7 @@ mod cp437;
mod crc32;
mod datetime;
pub mod extra_fields;
mod format;
mod path;
pub mod read;
pub mod result;
Expand Down
5 changes: 2 additions & 3 deletions src/read/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 0 additions & 1 deletion src/read/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
57 changes: 0 additions & 57 deletions src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ZipFlags> 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
Expand Down
Loading
Loading