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