zip/
types.rs

1//! Types that specify what is contained in a ZIP.
2use crate::cp437::FromCp437;
3use crate::write::FileOptionExtension;
4use crate::zipcrypto::EncryptWith;
5use core::fmt::{self, Debug, Formatter};
6use core::mem;
7use std::ffi::OsStr;
8use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR};
9use std::sync::{Arc, OnceLock};
10
11#[cfg(feature = "chrono")]
12use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
13#[cfg(feature = "jiff-02")]
14use jiff::civil;
15
16use crate::result::{invalid, ZipError, ZipResult};
17use crate::spec::{self, FixedSizeBlock, Pod};
18
19pub(crate) mod ffi {
20    pub const S_IFDIR: u32 = 0o0040000;
21    pub const S_IFREG: u32 = 0o0100000;
22    pub const S_IFLNK: u32 = 0o0120000;
23}
24
25use crate::extra_fields::ExtraField;
26use crate::read::find_data_start;
27use crate::result::DateTimeRangeError;
28use crate::spec::is_dir;
29use crate::types::ffi::S_IFDIR;
30use crate::{CompressionMethod, ZIP64_BYTES_THR};
31use std::io::{Read, Seek};
32#[cfg(feature = "time")]
33use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time};
34
35pub(crate) struct ZipRawValues {
36    pub(crate) crc32: u32,
37    pub(crate) compressed_size: u64,
38    pub(crate) uncompressed_size: u64,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
42#[repr(u8)]
43pub enum System {
44    Dos = 0,
45    Unix = 3,
46    #[default]
47    Unknown,
48}
49
50impl From<u8> for System {
51    fn from(system: u8) -> Self {
52        match system {
53            0 => Self::Dos,
54            3 => Self::Unix,
55            _ => Self::Unknown,
56        }
57    }
58}
59
60impl From<System> for u8 {
61    fn from(system: System) -> Self {
62        match system {
63            System::Dos => 0,
64            System::Unix => 3,
65            System::Unknown => 4,
66        }
67    }
68}
69
70/// Metadata for a file to be written
71#[derive(Clone, Debug, Copy, Eq, PartialEq)]
72pub struct FileOptions<'k, T: FileOptionExtension> {
73    pub(crate) compression_method: CompressionMethod,
74    pub(crate) compression_level: Option<i64>,
75    pub(crate) last_modified_time: DateTime,
76    pub(crate) permissions: Option<u32>,
77    pub(crate) large_file: bool,
78    pub(crate) encrypt_with: Option<EncryptWith<'k>>,
79    pub(crate) extended_options: T,
80    pub(crate) alignment: u16,
81    #[cfg(feature = "deflate-zopfli")]
82    pub(super) zopfli_buffer_size: Option<usize>,
83    #[cfg(feature = "aes-crypto")]
84    pub(crate) aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
85}
86/// Simple File Options. Can be copied and good for simple writing zip files
87pub type SimpleFileOptions = FileOptions<'static, ()>;
88
89/// Representation of a moment in time.
90///
91/// Zip files use an old format from DOS to store timestamps,
92/// with its own set of peculiarities.
93/// For example, it has a resolution of 2 seconds!
94///
95/// A [`DateTime`] can be stored directly in a zipfile with [`FileOptions::last_modified_time`],
96/// or read from one with [`ZipFile::last_modified`](crate::read::ZipFile::last_modified).
97///
98/// # Warning
99///
100/// Because there is no timezone associated with the [`DateTime`], they should ideally only
101/// be used for user-facing descriptions.
102///
103/// Modern zip files store more precise timestamps; see [`crate::extra_fields::ExtendedTimestamp`]
104/// for details.
105#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
106pub struct DateTime {
107    datepart: u16,
108    timepart: u16,
109}
110
111impl Debug for DateTime {
112    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
113        if *self == Self::default() {
114            return f.write_str("DateTime::default()");
115        }
116        f.write_fmt(format_args!(
117            "DateTime::from_date_and_time({}, {}, {}, {}, {}, {})?",
118            self.year(),
119            self.month(),
120            self.day(),
121            self.hour(),
122            self.minute(),
123            self.second()
124        ))
125    }
126}
127
128impl DateTime {
129    /// Constructs a default datetime of 1980-01-01 00:00:00.
130    pub const DEFAULT: Self = DateTime {
131        datepart: 0b0000000000100001,
132        timepart: 0,
133    };
134
135    /// Returns the current time if possible, otherwise the default of 1980-01-01.
136    #[cfg(feature = "time")]
137    pub fn default_for_write() -> Self {
138        let now = OffsetDateTime::now_utc();
139        PrimitiveDateTime::new(now.date(), now.time())
140            .try_into()
141            .unwrap_or_else(|_| DateTime::default())
142    }
143
144    /// Returns the current time if possible, otherwise the default of 1980-01-01.
145    #[cfg(not(feature = "time"))]
146    pub fn default_for_write() -> Self {
147        DateTime::default()
148    }
149}
150
151#[cfg(feature = "_arbitrary")]
152impl arbitrary::Arbitrary<'_> for DateTime {
153    fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result<Self> {
154        let year: u16 = u.int_in_range(1980..=2107)?;
155        let month: u16 = u.int_in_range(1..=12)?;
156        let day: u16 = u.int_in_range(1..=31)?;
157        let datepart = day | (month << 5) | ((year - 1980) << 9);
158        let hour: u16 = u.int_in_range(0..=23)?;
159        let minute: u16 = u.int_in_range(0..=59)?;
160        let second: u16 = u.int_in_range(0..=58)?;
161        let timepart = (second >> 1) | (minute << 5) | (hour << 11);
162        Ok(DateTime { datepart, timepart })
163    }
164}
165
166#[cfg(feature = "chrono")]
167impl TryFrom<NaiveDateTime> for DateTime {
168    type Error = DateTimeRangeError;
169
170    fn try_from(value: NaiveDateTime) -> Result<Self, Self::Error> {
171        DateTime::from_date_and_time(
172            value.year().try_into()?,
173            value.month().try_into()?,
174            value.day().try_into()?,
175            value.hour().try_into()?,
176            value.minute().try_into()?,
177            value.second().try_into()?,
178        )
179    }
180}
181
182#[cfg(feature = "chrono")]
183impl TryFrom<DateTime> for NaiveDateTime {
184    type Error = DateTimeRangeError;
185
186    fn try_from(value: DateTime) -> Result<Self, Self::Error> {
187        let date = NaiveDate::from_ymd_opt(
188            value.year().into(),
189            value.month().into(),
190            value.day().into(),
191        )
192        .ok_or(DateTimeRangeError)?;
193        let time = NaiveTime::from_hms_opt(
194            value.hour().into(),
195            value.minute().into(),
196            value.second().into(),
197        )
198        .ok_or(DateTimeRangeError)?;
199        Ok(NaiveDateTime::new(date, time))
200    }
201}
202
203#[cfg(feature = "jiff-02")]
204impl TryFrom<civil::DateTime> for DateTime {
205    type Error = DateTimeRangeError;
206
207    fn try_from(value: civil::DateTime) -> Result<Self, Self::Error> {
208        Self::from_date_and_time(
209            value.year().try_into()?,
210            value.month() as u8,
211            value.day() as u8,
212            value.hour() as u8,
213            value.minute() as u8,
214            value.second() as u8,
215        )
216    }
217}
218
219#[cfg(feature = "jiff-02")]
220impl TryFrom<DateTime> for civil::DateTime {
221    type Error = jiff::Error;
222
223    fn try_from(value: DateTime) -> Result<Self, Self::Error> {
224        Self::new(
225            value.year() as i16,
226            value.month() as i8,
227            value.day() as i8,
228            value.hour() as i8,
229            value.minute() as i8,
230            value.second() as i8,
231            0,
232        )
233    }
234}
235
236impl TryFrom<(u16, u16)> for DateTime {
237    type Error = DateTimeRangeError;
238
239    #[inline]
240    fn try_from(values: (u16, u16)) -> Result<Self, Self::Error> {
241        Self::try_from_msdos(values.0, values.1)
242    }
243}
244
245impl From<DateTime> for (u16, u16) {
246    #[inline]
247    fn from(dt: DateTime) -> Self {
248        (dt.datepart(), dt.timepart())
249    }
250}
251
252impl Default for DateTime {
253    /// Constructs an 'default' datetime of 1980-01-01 00:00:00
254    fn default() -> DateTime {
255        DateTime::DEFAULT
256    }
257}
258
259impl fmt::Display for DateTime {
260    #[inline]
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        write!(
263            f,
264            "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
265            self.year(),
266            self.month(),
267            self.day(),
268            self.hour(),
269            self.minute(),
270            self.second()
271        )
272    }
273}
274
275impl DateTime {
276    /// Converts an msdos (u16, u16) pair to a DateTime object
277    ///
278    /// # Safety
279    /// The caller must ensure the date and time are valid.
280    pub const unsafe fn from_msdos_unchecked(datepart: u16, timepart: u16) -> DateTime {
281        DateTime { datepart, timepart }
282    }
283
284    /// Converts an msdos (u16, u16) pair to a DateTime object if it represents a valid date and
285    /// time.
286    pub fn try_from_msdos(datepart: u16, timepart: u16) -> Result<DateTime, DateTimeRangeError> {
287        let seconds = (timepart & 0b0000000000011111) << 1;
288        let minutes = (timepart & 0b0000011111100000) >> 5;
289        let hours = (timepart & 0b1111100000000000) >> 11;
290        let days = datepart & 0b0000000000011111;
291        let months = (datepart & 0b0000000111100000) >> 5;
292        let years = (datepart & 0b1111111000000000) >> 9;
293        Self::from_date_and_time(
294            years.checked_add(1980).ok_or(DateTimeRangeError)?,
295            months.try_into()?,
296            days.try_into()?,
297            hours.try_into()?,
298            minutes.try_into()?,
299            seconds.try_into()?,
300        )
301    }
302
303    /// Constructs a DateTime from a specific date and time
304    ///
305    /// The bounds are:
306    /// * year: [1980, 2107]
307    /// * month: [1, 12]
308    /// * day: [1, 28..=31]
309    /// * hour: [0, 23]
310    /// * minute: [0, 59]
311    /// * second: [0, 58]
312    pub fn from_date_and_time(
313        year: u16,
314        month: u8,
315        day: u8,
316        hour: u8,
317        minute: u8,
318        second: u8,
319    ) -> Result<DateTime, DateTimeRangeError> {
320        fn is_leap_year(year: u16) -> bool {
321            (year % 4 == 0) && ((year % 25 != 0) || (year % 16 == 0))
322        }
323
324        if (1980..=2107).contains(&year)
325            && (1..=12).contains(&month)
326            && (1..=31).contains(&day)
327            && hour <= 23
328            && minute <= 59
329            && second <= 60
330        {
331            let second = second.min(58); // exFAT can't store leap seconds
332            let max_day = match month {
333                1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
334                4 | 6 | 9 | 11 => 30,
335                2 if is_leap_year(year) => 29,
336                2 => 28,
337                _ => unreachable!(),
338            };
339            if day > max_day {
340                return Err(DateTimeRangeError);
341            }
342            let datepart = (day as u16) | ((month as u16) << 5) | ((year - 1980) << 9);
343            let timepart = ((second as u16) >> 1) | ((minute as u16) << 5) | ((hour as u16) << 11);
344            Ok(DateTime { datepart, timepart })
345        } else {
346            Err(DateTimeRangeError)
347        }
348    }
349
350    /// Indicates whether this date and time can be written to a zip archive.
351    pub fn is_valid(&self) -> bool {
352        Self::try_from_msdos(self.datepart, self.timepart).is_ok()
353    }
354
355    #[cfg(feature = "time")]
356    /// Converts a OffsetDateTime object to a DateTime
357    ///
358    /// Returns `Err` when this object is out of bounds
359    #[deprecated(since = "0.6.4", note = "use `DateTime::try_from()` instead")]
360    pub fn from_time(dt: OffsetDateTime) -> Result<DateTime, DateTimeRangeError> {
361        dt.try_into()
362    }
363
364    /// Gets the time portion of this datetime in the msdos representation
365    pub const fn timepart(&self) -> u16 {
366        self.timepart
367    }
368
369    /// Gets the date portion of this datetime in the msdos representation
370    pub const fn datepart(&self) -> u16 {
371        self.datepart
372    }
373
374    #[cfg(feature = "time")]
375    /// Converts the DateTime to a OffsetDateTime structure
376    #[deprecated(since = "1.3.1", note = "use `OffsetDateTime::try_from()` instead")]
377    pub fn to_time(&self) -> Result<OffsetDateTime, ComponentRange> {
378        (*self).try_into()
379    }
380
381    /// Get the year. There is no epoch, i.e. 2018 will be returned as 2018.
382    pub const fn year(&self) -> u16 {
383        (self.datepart >> 9) + 1980
384    }
385
386    /// Get the month, where 1 = january and 12 = december
387    ///
388    /// # Warning
389    ///
390    /// When read from a zip file, this may not be a reasonable value
391    pub const fn month(&self) -> u8 {
392        ((self.datepart & 0b0000000111100000) >> 5) as u8
393    }
394
395    /// Get the day
396    ///
397    /// # Warning
398    ///
399    /// When read from a zip file, this may not be a reasonable value
400    pub const fn day(&self) -> u8 {
401        (self.datepart & 0b0000000000011111) as u8
402    }
403
404    /// Get the hour
405    ///
406    /// # Warning
407    ///
408    /// When read from a zip file, this may not be a reasonable value
409    pub const fn hour(&self) -> u8 {
410        (self.timepart >> 11) as u8
411    }
412
413    /// Get the minute
414    ///
415    /// # Warning
416    ///
417    /// When read from a zip file, this may not be a reasonable value
418    pub const fn minute(&self) -> u8 {
419        ((self.timepart & 0b0000011111100000) >> 5) as u8
420    }
421
422    /// Get the second
423    ///
424    /// # Warning
425    ///
426    /// When read from a zip file, this may not be a reasonable value
427    pub const fn second(&self) -> u8 {
428        ((self.timepart & 0b0000000000011111) << 1) as u8
429    }
430}
431
432#[cfg(feature = "time")]
433impl TryFrom<OffsetDateTime> for DateTime {
434    type Error = DateTimeRangeError;
435
436    fn try_from(dt: OffsetDateTime) -> Result<Self, Self::Error> {
437        Self::try_from(PrimitiveDateTime::new(dt.date(), dt.time()))
438    }
439}
440
441#[cfg(feature = "time")]
442impl TryFrom<PrimitiveDateTime> for DateTime {
443    type Error = DateTimeRangeError;
444
445    fn try_from(dt: PrimitiveDateTime) -> Result<Self, Self::Error> {
446        Self::from_date_and_time(
447            dt.year().try_into()?,
448            dt.month().into(),
449            dt.day(),
450            dt.hour(),
451            dt.minute(),
452            dt.second(),
453        )
454    }
455}
456
457#[cfg(feature = "time")]
458impl TryFrom<DateTime> for OffsetDateTime {
459    type Error = ComponentRange;
460
461    fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
462        PrimitiveDateTime::try_from(dt).map(PrimitiveDateTime::assume_utc)
463    }
464}
465
466#[cfg(feature = "time")]
467impl TryFrom<DateTime> for PrimitiveDateTime {
468    type Error = ComponentRange;
469
470    fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
471        let date =
472            Date::from_calendar_date(dt.year() as i32, Month::try_from(dt.month())?, dt.day())?;
473        let time = Time::from_hms(dt.hour(), dt.minute(), dt.second())?;
474        Ok(PrimitiveDateTime::new(date, time))
475    }
476}
477
478pub const MIN_VERSION: u8 = 10;
479pub const DEFAULT_VERSION: u8 = 45;
480
481/// Structure representing a ZIP file.
482#[derive(Debug, Clone, Default)]
483pub struct ZipFileData {
484    /// Compatibility of the file attribute information
485    pub system: System,
486    /// Specification version
487    pub version_made_by: u8,
488    /// ZIP flags
489    pub flags: u16,
490    /// True if the file is encrypted.
491    pub encrypted: bool,
492    /// True if file_name and file_comment are UTF8
493    pub is_utf8: bool,
494    /// True if the file uses a data-descriptor section
495    pub using_data_descriptor: bool,
496    /// Compression method used to store the file
497    pub compression_method: crate::compression::CompressionMethod,
498    /// Compression level to store the file
499    pub compression_level: Option<i64>,
500    /// Last modified time. This will only have a 2 second precision.
501    pub last_modified_time: Option<DateTime>,
502    /// CRC32 checksum
503    pub crc32: u32,
504    /// Size of the file in the ZIP
505    pub compressed_size: u64,
506    /// Size of the file when extracted
507    pub uncompressed_size: u64,
508    /// Name of the file
509    pub file_name: Box<str>,
510    /// Raw file name. To be used when file_name was incorrectly decoded.
511    pub file_name_raw: Box<[u8]>,
512    /// Extra field usually used for storage expansion
513    pub extra_field: Option<Arc<Vec<u8>>>,
514    /// Extra field only written to central directory
515    pub central_extra_field: Option<Arc<Vec<u8>>>,
516    /// File comment
517    pub file_comment: Box<str>,
518    /// Specifies where the local header of the file starts
519    pub header_start: u64,
520    /// Specifies where the extra data of the file starts
521    pub extra_data_start: Option<u64>,
522    /// Specifies where the central header of the file starts
523    ///
524    /// Note that when this is not known, it is set to 0
525    pub central_header_start: u64,
526    /// Specifies where the compressed data of the file starts
527    pub data_start: OnceLock<u64>,
528    /// External file attributes
529    pub external_attributes: u32,
530    /// Reserve local ZIP64 extra field
531    pub large_file: bool,
532    /// AES mode if applicable
533    pub aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
534    /// Specifies where in the extra data the AES metadata starts
535    pub aes_extra_data_start: u64,
536
537    /// extra fields, see <https://libzip.org/specifications/extrafld.txt>
538    pub extra_fields: Vec<ExtraField>,
539}
540
541impl ZipFileData {
542    /// Get the starting offset of the data of the compressed file
543    pub fn data_start(&self, reader: &mut (impl Read + Seek + Sized)) -> ZipResult<u64> {
544        match self.data_start.get() {
545            Some(data_start) => Ok(*data_start),
546            None => Ok(find_data_start(self, reader)?),
547        }
548    }
549
550    #[allow(dead_code)]
551    pub fn is_dir(&self) -> bool {
552        is_dir(&self.file_name)
553    }
554
555    pub fn file_name_sanitized(&self) -> PathBuf {
556        let no_null_filename = match self.file_name.find('\0') {
557            Some(index) => &self.file_name[0..index],
558            None => &self.file_name,
559        }
560        .to_string();
561
562        // zip files can contain both / and \ as separators regardless of the OS
563        // and as we want to return a sanitized PathBuf that only supports the
564        // OS separator let's convert incompatible separators to compatible ones
565        let separator = MAIN_SEPARATOR;
566        let opposite_separator = match separator {
567            '/' => '\\',
568            _ => '/',
569        };
570        let filename =
571            no_null_filename.replace(&opposite_separator.to_string(), &separator.to_string());
572
573        Path::new(&filename)
574            .components()
575            .filter(|component| matches!(*component, Component::Normal(..)))
576            .fold(PathBuf::new(), |mut path, ref cur| {
577                path.push(cur.as_os_str());
578                path
579            })
580    }
581
582    /// Simplify the file name by removing the prefix and parent directories and only return normal components
583    pub(crate) fn simplified_components(&self) -> Option<Vec<&OsStr>> {
584        if self.file_name.contains('\0') {
585            return None;
586        }
587        let input = Path::new(OsStr::new(&*self.file_name));
588        crate::path::simplified_components(input)
589    }
590
591    pub(crate) fn enclosed_name(&self) -> Option<PathBuf> {
592        if self.file_name.contains('\0') {
593            return None;
594        }
595        let path = PathBuf::from(self.file_name.to_string());
596        let mut depth = 0usize;
597        for component in path.components() {
598            match component {
599                Component::Prefix(_) | Component::RootDir => return None,
600                Component::ParentDir => depth = depth.checked_sub(1)?,
601                Component::Normal(_) => depth += 1,
602                Component::CurDir => (),
603            }
604        }
605        Some(path)
606    }
607
608    /// Get unix mode for the file
609    pub(crate) const fn unix_mode(&self) -> Option<u32> {
610        if self.external_attributes == 0 {
611            return None;
612        }
613
614        match self.system {
615            System::Unix => Some(self.external_attributes >> 16),
616            System::Dos => {
617                // Interpret MS-DOS directory bit
618                let mut mode = if 0x10 == (self.external_attributes & 0x10) {
619                    ffi::S_IFDIR | 0o0775
620                } else {
621                    ffi::S_IFREG | 0o0664
622                };
623                if 0x01 == (self.external_attributes & 0x01) {
624                    // Read-only bit; strip write permissions
625                    mode &= 0o0555;
626                }
627                Some(mode)
628            }
629            _ => None,
630        }
631    }
632
633    /// PKZIP version needed to open this file (from APPNOTE 4.4.3.2).
634    pub fn version_needed(&self) -> u16 {
635        let compression_version: u16 = match self.compression_method {
636            CompressionMethod::Stored => MIN_VERSION.into(),
637            #[cfg(feature = "_deflate-any")]
638            CompressionMethod::Deflated => 20,
639            #[cfg(feature = "bzip2")]
640            CompressionMethod::Bzip2 => 46,
641            #[cfg(feature = "deflate64")]
642            CompressionMethod::Deflate64 => 21,
643            #[cfg(feature = "lzma")]
644            CompressionMethod::Lzma => 63,
645            #[cfg(feature = "xz")]
646            CompressionMethod::Xz => 63,
647            // APPNOTE doesn't specify a version for Zstandard
648            _ => DEFAULT_VERSION as u16,
649        };
650        let crypto_version: u16 = if self.aes_mode.is_some() {
651            51
652        } else if self.encrypted {
653            20
654        } else {
655            10
656        };
657        let misc_feature_version: u16 = if self.large_file {
658            45
659        } else if self
660            .unix_mode()
661            .is_some_and(|mode| mode & S_IFDIR == S_IFDIR)
662        {
663            // file is directory
664            20
665        } else {
666            10
667        };
668        compression_version
669            .max(crypto_version)
670            .max(misc_feature_version)
671    }
672    #[inline(always)]
673    pub(crate) fn extra_field_len(&self) -> usize {
674        self.extra_field
675            .as_ref()
676            .map(|v| v.len())
677            .unwrap_or_default()
678    }
679    #[inline(always)]
680    pub(crate) fn central_extra_field_len(&self) -> usize {
681        self.central_extra_field
682            .as_ref()
683            .map(|v| v.len())
684            .unwrap_or_default()
685    }
686
687    #[allow(clippy::too_many_arguments)]
688    pub(crate) fn initialize_local_block<S, T: FileOptionExtension>(
689        name: S,
690        options: &FileOptions<T>,
691        raw_values: ZipRawValues,
692        header_start: u64,
693        extra_data_start: Option<u64>,
694        aes_extra_data_start: u64,
695        compression_method: crate::compression::CompressionMethod,
696        aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
697        extra_field: &[u8],
698    ) -> Self
699    where
700        S: ToString,
701    {
702        let permissions = options.permissions.unwrap_or(0o100644);
703        let file_name: Box<str> = name.to_string().into_boxed_str();
704        let file_name_raw: Box<[u8]> = file_name.bytes().collect();
705        let mut local_block = ZipFileData {
706            system: System::Unix,
707            version_made_by: DEFAULT_VERSION,
708            flags: 0,
709            encrypted: options.encrypt_with.is_some() || {
710                #[cfg(feature = "aes-crypto")]
711                {
712                    options.aes_mode.is_some()
713                }
714                #[cfg(not(feature = "aes-crypto"))]
715                {
716                    false
717                }
718            },
719            using_data_descriptor: false,
720            is_utf8: !file_name.is_ascii(),
721            compression_method,
722            compression_level: options.compression_level,
723            last_modified_time: Some(options.last_modified_time),
724            crc32: raw_values.crc32,
725            compressed_size: raw_values.compressed_size,
726            uncompressed_size: raw_values.uncompressed_size,
727            file_name, // Never used for saving, but used as map key in insert_file_data()
728            file_name_raw,
729            extra_field: Some(extra_field.to_vec().into()),
730            central_extra_field: options.extended_options.central_extra_data().cloned(),
731            file_comment: String::with_capacity(0).into_boxed_str(),
732            header_start,
733            data_start: OnceLock::new(),
734            central_header_start: 0,
735            external_attributes: permissions << 16,
736            large_file: options.large_file,
737            aes_mode,
738            extra_fields: Vec::new(),
739            extra_data_start,
740            aes_extra_data_start,
741        };
742        local_block.version_made_by = local_block.version_needed() as u8;
743        local_block
744    }
745
746    pub(crate) fn from_local_block<R: std::io::Read>(
747        block: ZipLocalEntryBlock,
748        reader: &mut R,
749    ) -> ZipResult<Self> {
750        let ZipLocalEntryBlock {
751            // magic,
752            version_made_by,
753            flags,
754            compression_method,
755            last_mod_time,
756            last_mod_date,
757            crc32,
758            compressed_size,
759            uncompressed_size,
760            file_name_length,
761            extra_field_length,
762            ..
763        } = block;
764
765        let encrypted: bool = flags & 1 == 1;
766        if encrypted {
767            return Err(ZipError::UnsupportedArchive(
768                "Encrypted files are not supported",
769            ));
770        }
771
772        /* FIXME: these were previously incorrect: add testing! */
773        /* flags & (1 << 3) != 0 */
774        let using_data_descriptor: bool = flags & (1 << 3) == 1 << 3;
775        if using_data_descriptor {
776            return Err(ZipError::UnsupportedArchive(
777                "The file length is not available in the local header",
778            ));
779        }
780
781        /* flags & (1 << 1) != 0 */
782        let is_utf8: bool = flags & (1 << 11) != 0;
783        let compression_method = crate::CompressionMethod::parse_from_u16(compression_method);
784        let file_name_length: usize = file_name_length.into();
785        let extra_field_length: usize = extra_field_length.into();
786
787        let mut file_name_raw = vec![0u8; file_name_length];
788        reader.read_exact(&mut file_name_raw)?;
789        let mut extra_field = vec![0u8; extra_field_length];
790        reader.read_exact(&mut extra_field)?;
791
792        let file_name: Box<str> = match is_utf8 {
793            true => String::from_utf8_lossy(&file_name_raw).into(),
794            false => file_name_raw.clone().from_cp437().into(),
795        };
796
797        let system: u8 = (version_made_by >> 8).try_into().unwrap();
798        Ok(ZipFileData {
799            system: System::from(system),
800            /* NB: this strips the top 8 bits! */
801            version_made_by: version_made_by as u8,
802            flags,
803            encrypted,
804            using_data_descriptor,
805            is_utf8,
806            compression_method,
807            compression_level: None,
808            last_modified_time: DateTime::try_from_msdos(last_mod_date, last_mod_time).ok(),
809            crc32,
810            compressed_size: compressed_size.into(),
811            uncompressed_size: uncompressed_size.into(),
812            file_name,
813            file_name_raw: file_name_raw.into(),
814            extra_field: Some(Arc::new(extra_field)),
815            central_extra_field: None,
816            file_comment: String::with_capacity(0).into_boxed_str(), // file comment is only available in the central directory
817            // header_start and data start are not available, but also don't matter, since seeking is
818            // not available.
819            header_start: 0,
820            data_start: OnceLock::new(),
821            central_header_start: 0,
822            // The external_attributes field is only available in the central directory.
823            // We set this to zero, which should be valid as the docs state 'If input came
824            // from standard input, this field is set to zero.'
825            external_attributes: 0,
826            large_file: false,
827            aes_mode: None,
828            extra_fields: Vec::new(),
829            extra_data_start: None,
830            aes_extra_data_start: 0,
831        })
832    }
833
834    fn is_utf8(&self) -> bool {
835        std::str::from_utf8(&self.file_name_raw).is_ok()
836    }
837
838    fn is_ascii(&self) -> bool {
839        self.file_name_raw.is_ascii()
840    }
841
842    fn flags(&self) -> u16 {
843        let utf8_bit: u16 = if self.is_utf8() && !self.is_ascii() {
844            1u16 << 11
845        } else {
846            0
847        };
848
849        let using_data_descriptor_bit = if self.using_data_descriptor {
850            1u16 << 3
851        } else {
852            0
853        };
854
855        let encrypted_bit: u16 = if self.encrypted { 1u16 << 0 } else { 0 };
856
857        utf8_bit | using_data_descriptor_bit | encrypted_bit
858    }
859
860    fn clamp_size_field(&self, field: u64) -> u32 {
861        if self.large_file {
862            spec::ZIP64_BYTES_THR as u32
863        } else {
864            field.min(spec::ZIP64_BYTES_THR).try_into().unwrap()
865        }
866    }
867
868    pub(crate) fn local_block(&self) -> ZipResult<ZipLocalEntryBlock> {
869        let (compressed_size, uncompressed_size) = if self.using_data_descriptor {
870            (0, 0)
871        } else {
872            (
873                self.clamp_size_field(self.compressed_size),
874                self.clamp_size_field(self.uncompressed_size),
875            )
876        };
877        let extra_field_length: u16 = self
878            .extra_field_len()
879            .try_into()
880            .map_err(|_| invalid!("Extra data field is too large"))?;
881
882        let last_modified_time = self
883            .last_modified_time
884            .unwrap_or_else(DateTime::default_for_write);
885        Ok(ZipLocalEntryBlock {
886            magic: ZipLocalEntryBlock::MAGIC,
887            version_made_by: self.version_needed(),
888            flags: self.flags(),
889            compression_method: self.compression_method.serialize_to_u16(),
890            last_mod_time: last_modified_time.timepart(),
891            last_mod_date: last_modified_time.datepart(),
892            crc32: self.crc32,
893            compressed_size,
894            uncompressed_size,
895            file_name_length: self.file_name_raw.len().try_into().unwrap(),
896            extra_field_length,
897        })
898    }
899
900    pub(crate) fn block(&self) -> ZipResult<ZipCentralEntryBlock> {
901        let extra_field_len: u16 = self.extra_field_len().try_into().unwrap();
902        let central_extra_field_len: u16 = self.central_extra_field_len().try_into().unwrap();
903        let last_modified_time = self
904            .last_modified_time
905            .unwrap_or_else(DateTime::default_for_write);
906        let version_to_extract = self.version_needed();
907        let version_made_by = (self.version_made_by as u16).max(version_to_extract);
908        Ok(ZipCentralEntryBlock {
909            magic: ZipCentralEntryBlock::MAGIC,
910            version_made_by: ((self.system as u16) << 8) | version_made_by,
911            version_to_extract,
912            flags: self.flags(),
913            compression_method: self.compression_method.serialize_to_u16(),
914            last_mod_time: last_modified_time.timepart(),
915            last_mod_date: last_modified_time.datepart(),
916            crc32: self.crc32,
917            compressed_size: self
918                .compressed_size
919                .min(spec::ZIP64_BYTES_THR)
920                .try_into()
921                .unwrap(),
922            uncompressed_size: self
923                .uncompressed_size
924                .min(spec::ZIP64_BYTES_THR)
925                .try_into()
926                .unwrap(),
927            file_name_length: self.file_name_raw.len().try_into().unwrap(),
928            extra_field_length: extra_field_len.checked_add(central_extra_field_len).ok_or(
929                invalid!("Extra field length in central directory exceeds 64KiB"),
930            )?,
931            file_comment_length: self.file_comment.len().try_into().unwrap(),
932            disk_number: 0,
933            internal_file_attributes: 0,
934            external_file_attributes: self.external_attributes,
935            offset: self
936                .header_start
937                .min(spec::ZIP64_BYTES_THR)
938                .try_into()
939                .unwrap(),
940        })
941    }
942
943    pub(crate) fn zip64_extra_field_block(&self) -> Option<Zip64ExtraFieldBlock> {
944        Zip64ExtraFieldBlock::maybe_new(
945            self.large_file,
946            self.uncompressed_size,
947            self.compressed_size,
948            self.header_start,
949        )
950    }
951
952    pub(crate) fn write_data_descriptor<W: std::io::Write>(
953        &self,
954        writer: &mut W,
955        auto_large_file: bool,
956    ) -> Result<(), ZipError> {
957        if self.large_file {
958            return self.zip64_data_descriptor_block().write(writer);
959        }
960        if self.compressed_size > spec::ZIP64_BYTES_THR
961            || self.uncompressed_size > spec::ZIP64_BYTES_THR
962        {
963            if auto_large_file {
964                return self.zip64_data_descriptor_block().write(writer);
965            }
966            return Err(ZipError::Io(std::io::Error::other(
967                "Large file option has not been set - use .large_file(true) in options",
968            )));
969        }
970        self.data_descriptor_block().write(writer)
971    }
972
973    pub(crate) fn data_descriptor_block(&self) -> ZipDataDescriptorBlock {
974        ZipDataDescriptorBlock {
975            magic: ZipDataDescriptorBlock::MAGIC,
976            crc32: self.crc32,
977            compressed_size: self.compressed_size as u32,
978            uncompressed_size: self.uncompressed_size as u32,
979        }
980    }
981
982    pub(crate) fn zip64_data_descriptor_block(&self) -> Zip64DataDescriptorBlock {
983        Zip64DataDescriptorBlock {
984            magic: Zip64DataDescriptorBlock::MAGIC,
985            crc32: self.crc32,
986            compressed_size: self.compressed_size,
987            uncompressed_size: self.uncompressed_size,
988        }
989    }
990}
991
992#[derive(Copy, Clone, Debug)]
993#[repr(packed, C)]
994pub(crate) struct ZipCentralEntryBlock {
995    magic: spec::Magic,
996    pub version_made_by: u16,
997    pub version_to_extract: u16,
998    pub flags: u16,
999    pub compression_method: u16,
1000    pub last_mod_time: u16,
1001    pub last_mod_date: u16,
1002    pub crc32: u32,
1003    pub compressed_size: u32,
1004    pub uncompressed_size: u32,
1005    pub file_name_length: u16,
1006    pub extra_field_length: u16,
1007    pub file_comment_length: u16,
1008    pub disk_number: u16,
1009    pub internal_file_attributes: u16,
1010    pub external_file_attributes: u32,
1011    pub offset: u32,
1012}
1013
1014unsafe impl Pod for ZipCentralEntryBlock {}
1015
1016impl FixedSizeBlock for ZipCentralEntryBlock {
1017    const MAGIC: spec::Magic = spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE;
1018
1019    #[inline(always)]
1020    fn magic(self) -> spec::Magic {
1021        self.magic
1022    }
1023
1024    const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid Central Directory header");
1025
1026    to_and_from_le![
1027        (magic, spec::Magic),
1028        (version_made_by, u16),
1029        (version_to_extract, u16),
1030        (flags, u16),
1031        (compression_method, u16),
1032        (last_mod_time, u16),
1033        (last_mod_date, u16),
1034        (crc32, u32),
1035        (compressed_size, u32),
1036        (uncompressed_size, u32),
1037        (file_name_length, u16),
1038        (extra_field_length, u16),
1039        (file_comment_length, u16),
1040        (disk_number, u16),
1041        (internal_file_attributes, u16),
1042        (external_file_attributes, u32),
1043        (offset, u32),
1044    ];
1045}
1046
1047#[derive(Copy, Clone, Debug)]
1048#[repr(packed, C)]
1049pub(crate) struct ZipLocalEntryBlock {
1050    magic: spec::Magic,
1051    pub version_made_by: u16,
1052    pub flags: u16,
1053    pub compression_method: u16,
1054    pub last_mod_time: u16,
1055    pub last_mod_date: u16,
1056    pub crc32: u32,
1057    pub compressed_size: u32,
1058    pub uncompressed_size: u32,
1059    pub file_name_length: u16,
1060    pub extra_field_length: u16,
1061}
1062
1063unsafe impl Pod for ZipLocalEntryBlock {}
1064
1065impl FixedSizeBlock for ZipLocalEntryBlock {
1066    const MAGIC: spec::Magic = spec::Magic::LOCAL_FILE_HEADER_SIGNATURE;
1067
1068    #[inline(always)]
1069    fn magic(self) -> spec::Magic {
1070        self.magic
1071    }
1072
1073    const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid local file header");
1074
1075    to_and_from_le![
1076        (magic, spec::Magic),
1077        (version_made_by, u16),
1078        (flags, u16),
1079        (compression_method, u16),
1080        (last_mod_time, u16),
1081        (last_mod_date, u16),
1082        (crc32, u32),
1083        (compressed_size, u32),
1084        (uncompressed_size, u32),
1085        (file_name_length, u16),
1086        (extra_field_length, u16),
1087    ];
1088}
1089
1090#[derive(Copy, Clone, Debug)]
1091pub(crate) struct Zip64ExtraFieldBlock {
1092    magic: spec::ExtraFieldMagic,
1093    size: u16,
1094    uncompressed_size: Option<u64>,
1095    compressed_size: Option<u64>,
1096    header_start: Option<u64>,
1097    // Excluded fields:
1098    // u32: disk start number
1099}
1100
1101impl Zip64ExtraFieldBlock {
1102    pub(crate) fn maybe_new(
1103        large_file: bool,
1104        uncompressed_size: u64,
1105        compressed_size: u64,
1106        header_start: u64,
1107    ) -> Option<Zip64ExtraFieldBlock> {
1108        let mut size: u16 = 0;
1109        let uncompressed_size = if uncompressed_size >= ZIP64_BYTES_THR || large_file {
1110            size += mem::size_of::<u64>() as u16;
1111            Some(uncompressed_size)
1112        } else {
1113            None
1114        };
1115        let compressed_size = if compressed_size >= ZIP64_BYTES_THR || large_file {
1116            size += mem::size_of::<u64>() as u16;
1117            Some(compressed_size)
1118        } else {
1119            None
1120        };
1121        let header_start = if header_start >= ZIP64_BYTES_THR {
1122            size += mem::size_of::<u64>() as u16;
1123            Some(header_start)
1124        } else {
1125            None
1126        };
1127        if size == 0 {
1128            return None;
1129        }
1130
1131        Some(Zip64ExtraFieldBlock {
1132            magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG,
1133            size,
1134            uncompressed_size,
1135            compressed_size,
1136            header_start,
1137        })
1138    }
1139}
1140
1141impl Zip64ExtraFieldBlock {
1142    pub fn full_size(&self) -> usize {
1143        assert!(self.size > 0);
1144        self.size as usize + mem::size_of::<spec::ExtraFieldMagic>() + mem::size_of::<u16>()
1145    }
1146
1147    pub fn serialize(self) -> Box<[u8]> {
1148        let Self {
1149            magic,
1150            size,
1151            uncompressed_size,
1152            compressed_size,
1153            header_start,
1154        } = self;
1155
1156        let full_size = self.full_size();
1157
1158        let mut ret = Vec::with_capacity(full_size);
1159        ret.extend(magic.to_le_bytes());
1160        ret.extend(u16::to_le_bytes(size));
1161
1162        if let Some(uncompressed_size) = uncompressed_size {
1163            ret.extend(u64::to_le_bytes(uncompressed_size));
1164        }
1165        if let Some(compressed_size) = compressed_size {
1166            ret.extend(u64::to_le_bytes(compressed_size));
1167        }
1168        if let Some(header_start) = header_start {
1169            ret.extend(u64::to_le_bytes(header_start));
1170        }
1171        debug_assert_eq!(ret.len(), full_size);
1172
1173        ret.into_boxed_slice()
1174    }
1175}
1176
1177#[derive(Copy, Clone, Debug)]
1178#[repr(packed, C)]
1179pub(crate) struct ZipDataDescriptorBlock {
1180    magic: spec::Magic,
1181    pub crc32: u32,
1182    pub compressed_size: u32,
1183    pub uncompressed_size: u32,
1184}
1185
1186unsafe impl Pod for ZipDataDescriptorBlock {}
1187
1188impl FixedSizeBlock for ZipDataDescriptorBlock {
1189    const MAGIC: spec::Magic = spec::Magic::DATA_DESCRIPTOR_SIGNATURE;
1190
1191    #[inline(always)]
1192    fn magic(self) -> spec::Magic {
1193        self.magic
1194    }
1195
1196    const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid data descriptor header");
1197
1198    to_and_from_le![
1199        (magic, spec::Magic),
1200        (crc32, u32),
1201        (compressed_size, u32),
1202        (uncompressed_size, u32),
1203    ];
1204}
1205
1206#[derive(Copy, Clone, Debug)]
1207#[repr(packed, C)]
1208pub(crate) struct Zip64DataDescriptorBlock {
1209    magic: spec::Magic,
1210    pub crc32: u32,
1211    pub compressed_size: u64,
1212    pub uncompressed_size: u64,
1213}
1214
1215unsafe impl Pod for Zip64DataDescriptorBlock {}
1216
1217impl FixedSizeBlock for Zip64DataDescriptorBlock {
1218    const MAGIC: spec::Magic = spec::Magic::DATA_DESCRIPTOR_SIGNATURE;
1219
1220    #[inline(always)]
1221    fn magic(self) -> spec::Magic {
1222        self.magic
1223    }
1224
1225    const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid zip64 data descriptor header");
1226
1227    to_and_from_le![
1228        (magic, spec::Magic),
1229        (crc32, u32),
1230        (compressed_size, u64),
1231        (uncompressed_size, u64),
1232    ];
1233}
1234
1235/// The encryption specification used to encrypt a file with AES.
1236///
1237/// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2
1238/// does not make use of the CRC check.
1239#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1240#[repr(u16)]
1241pub enum AesVendorVersion {
1242    Ae1 = 0x0001,
1243    Ae2 = 0x0002,
1244}
1245
1246/// AES variant used.
1247#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1248#[cfg_attr(feature = "_arbitrary", derive(arbitrary::Arbitrary))]
1249#[repr(u8)]
1250pub enum AesMode {
1251    /// 128-bit AES encryption.
1252    Aes128 = 0x01,
1253    /// 192-bit AES encryption.
1254    Aes192 = 0x02,
1255    /// 256-bit AES encryption.
1256    Aes256 = 0x03,
1257}
1258
1259#[cfg(feature = "aes-crypto")]
1260impl AesMode {
1261    /// Length of the salt for the given AES mode.
1262    pub const fn salt_length(&self) -> usize {
1263        self.key_length() / 2
1264    }
1265
1266    /// Length of the key for the given AES mode.
1267    pub const fn key_length(&self) -> usize {
1268        match self {
1269            Self::Aes128 => 16,
1270            Self::Aes192 => 24,
1271            Self::Aes256 => 32,
1272        }
1273    }
1274}
1275
1276#[cfg(test)]
1277mod test {
1278    #[test]
1279    fn system() {
1280        use super::System;
1281        assert_eq!(u8::from(System::Dos), 0u8);
1282        assert_eq!(System::Dos as u8, 0u8);
1283        assert_eq!(System::Unix as u8, 3u8);
1284        assert_eq!(u8::from(System::Unix), 3u8);
1285        assert_eq!(System::from(0), System::Dos);
1286        assert_eq!(System::from(3), System::Unix);
1287        assert_eq!(u8::from(System::Unknown), 4u8);
1288        assert_eq!(System::Unknown as u8, 4u8);
1289    }
1290
1291    #[test]
1292    fn sanitize() {
1293        let file_name = "/path/../../../../etc/./passwd\0/etc/shadow".to_string();
1294        let data = ZipFileData {
1295            system: System::Dos,
1296            version_made_by: 0,
1297            flags: 0,
1298            encrypted: false,
1299            using_data_descriptor: false,
1300            is_utf8: true,
1301            compression_method: crate::compression::CompressionMethod::Stored,
1302            compression_level: None,
1303            last_modified_time: None,
1304            crc32: 0,
1305            compressed_size: 0,
1306            uncompressed_size: 0,
1307            file_name: file_name.clone().into_boxed_str(),
1308            file_name_raw: file_name.into_bytes().into_boxed_slice(),
1309            extra_field: None,
1310            central_extra_field: None,
1311            file_comment: String::with_capacity(0).into_boxed_str(),
1312            header_start: 0,
1313            extra_data_start: None,
1314            data_start: OnceLock::new(),
1315            central_header_start: 0,
1316            external_attributes: 0,
1317            large_file: false,
1318            aes_mode: None,
1319            aes_extra_data_start: 0,
1320            extra_fields: Vec::new(),
1321        };
1322        assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd"));
1323    }
1324
1325    #[test]
1326    #[allow(clippy::unusual_byte_groupings)]
1327    fn datetime_default() {
1328        use super::DateTime;
1329        let dt = DateTime::default();
1330        assert_eq!(dt.timepart(), 0);
1331        assert_eq!(dt.datepart(), 0b0000000_0001_00001);
1332    }
1333
1334    #[test]
1335    #[allow(clippy::unusual_byte_groupings)]
1336    fn datetime_max() {
1337        use super::DateTime;
1338        let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap();
1339        assert_eq!(dt.timepart(), 0b10111_111011_11101);
1340        assert_eq!(dt.datepart(), 0b1111111_1100_11111);
1341    }
1342
1343    #[test]
1344    fn datetime_equality() {
1345        use super::DateTime;
1346
1347        let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
1348        assert_eq!(
1349            dt,
1350            DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()
1351        );
1352        assert_ne!(dt, DateTime::default());
1353    }
1354
1355    #[test]
1356    fn datetime_order() {
1357        use std::cmp::Ordering;
1358
1359        use super::DateTime;
1360
1361        let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
1362        assert_eq!(
1363            dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()),
1364            Ordering::Equal
1365        );
1366        // year
1367        assert!(dt < DateTime::from_date_and_time(2019, 11, 17, 10, 38, 30).unwrap());
1368        assert!(dt > DateTime::from_date_and_time(2017, 11, 17, 10, 38, 30).unwrap());
1369        // month
1370        assert!(dt < DateTime::from_date_and_time(2018, 12, 17, 10, 38, 30).unwrap());
1371        assert!(dt > DateTime::from_date_and_time(2018, 10, 17, 10, 38, 30).unwrap());
1372        // day
1373        assert!(dt < DateTime::from_date_and_time(2018, 11, 18, 10, 38, 30).unwrap());
1374        assert!(dt > DateTime::from_date_and_time(2018, 11, 16, 10, 38, 30).unwrap());
1375        // hour
1376        assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 11, 38, 30).unwrap());
1377        assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 9, 38, 30).unwrap());
1378        // minute
1379        assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 39, 30).unwrap());
1380        assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 37, 30).unwrap());
1381        // second
1382        assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 38, 32).unwrap());
1383        assert_eq!(
1384            dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 31).unwrap()),
1385            Ordering::Equal
1386        );
1387        assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 29).unwrap());
1388        assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 28).unwrap());
1389    }
1390
1391    #[test]
1392    fn datetime_display() {
1393        use super::DateTime;
1394
1395        assert_eq!(format!("{}", DateTime::default()), "1980-01-01 00:00:00");
1396        assert_eq!(
1397            format!(
1398                "{}",
1399                DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()
1400            ),
1401            "2018-11-17 10:38:30"
1402        );
1403        assert_eq!(
1404            format!(
1405                "{}",
1406                DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap()
1407            ),
1408            "2107-12-31 23:59:58"
1409        );
1410    }
1411
1412    #[test]
1413    fn datetime_bounds() {
1414        use super::DateTime;
1415
1416        assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok());
1417        assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err());
1418        assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err());
1419        assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err());
1420
1421        assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok());
1422        assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok());
1423        assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err());
1424        assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err());
1425        assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err());
1426        assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err());
1427        assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err());
1428        assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err());
1429
1430        assert!(DateTime::from_date_and_time(2018, 1, 31, 0, 0, 0).is_ok());
1431        assert!(DateTime::from_date_and_time(2018, 2, 28, 0, 0, 0).is_ok());
1432        assert!(DateTime::from_date_and_time(2018, 2, 29, 0, 0, 0).is_err());
1433        assert!(DateTime::from_date_and_time(2018, 3, 31, 0, 0, 0).is_ok());
1434        assert!(DateTime::from_date_and_time(2018, 4, 30, 0, 0, 0).is_ok());
1435        assert!(DateTime::from_date_and_time(2018, 4, 31, 0, 0, 0).is_err());
1436        assert!(DateTime::from_date_and_time(2018, 5, 31, 0, 0, 0).is_ok());
1437        assert!(DateTime::from_date_and_time(2018, 6, 30, 0, 0, 0).is_ok());
1438        assert!(DateTime::from_date_and_time(2018, 6, 31, 0, 0, 0).is_err());
1439        assert!(DateTime::from_date_and_time(2018, 7, 31, 0, 0, 0).is_ok());
1440        assert!(DateTime::from_date_and_time(2018, 8, 31, 0, 0, 0).is_ok());
1441        assert!(DateTime::from_date_and_time(2018, 9, 30, 0, 0, 0).is_ok());
1442        assert!(DateTime::from_date_and_time(2018, 9, 31, 0, 0, 0).is_err());
1443        assert!(DateTime::from_date_and_time(2018, 10, 31, 0, 0, 0).is_ok());
1444        assert!(DateTime::from_date_and_time(2018, 11, 30, 0, 0, 0).is_ok());
1445        assert!(DateTime::from_date_and_time(2018, 11, 31, 0, 0, 0).is_err());
1446        assert!(DateTime::from_date_and_time(2018, 12, 31, 0, 0, 0).is_ok());
1447
1448        // leap year: divisible by 4
1449        assert!(DateTime::from_date_and_time(2024, 2, 29, 0, 0, 0).is_ok());
1450        // leap year: divisible by 100 and by 400
1451        assert!(DateTime::from_date_and_time(2000, 2, 29, 0, 0, 0).is_ok());
1452        // common year: divisible by 100 but not by 400
1453        assert!(DateTime::from_date_and_time(2100, 2, 29, 0, 0, 0).is_err());
1454    }
1455
1456    use std::{path::PathBuf, sync::OnceLock};
1457
1458    #[cfg(feature = "time")]
1459    use time::{format_description::well_known::Rfc3339, OffsetDateTime, PrimitiveDateTime};
1460
1461    use crate::types::{System, ZipFileData};
1462
1463    #[cfg(feature = "time")]
1464    #[test]
1465    fn datetime_try_from_offset_datetime() {
1466        use time::macros::datetime;
1467
1468        use super::DateTime;
1469
1470        // 2018-11-17 10:38:30
1471        let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30 UTC)).unwrap();
1472        assert_eq!(dt.year(), 2018);
1473        assert_eq!(dt.month(), 11);
1474        assert_eq!(dt.day(), 17);
1475        assert_eq!(dt.hour(), 10);
1476        assert_eq!(dt.minute(), 38);
1477        assert_eq!(dt.second(), 30);
1478    }
1479
1480    #[cfg(feature = "time")]
1481    #[test]
1482    fn datetime_try_from_primitive_datetime() {
1483        use time::macros::datetime;
1484
1485        use super::DateTime;
1486
1487        // 2018-11-17 10:38:30
1488        let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30)).unwrap();
1489        assert_eq!(dt.year(), 2018);
1490        assert_eq!(dt.month(), 11);
1491        assert_eq!(dt.day(), 17);
1492        assert_eq!(dt.hour(), 10);
1493        assert_eq!(dt.minute(), 38);
1494        assert_eq!(dt.second(), 30);
1495    }
1496
1497    #[cfg(feature = "time")]
1498    #[test]
1499    fn datetime_try_from_bounds() {
1500        use super::DateTime;
1501        use time::macros::datetime;
1502
1503        // 1979-12-31 23:59:59
1504        assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59)).is_err());
1505
1506        // 1980-01-01 00:00:00
1507        assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00)).is_ok());
1508
1509        // 2107-12-31 23:59:59
1510        assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59)).is_ok());
1511
1512        // 2108-01-01 00:00:00
1513        assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00)).is_err());
1514    }
1515
1516    #[cfg(feature = "time")]
1517    #[test]
1518    fn offset_datetime_try_from_datetime() {
1519        use time::macros::datetime;
1520
1521        use super::DateTime;
1522
1523        // 2018-11-17 10:38:30 UTC
1524        let dt =
1525            OffsetDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
1526        assert_eq!(dt, datetime!(2018-11-17 10:38:30 UTC));
1527    }
1528
1529    #[cfg(feature = "time")]
1530    #[test]
1531    fn primitive_datetime_try_from_datetime() {
1532        use time::macros::datetime;
1533
1534        use super::DateTime;
1535
1536        // 2018-11-17 10:38:30
1537        let dt =
1538            PrimitiveDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
1539        assert_eq!(dt, datetime!(2018-11-17 10:38:30));
1540    }
1541
1542    #[cfg(feature = "time")]
1543    #[test]
1544    fn offset_datetime_try_from_bounds() {
1545        use super::DateTime;
1546
1547        // 1980-00-00 00:00:00
1548        assert!(OffsetDateTime::try_from(unsafe {
1549            DateTime::from_msdos_unchecked(0x0000, 0x0000)
1550        })
1551        .is_err());
1552
1553        // 2107-15-31 31:63:62
1554        assert!(OffsetDateTime::try_from(unsafe {
1555            DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF)
1556        })
1557        .is_err());
1558    }
1559
1560    #[cfg(feature = "time")]
1561    #[test]
1562    fn primitive_datetime_try_from_bounds() {
1563        use super::DateTime;
1564
1565        // 1980-00-00 00:00:00
1566        assert!(PrimitiveDateTime::try_from(unsafe {
1567            DateTime::from_msdos_unchecked(0x0000, 0x0000)
1568        })
1569        .is_err());
1570
1571        // 2107-15-31 31:63:62
1572        assert!(PrimitiveDateTime::try_from(unsafe {
1573            DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF)
1574        })
1575        .is_err());
1576    }
1577
1578    #[cfg(feature = "jiff-02")]
1579    #[test]
1580    fn datetime_try_from_civil_datetime() {
1581        use jiff::civil;
1582
1583        use super::DateTime;
1584
1585        // 2018-11-17 10:38:30
1586        let dt = DateTime::try_from(civil::datetime(2018, 11, 17, 10, 38, 30, 0)).unwrap();
1587        assert_eq!(dt.year(), 2018);
1588        assert_eq!(dt.month(), 11);
1589        assert_eq!(dt.day(), 17);
1590        assert_eq!(dt.hour(), 10);
1591        assert_eq!(dt.minute(), 38);
1592        assert_eq!(dt.second(), 30);
1593    }
1594
1595    #[cfg(feature = "jiff-02")]
1596    #[test]
1597    fn datetime_try_from_civil_datetime_bounds() {
1598        use jiff::civil;
1599
1600        use super::DateTime;
1601
1602        // 1979-12-31 23:59:59
1603        assert!(DateTime::try_from(civil::datetime(1979, 12, 31, 23, 59, 59, 0)).is_err());
1604
1605        // 1980-01-01 00:00:00
1606        assert!(DateTime::try_from(civil::datetime(1980, 1, 1, 0, 0, 0, 0)).is_ok());
1607
1608        // 2107-12-31 23:59:59
1609        assert!(DateTime::try_from(civil::datetime(2107, 12, 31, 23, 59, 59, 0)).is_ok());
1610
1611        // 2108-01-01 00:00:00
1612        assert!(DateTime::try_from(civil::datetime(2108, 1, 1, 0, 0, 0, 0)).is_err());
1613    }
1614
1615    #[cfg(feature = "jiff-02")]
1616    #[test]
1617    fn civil_datetime_try_from_datetime() {
1618        use jiff::civil;
1619
1620        use super::DateTime;
1621
1622        // 2018-11-17 10:38:30 UTC
1623        let dt =
1624            civil::DateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
1625        assert_eq!(dt, civil::datetime(2018, 11, 17, 10, 38, 30, 0));
1626    }
1627
1628    #[cfg(feature = "jiff-02")]
1629    #[test]
1630    fn civil_datetime_try_from_datetime_bounds() {
1631        use jiff::civil;
1632
1633        use super::DateTime;
1634
1635        // 1980-00-00 00:00:00
1636        assert!(civil::DateTime::try_from(unsafe {
1637            DateTime::from_msdos_unchecked(0x0000, 0x0000)
1638        })
1639        .is_err());
1640
1641        // 2107-15-31 31:63:62
1642        assert!(civil::DateTime::try_from(unsafe {
1643            DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF)
1644        })
1645        .is_err());
1646    }
1647
1648    #[test]
1649    #[allow(deprecated)]
1650    fn time_conversion() {
1651        use super::DateTime;
1652        let dt = DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap();
1653        assert_eq!(dt.year(), 2018);
1654        assert_eq!(dt.month(), 11);
1655        assert_eq!(dt.day(), 17);
1656        assert_eq!(dt.hour(), 10);
1657        assert_eq!(dt.minute(), 38);
1658        assert_eq!(dt.second(), 30);
1659
1660        let dt = DateTime::try_from((0x4D71, 0x54CF)).unwrap();
1661        assert_eq!(dt.year(), 2018);
1662        assert_eq!(dt.month(), 11);
1663        assert_eq!(dt.day(), 17);
1664        assert_eq!(dt.hour(), 10);
1665        assert_eq!(dt.minute(), 38);
1666        assert_eq!(dt.second(), 30);
1667
1668        #[cfg(feature = "time")]
1669        assert_eq!(
1670            dt.to_time().unwrap().format(&Rfc3339).unwrap(),
1671            "2018-11-17T10:38:30Z"
1672        );
1673
1674        assert_eq!(<(u16, u16)>::from(dt), (0x4D71, 0x54CF));
1675    }
1676
1677    #[test]
1678    #[allow(deprecated)]
1679    fn time_out_of_bounds() {
1680        use super::DateTime;
1681        let dt = unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) };
1682        assert_eq!(dt.year(), 2107);
1683        assert_eq!(dt.month(), 15);
1684        assert_eq!(dt.day(), 31);
1685        assert_eq!(dt.hour(), 31);
1686        assert_eq!(dt.minute(), 63);
1687        assert_eq!(dt.second(), 62);
1688
1689        #[cfg(feature = "time")]
1690        assert!(dt.to_time().is_err());
1691
1692        let dt = unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) };
1693        assert_eq!(dt.year(), 1980);
1694        assert_eq!(dt.month(), 0);
1695        assert_eq!(dt.day(), 0);
1696        assert_eq!(dt.hour(), 0);
1697        assert_eq!(dt.minute(), 0);
1698        assert_eq!(dt.second(), 0);
1699
1700        #[cfg(feature = "time")]
1701        assert!(dt.to_time().is_err());
1702    }
1703
1704    #[cfg(feature = "time")]
1705    #[test]
1706    fn time_at_january() {
1707        use super::DateTime;
1708
1709        // 2020-01-01 00:00:00
1710        let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap();
1711
1712        assert!(DateTime::try_from(PrimitiveDateTime::new(clock.date(), clock.time())).is_ok());
1713    }
1714}