zip/
types.rs

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