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