#![allow(clippy::unreadable_literal)]
use packed_struct::prelude::*;
#[derive(Debug, PackedStruct)]
#[packed_struct(endian = "lsb")]
pub struct LocalFileHeader {
pub signature: u32,
pub version_to_extract: u16,
#[packed_field(size_bytes = "2")]
pub flags: GpBitFlag,
#[packed_field(size_bytes = "2", ty = "enum")]
pub compression: Compression,
#[packed_field(size_bytes = "4")]
pub last_mod_datetime: DosDatetime,
pub crc32: u32,
pub compressed_size: u32,
pub uncompressed_size: u32,
pub file_name_len: u16,
pub extra_field_len: u16,
}
impl LocalFileHeader {
pub const SIGNATURE: u32 = 0x04034b50;
}
#[derive(Debug, PackedStruct)]
#[packed_struct(bit_numbering = "lsb0", size_bytes = "2")]
pub struct GpBitFlag {
#[packed_field(bits = "11")]
pub use_data_descriptor: bool,
#[packed_field(bits = "3")]
pub language_encoding: bool,
}
#[derive(Debug, PackedStruct)]
#[packed_struct(endian = "lsb")]
pub struct Zip64ExtraField {
pub tag: u16,
pub size: u16,
pub uncompressed_size: u64,
pub compressed_size: u64,
pub offset: u64,
}
impl Zip64ExtraField {
pub const TAG: u16 = 0x0001;
}
#[derive(Debug, PackedStruct)]
#[packed_struct(endian = "lsb")]
pub struct DataDescriptor64 {
pub signature: u32,
pub crc32: u32,
pub compressed_size: u64,
pub uncompressed_size: u64,
}
impl DataDescriptor64 {
pub const SIGNATURE: u32 = 0x08074b50;
}
#[derive(Debug, PackedStruct)]
#[packed_struct(endian = "lsb")]
pub struct CentralDirectoryHeader {
pub signature: u32,
#[packed_field(size_bytes = "2")]
pub version_made_by: VersionMadeBy,
pub version_to_extract: u16,
#[packed_field(size_bytes = "2")]
pub flags: GpBitFlag,
#[packed_field(size_bytes = "2", ty = "enum")]
pub compression: Compression,
#[packed_field(size_bytes = "4")]
pub last_mod_datetime: DosDatetime,
pub crc32: u32,
pub compressed_size: u32,
pub uncompressed_size: u32,
pub file_name_len: u16,
pub extra_field_len: u16,
pub file_comment_length: u16,
pub disk_number_start: u16,
pub internal_attributes: u16,
pub external_attributes: u32,
pub local_header_offset: u32,
}
impl CentralDirectoryHeader {
pub const SIGNATURE: u32 = 0x02014b50;
}
#[derive(Debug, PackedStruct)]
#[packed_struct(endian = "lsb")]
pub struct VersionMadeBy {
pub spec_version: u8,
#[packed_field(size_bytes = "1", ty = "enum")]
pub os: VersionMadeByOs,
}
#[derive(Clone, Copy, Debug, PrimitiveEnum_u8)]
#[non_exhaustive]
pub enum VersionMadeByOs {
Unix = 3,
}
#[derive(Debug, PackedStruct)]
#[packed_struct(endian = "lsb")]
pub struct Zip64EndOfCentralDirectoryRecord {
pub signature: u32,
pub size_of_zip64_eocd: u64,
#[packed_field(size_bytes = "2")]
pub version_made_by: VersionMadeBy,
pub version_to_extract: u16,
pub this_disk_number: u32,
pub start_of_cd_disk_number: u32,
pub this_cd_entry_count: u64,
pub total_cd_entry_count: u64,
pub size_of_cd: u64,
pub cd_offset: u64,
}
impl Zip64EndOfCentralDirectoryRecord {
pub const SIGNATURE: u32 = 0x06064b50;
}
#[derive(Debug, PackedStruct)]
#[packed_struct(endian = "lsb")]
pub struct Zip64EndOfCentralDirectoryLocator {
pub signature: u32,
pub start_of_cd_disk_number: u32,
pub zip64_eocd_offset: u64,
pub number_of_disks: u32,
}
impl Zip64EndOfCentralDirectoryLocator {
pub const SIGNATURE: u32 = 0x07064b50;
}
#[derive(Debug, PackedStruct)]
#[packed_struct(endian = "lsb")]
pub struct EndOfCentralDirectory {
pub signature: u32,
pub this_disk_number: u16,
pub start_of_cd_disk_number: u16,
pub this_cd_entry_count: u16,
pub total_cd_entry_count: u16,
pub size_of_cd: u32,
pub cd_offset: u32,
pub file_comment_length: u16,
}
impl EndOfCentralDirectory {
pub const SIGNATURE: u32 = 0x06054b50;
}
#[derive(Clone, Copy, Debug, PrimitiveEnum_u16)]
#[non_exhaustive]
pub enum Compression {
Store = 0,
}
#[derive(Copy, Clone, Debug, Default, PackedStruct, PartialEq, Eq)]
#[packed_struct(endian = "lsb", size_bytes = "4")]
pub struct DosDatetime {
time: u16,
date: u16,
}
impl DosDatetime {
pub(crate) fn new(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> Option<Self> {
if !(1980..(1980 + 128)).contains(&year) {
return None;
}
let year = u16::try_from(year - 1980).ok()?;
if !(1..=12).contains(&month) {
return None;
}
let month = u16::try_from(month).ok()?;
if !(1..=31).contains(&day) {
return None;
}
let day = u16::try_from(day).ok()?;
if hour > 23 {
return None;
}
let hour = u16::try_from(hour).ok()?;
if minute > 59 {
return None;
}
let minute = u16::try_from(minute).ok()?;
if second > 59 {
return None;
}
let two_second = u16::try_from(second / 2).ok()?;
Some(DosDatetime {
time: two_second | (minute << 5) | (hour << 11),
date: day | (month << 5) | (year << 9),
})
}
#[cfg(test)]
pub fn year(self) -> i32 {
i32::from((self.date >> 9) + 1980)
}
#[cfg(test)]
pub fn month(self) -> u32 {
u32::from((self.date >> 5) & 15)
}
#[cfg(test)]
pub fn day(self) -> u32 {
u32::from(self.date & 31)
}
#[cfg(test)]
pub fn hour(self) -> u32 {
u32::from(self.time >> 11)
}
#[cfg(test)]
pub fn minute(self) -> u32 {
u32::from((self.time >> 5) & 63)
}
#[cfg(test)]
pub fn second(self) -> u32 {
u32::from((self.time & 31) * 2)
}
}
pub trait PackedStructZippityExt {
fn packed_size() -> u64;
fn packed_size_usize() -> usize;
fn packed_size_u16() -> u16;
}
impl<T: PackedStruct> PackedStructZippityExt for T {
fn packed_size() -> u64 {
Self::packed_size_usize() as u64
}
fn packed_size_usize() -> usize {
Self::packed_bytes_size(None).unwrap_or_else(|_| unreachable!("This never fails"))
}
fn packed_size_u16() -> u16 {
Self::packed_size_usize()
.try_into()
.expect("The struct is small")
}
}
#[cfg(test)]
mod tests {
use super::DosDatetime;
use assert2::assert;
use test_strategy::proptest;
#[test]
fn valid_dosdatetime_creation() {
let dt = DosDatetime::new(1985, 12, 25, 14, 30, 28);
assert!(dt.is_some());
}
#[test]
fn invalid_year_lower_bound() {
let dt = DosDatetime::new(1979, 5, 10, 10, 20, 20);
assert!(dt.is_none());
}
#[test]
fn invalid_year_upper_bound() {
let dt = DosDatetime::new(2108, 5, 10, 10, 20, 20);
assert!(dt.is_none());
}
#[test]
fn invalid_month() {
let dt = DosDatetime::new(2000, 13, 10, 10, 20, 20);
assert!(dt.is_none());
}
#[test]
fn invalid_day() {
let dt = DosDatetime::new(2000, 5, 32, 10, 20, 20);
assert!(dt.is_none());
}
#[test]
fn invalid_hour() {
let dt = DosDatetime::new(2000, 5, 10, 24, 20, 20);
assert!(dt.is_none());
}
#[test]
fn invalid_minute() {
let dt = DosDatetime::new(2000, 5, 10, 10, 60, 20);
assert!(dt.is_none());
}
#[test]
fn invalid_second() {
let dt = DosDatetime::new(2000, 5, 10, 10, 20, 60);
assert!(dt.is_none());
}
#[test]
fn even_second_rounding() {
let dt = DosDatetime::new(2000, 5, 10, 10, 20, 31);
assert!(dt.is_some());
let datetime = dt.unwrap();
assert!(datetime.time & 0b11111 == 15); }
#[proptest]
fn dos_datetime_round_trip(
#[strategy(1980..2108)] year: i32,
#[strategy(1u32..=12u32)] month: u32,
#[strategy(1u32..=31u32)] day: u32,
#[strategy(0u32..24u32)] hour: u32,
#[strategy(0u32..60u32)] minute: u32,
#[strategy(0u32..60u32)] second: u32,
) {
let dt = DosDatetime::new(year, month, day, hour, minute, second).unwrap();
assert!(dt.year() == year);
assert!(dt.month() == month);
assert!(dt.day() == day);
assert!(dt.hour() == hour);
assert!(dt.minute() == minute);
assert!(dt.second() == second & 0xfe);
}
}