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