//! Types that specify what is contained in a ZIP. #[cfg(doc)] use {crate::read::ZipFile, crate::write::FileOptions}; #[cfg(not(any( all(target_arch = "arm", target_pointer_width = "32"), target_arch = "mips", target_arch = "powerpc" )))] use std::sync::atomic; #[cfg(any( all(target_arch = "arm", target_pointer_width = "32"), target_arch = "mips", target_arch = "powerpc" ))] mod atomic { use crossbeam_utils::sync::ShardedLock; pub use std::sync::atomic::Ordering; #[derive(Debug, Default)] pub struct AtomicU64 { value: ShardedLock, } impl AtomicU64 { pub fn new(v: u64) -> Self { Self { value: ShardedLock::new(v), } } pub fn get_mut(&mut self) -> &mut u64 { self.value.get_mut().unwrap() } pub fn load(&self, _: Ordering) -> u64 { *self.value.read().unwrap() } pub fn store(&self, value: u64, _: Ordering) { *self.value.write().unwrap() = value; } } } #[cfg(feature = "time")] use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time}; #[derive(Clone, Copy, Debug, PartialEq)] pub enum System { Dos = 0, Unix = 3, Unknown, } impl System { pub fn from_u8(system: u8) -> System { use self::System::*; match system { 0 => Dos, 3 => Unix, _ => Unknown, } } } /// Representation of a moment in time. /// /// Zip files use an old format from DOS to store timestamps, /// with its own set of peculiarities. /// For example, it has a resolution of 2 seconds! /// /// A [`DateTime`] can be stored directly in a zipfile with [`FileOptions::last_modified_time`], /// or read from one with [`ZipFile::last_modified`] /// /// # Warning /// /// Because there is no timezone associated with the [`DateTime`], they should ideally only /// be used for user-facing descriptions. This also means [`DateTime::to_time`] returns an /// [`OffsetDateTime`] (which is the equivalent of chrono's `NaiveDateTime`). /// /// Modern zip files store more precise timestamps, which are ignored by [`crate::read::ZipArchive`], /// so keep in mind that these timestamps are unreliable. [We're working on this](https://github.com/zip-rs/zip/issues/156#issuecomment-652981904). #[derive(Debug, Clone, Copy)] pub struct DateTime { year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8, } impl ::std::default::Default for DateTime { /// Constructs an 'default' datetime of 1980-01-01 00:00:00 fn default() -> DateTime { DateTime { year: 1980, month: 1, day: 1, hour: 0, minute: 0, second: 0, } } } impl DateTime { /// Converts an msdos (u16, u16) pair to a DateTime object pub fn from_msdos(datepart: u16, timepart: u16) -> DateTime { let seconds = (timepart & 0b0000000000011111) << 1; let minutes = (timepart & 0b0000011111100000) >> 5; let hours = (timepart & 0b1111100000000000) >> 11; let days = datepart & 0b0000000000011111; let months = (datepart & 0b0000000111100000) >> 5; let years = (datepart & 0b1111111000000000) >> 9; DateTime { year: (years + 1980) as u16, month: months as u8, day: days as u8, hour: hours as u8, minute: minutes as u8, second: seconds as u8, } } /// Constructs a DateTime from a specific date and time /// /// The bounds are: /// * year: [1980, 2107] /// * month: [1, 12] /// * day: [1, 31] /// * hour: [0, 23] /// * minute: [0, 59] /// * second: [0, 60] #[allow(clippy::result_unit_err)] pub fn from_date_and_time( year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8, ) -> Result { if (1980..=2107).contains(&year) && month >= 1 && month <= 12 && day >= 1 && day <= 31 && hour <= 23 && minute <= 59 && second <= 60 { Ok(DateTime { year, month, day, hour, minute, second, }) } else { Err(()) } } #[cfg(feature = "time")] /// Converts a OffsetDateTime object to a DateTime /// /// Returns `Err` when this object is out of bounds #[allow(clippy::result_unit_err)] pub fn from_time(dt: OffsetDateTime) -> Result { if dt.year() >= 1980 && dt.year() <= 2107 { Ok(DateTime { year: (dt.year()) as u16, month: (dt.month()) as u8, day: dt.day() as u8, hour: dt.hour() as u8, minute: dt.minute() as u8, second: dt.second() as u8, }) } else { Err(()) } } /// Gets the time portion of this datetime in the msdos representation pub fn timepart(&self) -> u16 { ((self.second as u16) >> 1) | ((self.minute as u16) << 5) | ((self.hour as u16) << 11) } /// Gets the date portion of this datetime in the msdos representation pub fn datepart(&self) -> u16 { (self.day as u16) | ((self.month as u16) << 5) | ((self.year - 1980) << 9) } #[cfg(feature = "time")] /// Converts the DateTime to a OffsetDateTime structure pub fn to_time(&self) -> Result { use std::convert::TryFrom; let date = Date::from_calendar_date(self.year as i32, Month::try_from(self.month)?, self.day)?; let time = Time::from_hms(self.hour, self.minute, self.second)?; Ok(PrimitiveDateTime::new(date, time).assume_utc()) } /// Get the year. There is no epoch, i.e. 2018 will be returned as 2018. pub fn year(&self) -> u16 { self.year } /// Get the month, where 1 = january and 12 = december /// /// # Warning /// /// When read from a zip file, this may not be a reasonable value pub fn month(&self) -> u8 { self.month } /// Get the day /// /// # Warning /// /// When read from a zip file, this may not be a reasonable value pub fn day(&self) -> u8 { self.day } /// Get the hour /// /// # Warning /// /// When read from a zip file, this may not be a reasonable value pub fn hour(&self) -> u8 { self.hour } /// Get the minute /// /// # Warning /// /// When read from a zip file, this may not be a reasonable value pub fn minute(&self) -> u8 { self.minute } /// Get the second /// /// # Warning /// /// When read from a zip file, this may not be a reasonable value pub fn second(&self) -> u8 { self.second } } pub const DEFAULT_VERSION: u8 = 46; /// A type like `AtomicU64` except it implements `Clone` and has predefined /// ordering. /// /// It uses `Relaxed` ordering because it is not used for synchronisation. #[derive(Debug)] pub struct AtomicU64(atomic::AtomicU64); impl AtomicU64 { pub fn new(v: u64) -> Self { Self(atomic::AtomicU64::new(v)) } pub fn load(&self) -> u64 { self.0.load(atomic::Ordering::Relaxed) } pub fn store(&self, val: u64) { self.0.store(val, atomic::Ordering::Relaxed) } pub fn get_mut(&mut self) -> &mut u64 { self.0.get_mut() } } impl Clone for AtomicU64 { fn clone(&self) -> Self { Self(atomic::AtomicU64::new(self.load())) } } /// Structure representing a ZIP file. #[derive(Debug, Clone)] pub struct ZipFileData { /// Compatibility of the file attribute information pub system: System, /// Specification version pub version_made_by: u8, /// True if the file is encrypted. pub encrypted: bool, /// True if the file uses a data-descriptor section pub using_data_descriptor: bool, /// Compression method used to store the file pub compression_method: crate::compression::CompressionMethod, /// Compression level to store the file pub compression_level: Option, /// Last modified time. This will only have a 2 second precision. pub last_modified_time: DateTime, /// CRC32 checksum pub crc32: u32, /// Size of the file in the ZIP pub compressed_size: u64, /// Size of the file when extracted pub uncompressed_size: u64, /// Name of the file pub file_name: String, /// Raw file name. To be used when file_name was incorrectly decoded. pub file_name_raw: Vec, /// Extra field usually used for storage expansion pub extra_field: Vec, /// File comment pub file_comment: String, /// Specifies where the local header of the file starts pub header_start: u64, /// Specifies where the central header of the file starts /// /// Note that when this is not known, it is set to 0 pub central_header_start: u64, /// Specifies where the compressed data of the file starts pub data_start: AtomicU64, /// External file attributes pub external_attributes: u32, /// Reserve local ZIP64 extra field pub large_file: bool, /// AES mode if applicable pub aes_mode: Option<(AesMode, AesVendorVersion)>, } impl ZipFileData { pub fn file_name_sanitized(&self) -> ::std::path::PathBuf { let no_null_filename = match self.file_name.find('\0') { Some(index) => &self.file_name[0..index], None => &self.file_name, } .to_string(); // zip files can contain both / and \ as separators regardless of the OS // and as we want to return a sanitized PathBuf that only supports the // OS separator let's convert incompatible separators to compatible ones let separator = ::std::path::MAIN_SEPARATOR; let opposite_separator = match separator { '/' => '\\', _ => '/', }; let filename = no_null_filename.replace(&opposite_separator.to_string(), &separator.to_string()); ::std::path::Path::new(&filename) .components() .filter(|component| matches!(*component, ::std::path::Component::Normal(..))) .fold(::std::path::PathBuf::new(), |mut path, ref cur| { path.push(cur.as_os_str()); path }) } pub fn zip64_extension(&self) -> bool { self.uncompressed_size > 0xFFFFFFFF || self.compressed_size > 0xFFFFFFFF || self.header_start > 0xFFFFFFFF } pub fn version_needed(&self) -> u16 { // higher versions matched first match (self.zip64_extension(), self.compression_method) { #[cfg(feature = "bzip2")] (_, crate::compression::CompressionMethod::Bzip2) => 46, (true, _) => 45, _ => 20, } } } /// 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)] pub enum AesVendorVersion { Ae1, Ae2, } /// AES variant used. #[derive(Copy, Clone, Debug)] pub enum AesMode { Aes128, Aes192, Aes256, } #[cfg(feature = "aes-crypto")] impl AesMode { pub fn salt_length(&self) -> usize { self.key_length() / 2 } pub fn key_length(&self) -> usize { match self { Self::Aes128 => 16, Self::Aes192 => 24, Self::Aes256 => 32, } } } #[cfg(test)] mod test { #[test] fn system() { use super::System; assert_eq!(System::Dos as u16, 0u16); assert_eq!(System::Unix as u16, 3u16); assert_eq!(System::from_u8(0), System::Dos); assert_eq!(System::from_u8(3), System::Unix); } #[test] fn sanitize() { use super::*; let file_name = "/path/../../../../etc/./passwd\0/etc/shadow".to_string(); let data = ZipFileData { system: System::Dos, version_made_by: 0, encrypted: false, using_data_descriptor: false, compression_method: crate::compression::CompressionMethod::Stored, compression_level: None, last_modified_time: DateTime::default(), crc32: 0, compressed_size: 0, uncompressed_size: 0, file_name: file_name.clone(), file_name_raw: file_name.into_bytes(), extra_field: Vec::new(), file_comment: String::new(), header_start: 0, data_start: AtomicU64::new(0), central_header_start: 0, external_attributes: 0, large_file: false, aes_mode: None, }; assert_eq!( data.file_name_sanitized(), ::std::path::PathBuf::from("path/etc/passwd") ); } #[test] #[allow(clippy::unusual_byte_groupings)] fn datetime_default() { use super::DateTime; let dt = DateTime::default(); assert_eq!(dt.timepart(), 0); assert_eq!(dt.datepart(), 0b0000000_0001_00001); } #[test] #[allow(clippy::unusual_byte_groupings)] fn datetime_max() { use super::DateTime; let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 60).unwrap(); assert_eq!(dt.timepart(), 0b10111_111011_11110); assert_eq!(dt.datepart(), 0b1111111_1100_11111); } #[test] fn datetime_bounds() { use super::DateTime; assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok()); assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err()); assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err()); assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err()); assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok()); assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok()); assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err()); assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err()); assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err()); assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err()); assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err()); assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err()); } #[cfg(feature = "time")] use time::{format_description::well_known::Rfc3339, OffsetDateTime}; #[cfg(feature = "time")] #[test] fn datetime_from_time_bounds() { use super::DateTime; use time::macros::datetime; // 1979-12-31 23:59:59 assert!(DateTime::from_time(datetime!(1979-12-31 23:59:59 UTC)).is_err()); // 1980-01-01 00:00:00 assert!(DateTime::from_time(datetime!(1980-01-01 00:00:00 UTC)).is_ok()); // 2107-12-31 23:59:59 assert!(DateTime::from_time(datetime!(2107-12-31 23:59:59 UTC)).is_ok()); // 2108-01-01 00:00:00 assert!(DateTime::from_time(datetime!(2108-01-01 00:00:00 UTC)).is_err()); } #[test] fn time_conversion() { use super::DateTime; let dt = DateTime::from_msdos(0x4D71, 0x54CF); assert_eq!(dt.year(), 2018); assert_eq!(dt.month(), 11); assert_eq!(dt.day(), 17); assert_eq!(dt.hour(), 10); assert_eq!(dt.minute(), 38); assert_eq!(dt.second(), 30); #[cfg(feature = "time")] assert_eq!( dt.to_time().unwrap().format(&Rfc3339).unwrap(), "2018-11-17T10:38:30Z" ); } #[test] fn time_out_of_bounds() { use super::DateTime; let dt = DateTime::from_msdos(0xFFFF, 0xFFFF); assert_eq!(dt.year(), 2107); assert_eq!(dt.month(), 15); assert_eq!(dt.day(), 31); assert_eq!(dt.hour(), 31); assert_eq!(dt.minute(), 63); assert_eq!(dt.second(), 62); #[cfg(feature = "time")] assert!(dt.to_time().is_err()); let dt = DateTime::from_msdos(0x0000, 0x0000); assert_eq!(dt.year(), 1980); assert_eq!(dt.month(), 0); assert_eq!(dt.day(), 0); assert_eq!(dt.hour(), 0); assert_eq!(dt.minute(), 0); assert_eq!(dt.second(), 0); #[cfg(feature = "time")] assert!(dt.to_time().is_err()); } #[cfg(feature = "time")] #[test] fn time_at_january() { use super::DateTime; // 2020-01-01 00:00:00 let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap(); assert!(DateTime::from_time(clock).is_ok()); } }