//! Types that specify what is contained in a ZIP.
use crate::cfg_if_expr;
use crate::cp437::FromCp437;
use crate::result::{ZipError, ZipResult, invalid};
use crate::spec::{self, FixedSizeBlock, Magic, Pod, ZipFlags};
use crate::write::FileOptionExtension;
use crate::zipcrypto::EncryptWith;
use core::fmt::{self, Debug, Formatter};
use core::mem;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use typed_path::{Utf8WindowsComponent, Utf8WindowsPath};

pub(crate) mod ffi {
    pub const S_IFDIR: u32 = 0o0040000;
    pub const S_IFREG: u32 = 0o0100000;
    pub const S_IFLNK: u32 = 0o0120000;
}

use crate::extra_fields::{ExtraField, UsedExtraField};
use crate::read::find_data_start;
use crate::result::DateTimeRangeError;
use crate::spec::is_dir;
use crate::types::ffi::S_IFDIR;
use crate::{CompressionMethod, ZIP64_BYTES_THR};
use std::io::{Read, Seek};

pub(crate) struct ZipRawValues {
    pub(crate) crc32: u32,
    pub(crate) compressed_size: u64,
    pub(crate) uncompressed_size: u64,
}

/// System inside `version made by` (upper byte)
/// Reference: 4.4.2.2
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[allow(clippy::upper_case_acronyms)]
#[repr(u8)]
pub enum System {
    /// MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems; default on Windows)
    Dos = 0,
    /// Amiga
    Amiga = 1,
    /// OpenVMS
    OpenVMS = 2,
    /// Default on Unix; default for symlinks on all platforms
    Unix = 3,
    /// VM/CMS
    VmCms = 4,
    /// Atari ST
    AtariSt = 5,
    /// OS/2 H.P.F.S.
    Os2 = 6,
    /// Legacy Mac OS, pre OS X
    Macintosh = 7,
    /// Z-System
    ZSystemO = 8,
    /// CP/M
    CPM = 9,
    /// Windows NTFS (with extra attributes; not used by default)
    WindowsNTFS = 10,
    /// MVS (OS/390 - Z/OS)
    MVS = 11,
    /// VSE
    VSE = 12,
    /// Acorn Risc
    AcornRisc = 13,
    /// VFAT
    VFAT = 14,
    /// alternate MVS
    AlternateMVS = 15,
    /// BeOS
    BeOS = 16,
    /// Tandem
    Tandem = 17,
    /// OS/400
    Os400 = 18,
    /// OS X (Darwin) (with extra attributes; not used by default)
    OsDarwin = 19,
    /// unused
    #[default]
    Unknown = 255,
}

impl System {
    /// Parse `version_made_by` block in local entry block.
    pub fn from_version_made_by(version_made_by: u16) -> Self {
        let upper_byte = (version_made_by >> 8) as u8;
        System::from(upper_byte) // from u8
    }
}

impl From<u8> for System {
    fn from(system: u8) -> Self {
        match system {
            0 => System::Dos,
            1 => System::Amiga,
            2 => System::OpenVMS,
            3 => System::Unix,
            4 => System::VmCms,
            5 => System::AtariSt,
            6 => System::Os2,
            7 => System::Macintosh,
            8 => System::ZSystemO,
            9 => System::CPM,
            10 => System::WindowsNTFS,
            11 => System::MVS,
            12 => System::VSE,
            13 => System::AcornRisc,
            14 => System::VFAT,
            15 => System::AlternateMVS,
            16 => System::BeOS,
            17 => System::Tandem,
            18 => System::Os400,
            19 => System::OsDarwin,
            _ => System::Unknown,
        }
    }
}

impl From<System> for u8 {
    fn from(system: System) -> Self {
        system as u8
    }
}

/// Metadata for a file to be written
#[non_exhaustive]
#[derive(Clone, Debug, Copy, Eq, PartialEq)]
pub struct FileOptions<'k, T: FileOptionExtension> {
    pub(crate) compression_method: CompressionMethod,
    pub(crate) compression_level: Option<i64>,
    pub(crate) last_modified_time: DateTime,
    pub(crate) permissions: Option<u32>,
    pub(crate) large_file: bool,
    pub(crate) encrypt_with: Option<EncryptWith<'k>>,
    pub(crate) extended_options: T,
    pub(crate) alignment: u16,
    #[cfg(feature = "deflate-zopfli")]
    pub(super) zopfli_buffer_size: Option<usize>,
    #[cfg(feature = "aes-crypto")]
    pub(crate) aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
    pub(crate) system: Option<System>,
}
/// Simple File Options. Can be copied and good for simple writing zip files
pub type SimpleFileOptions = FileOptions<'static, ()>;

impl FileOptions<'static, ()> {
    const DEFAULT_FILE_PERMISSION: u32 = 0o100644;
}
/// Representation of a moment in time.
///
/// Zip files use an old format from DOS to store timestamps,
/// with its own set of peculiarities.
/// For example, it has a resolution of 2 seconds!
///
/// A [`DateTime`] can be stored directly in a zipfile with [`FileOptions::last_modified_time`],
/// or read from one with [`ZipFile::last_modified`](crate::read::ZipFile::last_modified).
///
/// # Warning
///
/// Because there is no timezone associated with the [`DateTime`], they should ideally only
/// be used for user-facing descriptions.
///
/// Modern zip files store more precise timestamps; see [`crate::extra_fields::ExtendedTimestamp`]
/// for details.
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DateTime {
    datepart: u16,
    timepart: u16,
}

impl Debug for DateTime {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        if *self == Self::default() {
            return f.write_str("DateTime::default()");
        }
        f.write_fmt(format_args!(
            "DateTime::from_date_and_time({}, {}, {}, {}, {}, {})?",
            self.year(),
            self.month(),
            self.day(),
            self.hour(),
            self.minute(),
            self.second()
        ))
    }
}

impl DateTime {
    /// Constructs a default datetime of 1980-01-01 00:00:00.
    pub const DEFAULT: Self = DateTime {
        datepart: 0b0000_0000_0010_0001,
        timepart: 0,
    };

    /// Returns the current time if possible, otherwise the default of 1980-01-01.
    #[cfg(feature = "time")]
    #[must_use]
    pub fn default_for_write() -> Self {
        let now = time::OffsetDateTime::now_utc();
        time::PrimitiveDateTime::new(now.date(), now.time())
            .try_into()
            .unwrap_or_else(|_| DateTime::default())
    }

    /// Returns the current time if possible, otherwise the default of 1980-01-01.
    #[cfg(not(feature = "time"))]
    pub fn default_for_write() -> Self {
        DateTime::default()
    }
}

#[cfg(feature = "_arbitrary")]
impl arbitrary::Arbitrary<'_> for DateTime {
    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
        // DOS time format stores seconds divided by 2 in a 5-bit field (0..=29),
        // so the maximum representable second value is 58.
        const MAX_DOS_SECONDS: u16 = 58;

        let year: u16 = u.int_in_range(1980..=2107)?;
        let month: u16 = u.int_in_range(1..=12)?;
        let day: u16 = u.int_in_range(1..=31)?;
        let datepart = day | (month << 5) | ((year - 1980) << 9);
        let hour: u16 = u.int_in_range(0..=23)?;
        let minute: u16 = u.int_in_range(0..=59)?;
        let second: u16 = u.int_in_range(0..=MAX_DOS_SECONDS)?;
        let timepart = (second >> 1) | (minute << 5) | (hour << 11);
        Ok(DateTime { datepart, timepart })
    }
}

#[cfg(feature = "chrono")]
impl TryFrom<chrono::NaiveDateTime> for DateTime {
    type Error = DateTimeRangeError;

    fn try_from(value: chrono::NaiveDateTime) -> Result<Self, Self::Error> {
        use chrono::{Datelike, Timelike};

        DateTime::from_date_and_time(
            value.year().try_into()?,
            value.month().try_into()?,
            value.day().try_into()?,
            value.hour().try_into()?,
            value.minute().try_into()?,
            value.second().try_into()?,
        )
    }
}

#[cfg(feature = "chrono")]
impl TryFrom<DateTime> for chrono::NaiveDateTime {
    type Error = DateTimeRangeError;

    fn try_from(value: DateTime) -> Result<Self, Self::Error> {
        let date = chrono::NaiveDate::from_ymd_opt(
            value.year().into(),
            value.month().into(),
            value.day().into(),
        )
        .ok_or(DateTimeRangeError)?;
        let time = chrono::NaiveTime::from_hms_opt(
            value.hour().into(),
            value.minute().into(),
            value.second().into(),
        )
        .ok_or(DateTimeRangeError)?;
        Ok(chrono::NaiveDateTime::new(date, time))
    }
}

#[cfg(feature = "jiff-02")]
impl TryFrom<jiff::civil::DateTime> for DateTime {
    type Error = DateTimeRangeError;

    fn try_from(value: jiff::civil::DateTime) -> Result<Self, Self::Error> {
        Self::from_date_and_time(
            value.year().try_into()?,
            value.month() as u8,
            value.day() as u8,
            value.hour() as u8,
            value.minute() as u8,
            value.second() as u8,
        )
    }
}

#[cfg(feature = "jiff-02")]
impl TryFrom<DateTime> for jiff::civil::DateTime {
    type Error = jiff::Error;

    fn try_from(value: DateTime) -> Result<Self, Self::Error> {
        Self::new(
            value.year() as i16,
            value.month() as i8,
            value.day() as i8,
            value.hour() as i8,
            value.minute() as i8,
            value.second() as i8,
            0,
        )
    }
}

impl TryFrom<(u16, u16)> for DateTime {
    type Error = DateTimeRangeError;

    #[inline]
    fn try_from(values: (u16, u16)) -> Result<Self, Self::Error> {
        Self::try_from_msdos(values.0, values.1)
    }
}

impl From<DateTime> for (u16, u16) {
    #[inline]
    fn from(dt: DateTime) -> Self {
        (dt.datepart(), dt.timepart())
    }
}

impl Default for DateTime {
    /// Constructs an 'default' datetime of 1980-01-01 00:00:00
    fn default() -> DateTime {
        DateTime::DEFAULT
    }
}

impl fmt::Display for DateTime {
    #[inline]
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
            self.year(),
            self.month(),
            self.day(),
            self.hour(),
            self.minute(),
            self.second()
        )
    }
}

impl DateTime {
    /// Converts an msdos (u16, u16) pair to a `DateTime` object
    ///
    /// # Safety
    /// The caller must ensure the date and time are valid.
    #[must_use]
    pub const unsafe fn from_msdos_unchecked(datepart: u16, timepart: u16) -> DateTime {
        DateTime { datepart, timepart }
    }

    /// Converts an msdos (u16, u16) pair to a `DateTime` object if it represents a valid date and
    /// time.
    pub fn try_from_msdos(datepart: u16, timepart: u16) -> Result<DateTime, DateTimeRangeError> {
        let seconds = (timepart & 0b0000000000011111) << 1;
        let minutes = (timepart & 0b0000011111100000) >> 5;
        let hours = (timepart & 0b1111100000000000) >> 11;
        let days = datepart & 0b0000000000011111;
        let months = (datepart & 0b0000000111100000) >> 5;
        let years = (datepart & 0b1111111000000000) >> 9;
        Self::from_date_and_time(
            years.checked_add(1980).ok_or(DateTimeRangeError)?,
            months.try_into()?,
            days.try_into()?,
            hours.try_into()?,
            minutes.try_into()?,
            seconds.try_into()?,
        )
    }

    /// Constructs a `DateTime` from a specific date and time
    ///
    /// The bounds are:
    /// * year: [1980, 2107]
    /// * month: [1, 12]
    /// * day: [1, 28..=31]
    /// * hour: [0, 23]
    /// * minute: [0, 59]
    /// * second: [0, 58]
    pub fn from_date_and_time(
        year: u16,
        month: u8,
        day: u8,
        hour: u8,
        minute: u8,
        second: u8,
    ) -> Result<DateTime, DateTimeRangeError> {
        fn is_leap_year(year: u16) -> bool {
            year.is_multiple_of(4) && (!year.is_multiple_of(25) || year.is_multiple_of(16))
        }

        if (1980..=2107).contains(&year)
            && (1..=12).contains(&month)
            && (1..=31).contains(&day)
            && hour <= 23
            && minute <= 59
            && second <= 60
        {
            // DOS/ZIP timestamp stores seconds/2 in 5 bits and cannot represent 59 or 60 seconds (incl. leap seconds)
            let second = second.min(58);
            let max_day = match month {
                1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
                4 | 6 | 9 | 11 => 30,
                2 if is_leap_year(year) => 29,
                2 => 28,
                _ => unreachable!(),
            };
            if day > max_day {
                return Err(DateTimeRangeError);
            }
            let datepart = u16::from(day) | (u16::from(month) << 5) | ((year - 1980) << 9);
            let timepart =
                (u16::from(second) >> 1) | (u16::from(minute) << 5) | (u16::from(hour) << 11);
            Ok(DateTime { datepart, timepart })
        } else {
            Err(DateTimeRangeError)
        }
    }

    /// Indicates whether this date and time can be written to a zip archive.
    #[must_use]
    pub fn is_valid(&self) -> bool {
        Self::try_from_msdos(self.datepart, self.timepart).is_ok()
    }

    /// Gets the time portion of this datetime in the msdos representation
    #[must_use]
    pub const fn timepart(&self) -> u16 {
        self.timepart
    }

    /// Gets the date portion of this datetime in the msdos representation
    #[must_use]
    pub const fn datepart(&self) -> u16 {
        self.datepart
    }

    /// Get the year. There is no epoch, i.e. 2018 will be returned as 2018.
    #[must_use]
    pub const fn year(&self) -> u16 {
        (self.datepart >> 9) + 1980
    }

    /// Get the month, where 1 = january and 12 = december
    ///
    /// # Warning
    ///
    /// When read from a zip file, this may not be a reasonable value
    #[must_use]
    pub const fn month(&self) -> u8 {
        ((self.datepart & 0b0000000111100000) >> 5) as u8
    }

    /// Get the day
    ///
    /// # Warning
    ///
    /// When read from a zip file, this may not be a reasonable value
    #[must_use]
    pub const fn day(&self) -> u8 {
        (self.datepart & 0b0000000000011111) as u8
    }

    /// Get the hour
    ///
    /// # Warning
    ///
    /// When read from a zip file, this may not be a reasonable value
    #[must_use]
    pub const fn hour(&self) -> u8 {
        (self.timepart >> 11) as u8
    }

    /// Get the minute
    ///
    /// # Warning
    ///
    /// When read from a zip file, this may not be a reasonable value
    #[must_use]
    pub const fn minute(&self) -> u8 {
        ((self.timepart & 0b0000011111100000) >> 5) as u8
    }

    /// Get the second
    ///
    /// # Warning
    ///
    /// When read from a zip file, this may not be a reasonable value
    #[must_use]
    pub const fn second(&self) -> u8 {
        ((self.timepart & 0b0000000000011111) << 1) as u8
    }
}

#[cfg(all(feature = "time", feature = "deprecated-time"))]
impl TryFrom<time::OffsetDateTime> for DateTime {
    type Error = DateTimeRangeError;

    fn try_from(dt: time::OffsetDateTime) -> Result<Self, Self::Error> {
        Self::try_from(time::PrimitiveDateTime::new(dt.date(), dt.time()))
    }
}

#[cfg(feature = "time")]
impl TryFrom<time::PrimitiveDateTime> for DateTime {
    type Error = DateTimeRangeError;

    fn try_from(dt: time::PrimitiveDateTime) -> Result<Self, Self::Error> {
        Self::from_date_and_time(
            dt.year().try_into()?,
            dt.month().into(),
            dt.day(),
            dt.hour(),
            dt.minute(),
            dt.second(),
        )
    }
}

#[cfg(all(feature = "time", feature = "deprecated-time"))]
impl TryFrom<DateTime> for time::OffsetDateTime {
    type Error = time::error::ComponentRange;

    fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
        time::PrimitiveDateTime::try_from(dt).map(time::PrimitiveDateTime::assume_utc)
    }
}

#[cfg(feature = "time")]
impl TryFrom<DateTime> for time::PrimitiveDateTime {
    type Error = time::error::ComponentRange;

    fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
        use time::{Date, Month, Time};
        let date =
            Date::from_calendar_date(i32::from(dt.year()), Month::try_from(dt.month())?, dt.day())?;
        let time = Time::from_hms(dt.hour(), dt.minute(), dt.second())?;
        Ok(time::PrimitiveDateTime::new(date, time))
    }
}

pub const MIN_VERSION: u8 = 10;
pub const DEFAULT_VERSION: u8 = 45;

/// Structure representing a ZIP file.
#[derive(Debug, Clone, Default)]
pub struct ZipFileData {
    /// Compatibility of the file attribute information
    pub system: System,
    /// Specification version
    pub version_made_by: u8,
    /// ZIP flags
    pub flags: u16,
    /// True if the file is encrypted.
    pub encrypted: bool,
    /// True if `file_name` and `file_comment` are UTF8
    pub is_utf8: bool,
    /// True if the file uses a data-descriptor section
    pub using_data_descriptor: bool,
    /// Compression method used to store the file
    pub compression_method: crate::compression::CompressionMethod,
    /// Compression level to store the file
    pub compression_level: Option<i64>,
    /// Last modified time. This will only have a 2 second precision.
    pub last_modified_time: Option<DateTime>,
    /// CRC32 checksum
    pub crc32: u32,
    /// Size of the file in the ZIP
    pub compressed_size: u64,
    /// Size of the file when extracted
    pub uncompressed_size: u64,
    /// Name of the file
    pub file_name: Box<str>,
    /// Raw file name. To be used when `file_name` was incorrectly decoded.
    pub file_name_raw: Box<[u8]>,
    /// Extra field usually used for storage expansion
    pub extra_field: Option<Arc<Vec<u8>>>,
    /// Extra field only written to central directory
    pub central_extra_field: Option<Arc<Vec<u8>>>,
    /// File comment
    pub file_comment: Box<str>,
    /// Specifies where the local header of the file starts
    pub header_start: u64,
    /// Specifies where the extra data of the file starts
    pub extra_data_start: Option<u64>,
    /// Specifies where the central header of the file starts
    ///
    /// Note that when this is not known, it is set to 0
    pub central_header_start: u64,
    /// Specifies where the compressed data of the file starts
    pub data_start: OnceLock<u64>,
    /// External file attributes
    pub external_attributes: u32,
    /// Reserve local ZIP64 extra field
    pub large_file: bool,
    /// AES mode if applicable
    pub aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
    /// Specifies where in the extra data the AES metadata starts
    pub aes_extra_data_start: u64,

    /// extra fields, see <https://libzip.org/specifications/extrafld.txt>
    pub extra_fields: Vec<ExtraField>,
}

impl ZipFileData {
    /// Get the starting offset of the data of the compressed file
    pub fn data_start(&self, reader: &mut (impl Read + Seek + ?Sized)) -> ZipResult<u64> {
        match self.data_start.get() {
            Some(data_start) => Ok(*data_start),
            None => Ok(find_data_start(self, reader)?),
        }
    }

    #[allow(dead_code)]
    pub fn is_dir(&self) -> bool {
        is_dir(&self.file_name)
    }

    pub fn file_name_sanitized(&self) -> PathBuf {
        let no_null_filename = match self.file_name.find('\0') {
            Some(index) => &self.file_name[0..index],
            None => &self.file_name,
        };

        Utf8WindowsPath::new(no_null_filename)
            .components()
            .filter(|component| matches!(*component, Utf8WindowsComponent::Normal(..)))
            .fold(PathBuf::new(), |mut path, cur| {
                if let Utf8WindowsComponent::Normal(s) = cur {
                    path.push(s);
                }
                path
            })
    }

    /// Simplify the file name by removing the prefix and parent directories and only return normal components
    pub(crate) fn simplified_components(&self) -> Option<Vec<&OsStr>> {
        if self.file_name.contains('\0') {
            return None;
        }
        let input = Path::new(OsStr::new(&*self.file_name));
        crate::path::simplified_components(input)
    }

    pub(crate) fn enclosed_name(&self) -> Option<PathBuf> {
        if self.file_name.contains('\0') {
            return None;
        }
        let mut depth = 0usize;
        let mut out_path = PathBuf::new();
        for component in Utf8WindowsPath::new(&self.file_name).components() {
            match component {
                Utf8WindowsComponent::Prefix(_) | Utf8WindowsComponent::RootDir => {
                    if depth > 0 {
                        return None;
                    }
                }
                Utf8WindowsComponent::ParentDir => {
                    depth = depth.checked_sub(1)?;
                    out_path.pop();
                }
                Utf8WindowsComponent::Normal(s) => {
                    depth += 1;
                    out_path.push(s);
                }
                Utf8WindowsComponent::CurDir => (),
            }
        }
        Some(out_path)
    }

    /// Get unix mode for the file
    pub(crate) const fn unix_mode(&self) -> Option<u32> {
        if self.external_attributes == 0 {
            return None;
        }
        let unix_mode = self.external_attributes >> 16;
        if unix_mode != 0 {
            // If the high 16 bits are non-zero, they probably contain Unix permissions.
            // This happens for archives created on Windows by this crate or other tools,
            // and is the only way to identify symlinks in such archives.
            return Some(unix_mode);
        }
        match self.system {
            System::Unix => Some(unix_mode),
            System::Dos => {
                // Interpret MS-DOS directory bit
                let mut mode = if 0x10 == (self.external_attributes & 0x10) {
                    ffi::S_IFDIR | 0o0775
                } else {
                    ffi::S_IFREG | 0o0664
                };
                if 0x01 == (self.external_attributes & 0x01) {
                    // Read-only bit; strip write permissions
                    mode &= !0o222;
                }
                Some(mode)
            }
            _ => None,
        }
    }

    /// PKZIP version needed to open this file (from APPNOTE 4.4.3.2).
    pub fn version_needed(&self) -> u16 {
        let compression_version: u16 = match self.compression_method {
            CompressionMethod::Stored => MIN_VERSION.into(),
            #[cfg(feature = "_deflate-any")]
            CompressionMethod::Deflated => 20,
            #[cfg(feature = "_bzip2_any")]
            CompressionMethod::Bzip2 => 46,
            #[cfg(feature = "deflate64")]
            CompressionMethod::Deflate64 => 21,
            #[cfg(feature = "lzma")]
            CompressionMethod::Lzma => 63,
            #[cfg(feature = "xz")]
            CompressionMethod::Xz => 63,
            // APPNOTE doesn't specify a version for Zstandard
            _ => u16::from(DEFAULT_VERSION),
        };
        let crypto_version: u16 = if self.aes_mode.is_some() {
            51
        } else if self.encrypted {
            20
        } else {
            10
        };
        let misc_feature_version: u16 = if self.large_file {
            45
        } else if self
            .unix_mode()
            .is_some_and(|mode| mode & S_IFDIR == S_IFDIR)
        {
            // file is directory
            20
        } else {
            10
        };
        compression_version
            .max(crypto_version)
            .max(misc_feature_version)
    }
    #[inline(always)]
    pub(crate) fn extra_field_len(&self) -> usize {
        self.extra_field
            .as_ref()
            .map(|v| v.len())
            .unwrap_or_default()
    }
    #[inline(always)]
    pub(crate) fn central_extra_field_len(&self) -> usize {
        self.central_extra_field
            .as_ref()
            .map(|v| v.len())
            .unwrap_or_default()
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) fn initialize_local_block<S, T: FileOptionExtension>(
        name: S,
        options: &FileOptions<'_, T>,
        raw_values: ZipRawValues,
        header_start: u64,
        extra_data_start: Option<u64>,
        aes_extra_data_start: u64,
        compression_method: crate::compression::CompressionMethod,
        aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>,
        extra_field: &[u8],
    ) -> Self
    where
        S: ToString,
    {
        let permissions = options
            .permissions
            .unwrap_or(FileOptions::DEFAULT_FILE_PERMISSION);
        let file_name: Box<str> = name.to_string().into_boxed_str();
        let file_name_raw: Box<[u8]> = file_name.bytes().collect();
        let mut external_attributes = permissions << 16;
        let system = if (permissions & ffi::S_IFLNK) == ffi::S_IFLNK {
            System::Unix
        } else if let Some(system_option) = options.system {
            // user provided
            system_option
        } else if cfg!(windows) {
            System::Dos
        } else {
            System::Unix
        };
        if system == System::Dos {
            if is_dir(&file_name) {
                // DOS directory bit
                external_attributes |= 0x10;
            }
            if options
                .permissions
                .is_some_and(|permissions| permissions & 0o444 == 0)
            {
                // DOS read-only bit
                external_attributes |= 0x01;
            }
        }
        let mut local_block = ZipFileData {
            system,
            version_made_by: DEFAULT_VERSION,
            flags: 0,
            encrypted: options.encrypt_with.is_some()
                || cfg_if_expr! {
                    #[cfg(feature = "aes-crypto")] => options.aes_mode.is_some(),
                    _ => false
                },
            using_data_descriptor: false,
            is_utf8: !file_name.is_ascii(),
            compression_method,
            compression_level: options.compression_level,
            last_modified_time: Some(options.last_modified_time),
            crc32: raw_values.crc32,
            compressed_size: raw_values.compressed_size,
            uncompressed_size: raw_values.uncompressed_size,
            file_name, // Never used for saving, but used as map key in insert_file_data()
            file_name_raw,
            extra_field: Some(extra_field.to_vec().into()),
            central_extra_field: options.extended_options.central_extra_data().cloned(),
            file_comment: String::with_capacity(0).into_boxed_str(),
            header_start,
            data_start: OnceLock::new(),
            central_header_start: 0,
            external_attributes,
            large_file: options.large_file,
            aes_mode,
            extra_fields: Vec::new(),
            extra_data_start,
            aes_extra_data_start,
        };
        local_block.version_made_by = local_block.version_needed() as u8;
        local_block
    }

    pub(crate) fn from_local_block<R: std::io::Read + ?Sized>(
        block: ZipLocalEntryBlock,
        reader: &mut R,
    ) -> ZipResult<Self> {
        let ZipLocalEntryBlock {
            // magic,
            version_made_by,
            flags,
            compression_method,
            last_mod_time,
            last_mod_date,
            crc32,
            compressed_size,
            uncompressed_size,
            file_name_length,
            extra_field_length,
            ..
        } = block;

        let encrypted: bool = flags & (ZipFlags::Encrypted as u16) != 0;
        if encrypted {
            return Err(ZipError::UnsupportedArchive(
                "Encrypted files are not supported",
            ));
        }

        /* FIXME: these were previously incorrect: add testing! */
        let using_data_descriptor: bool = flags & (ZipFlags::UsingDataDescriptor as u16) != 0;
        if using_data_descriptor {
            return Err(ZipError::UnsupportedArchive(
                "The file length is not available in the local header",
            ));
        }

        let is_utf8: bool = flags & (ZipFlags::LanguageEncoding as u16) != 0;
        let compression_method = crate::CompressionMethod::parse_from_u16(compression_method);
        let file_name_length: usize = file_name_length.into();
        let extra_field_length: usize = extra_field_length.into();

        let mut file_name_raw = vec![0u8; file_name_length];
        if let Err(e) = reader.read_exact(&mut file_name_raw) {
            if e.kind() == std::io::ErrorKind::UnexpectedEof {
                return Err(invalid!("File name extends beyond file boundary"));
            }
            return Err(e.into());
        }
        let mut extra_field = vec![0u8; extra_field_length];
        if let Err(e) = reader.read_exact(&mut extra_field) {
            if e.kind() == std::io::ErrorKind::UnexpectedEof {
                return Err(invalid!("Extra field extends beyond file boundary"));
            }
            return Err(e.into());
        }

        let file_name: Box<str> = if is_utf8 {
            String::from_utf8_lossy(&file_name_raw).into()
        } else {
            file_name_raw
                .from_cp437()
                .map_err(std::io::Error::other)?
                .into()
        };

        Ok(ZipFileData {
            system: System::from_version_made_by(version_made_by),
            /* NB: this strips the top 8 bits! */
            version_made_by: version_made_by as u8,
            flags,
            encrypted,
            using_data_descriptor,
            is_utf8,
            compression_method,
            compression_level: None,
            last_modified_time: DateTime::try_from_msdos(last_mod_date, last_mod_time).ok(),
            crc32,
            compressed_size: compressed_size.into(),
            uncompressed_size: uncompressed_size.into(),
            file_name,
            file_name_raw: file_name_raw.into(),
            extra_field: Some(Arc::new(extra_field)),
            central_extra_field: None,
            file_comment: String::with_capacity(0).into_boxed_str(), // file comment is only available in the central directory
            // header_start and data start are not available, but also don't matter, since seeking is
            // not available.
            header_start: 0,
            data_start: OnceLock::new(),
            central_header_start: 0,
            // The external_attributes field is only available in the central directory.
            // We set this to zero, which should be valid as the docs state 'If input came
            // from standard input, this field is set to zero.'
            external_attributes: 0,
            large_file: false,
            aes_mode: None,
            extra_fields: Vec::new(),
            extra_data_start: None,
            aes_extra_data_start: 0,
        })
    }

    fn is_utf8(&self) -> bool {
        std::str::from_utf8(&self.file_name_raw).is_ok()
    }

    fn is_ascii(&self) -> bool {
        self.file_name_raw.is_ascii() && self.file_comment.is_ascii()
    }

    fn flags(&self) -> u16 {
        let utf8_bit: u16 = if self.is_utf8() && !self.is_ascii() {
            ZipFlags::LanguageEncoding as u16
        } else {
            0
        };

        let using_data_descriptor_bit = if self.using_data_descriptor {
            ZipFlags::UsingDataDescriptor as u16
        } else {
            0
        };

        let encrypted_bit: u16 = if self.encrypted { 1u16 << 0 } else { 0 };

        utf8_bit | using_data_descriptor_bit | encrypted_bit
    }
    fn clamp_size_field(&self, field: u64) -> Result<u32, std::io::Error> {
        if self.large_file {
            Ok(spec::ZIP64_BYTES_THR as u32)
        } else {
            field.min(spec::ZIP64_BYTES_THR).try_into().map_err(|_| {
                std::io::Error::other(format!(
                    "File size {field} exceeds maximum size for non-ZIP64 files"
                ))
            })
        }
    }

    pub(crate) fn local_block(&self) -> ZipResult<ZipLocalEntryBlock> {
        let (compressed_size, uncompressed_size) = if self.using_data_descriptor {
            (0, 0)
        } else {
            (
                self.clamp_size_field(self.compressed_size)?,
                self.clamp_size_field(self.uncompressed_size)?,
            )
        };
        let extra_field_length: u16 = self
            .extra_field_len()
            .try_into()
            .map_err(|_| invalid!("Extra data field is too large"))?;

        let last_modified_time = self
            .last_modified_time
            .unwrap_or_else(DateTime::default_for_write);
        Ok(ZipLocalEntryBlock {
            magic: ZipLocalEntryBlock::MAGIC,
            version_made_by: self.version_needed(),
            flags: self.flags(),
            compression_method: self.compression_method.serialize_to_u16(),
            last_mod_time: last_modified_time.timepart(),
            last_mod_date: last_modified_time.datepart(),
            crc32: self.crc32,
            compressed_size,
            uncompressed_size,
            file_name_length: self
                .file_name_raw
                .len()
                .try_into()
                .map_err(std::io::Error::other)?,
            extra_field_length,
        })
    }

    pub(crate) fn block(&self) -> ZipResult<ZipCentralEntryBlock> {
        let extra_field_len: u16 = self
            .extra_field_len()
            .try_into()
            .map_err(std::io::Error::other)?;
        let central_extra_field_len: u16 = self
            .central_extra_field_len()
            .try_into()
            .map_err(std::io::Error::other)?;
        let last_modified_time = self
            .last_modified_time
            .unwrap_or_else(DateTime::default_for_write);
        let version_to_extract = self.version_needed();
        let version_made_by = u16::from(self.version_made_by).max(version_to_extract);
        Ok(ZipCentralEntryBlock {
            magic: ZipCentralEntryBlock::MAGIC,
            version_made_by: ((self.system as u16) << 8) | version_made_by,
            version_to_extract,
            flags: self.flags(),
            compression_method: self.compression_method.serialize_to_u16(),
            last_mod_time: last_modified_time.timepart(),
            last_mod_date: last_modified_time.datepart(),
            crc32: self.crc32,
            compressed_size: self
                .compressed_size
                .min(spec::ZIP64_BYTES_THR)
                .try_into()
                .map_err(std::io::Error::other)?,
            uncompressed_size: self
                .uncompressed_size
                .min(spec::ZIP64_BYTES_THR)
                .try_into()
                .map_err(std::io::Error::other)?,
            file_name_length: self
                .file_name_raw
                .len()
                .try_into()
                .map_err(std::io::Error::other)?,
            extra_field_length: extra_field_len.checked_add(central_extra_field_len).ok_or(
                invalid!("Extra field length in central directory exceeds 64KiB"),
            )?,
            file_comment_length: self
                .file_comment
                .len()
                .try_into()
                .map_err(std::io::Error::other)?,
            disk_number: 0,
            internal_file_attributes: 0,
            external_file_attributes: self.external_attributes,
            offset: self
                .header_start
                .min(spec::ZIP64_BYTES_THR)
                .try_into()
                .map_err(std::io::Error::other)?,
        })
    }

    pub(crate) fn zip64_extra_field_block(&self) -> Option<Zip64ExtraFieldBlock> {
        Zip64ExtraFieldBlock::maybe_new(
            self.large_file,
            self.uncompressed_size,
            self.compressed_size,
            self.header_start,
        )
    }

    pub(crate) fn write_data_descriptor<W: std::io::Write>(
        &self,
        writer: &mut W,
        auto_large_file: bool,
    ) -> Result<(), ZipError> {
        if self.large_file {
            return self.zip64_data_descriptor_block().write(writer);
        }
        if self.compressed_size > spec::ZIP64_BYTES_THR
            || self.uncompressed_size > spec::ZIP64_BYTES_THR
        {
            if auto_large_file {
                return self.zip64_data_descriptor_block().write(writer);
            }
            return Err(ZipError::Io(std::io::Error::other(
                "Large file option has not been set - use .large_file(true) in options",
            )));
        }
        self.data_descriptor_block().write(writer)
    }

    pub(crate) fn data_descriptor_block(&self) -> ZipDataDescriptorBlock {
        ZipDataDescriptorBlock {
            magic: ZipDataDescriptorBlock::MAGIC,
            crc32: self.crc32,
            compressed_size: self.compressed_size as u32,
            uncompressed_size: self.uncompressed_size as u32,
        }
    }

    pub(crate) fn zip64_data_descriptor_block(&self) -> Zip64DataDescriptorBlock {
        Zip64DataDescriptorBlock {
            magic: Zip64DataDescriptorBlock::MAGIC,
            crc32: self.crc32,
            compressed_size: self.compressed_size,
            uncompressed_size: self.uncompressed_size,
        }
    }
}

#[derive(Copy, Clone, Debug)]
#[repr(packed, C)]
pub(crate) struct ZipCentralEntryBlock {
    magic: spec::Magic,
    pub version_made_by: u16,
    pub version_to_extract: u16,
    pub flags: u16,
    pub compression_method: u16,
    pub last_mod_time: u16,
    pub last_mod_date: u16,
    pub crc32: u32,
    pub compressed_size: u32,
    pub uncompressed_size: u32,
    pub file_name_length: u16,
    pub extra_field_length: u16,
    pub file_comment_length: u16,
    pub disk_number: u16,
    pub internal_file_attributes: u16,
    pub external_file_attributes: u32,
    pub offset: u32,
}

unsafe impl Pod for ZipCentralEntryBlock {}

impl FixedSizeBlock for ZipCentralEntryBlock {
    type Magic = Magic;
    const MAGIC: Magic = Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE;

    #[inline(always)]
    fn magic(self) -> Magic {
        self.magic
    }

    const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid Central Directory header");

    to_and_from_le![
        (magic, Magic),
        (version_made_by, u16),
        (version_to_extract, u16),
        (flags, u16),
        (compression_method, u16),
        (last_mod_time, u16),
        (last_mod_date, u16),
        (crc32, u32),
        (compressed_size, u32),
        (uncompressed_size, u32),
        (file_name_length, u16),
        (extra_field_length, u16),
        (file_comment_length, u16),
        (disk_number, u16),
        (internal_file_attributes, u16),
        (external_file_attributes, u32),
        (offset, u32),
    ];
}

#[derive(Copy, Clone, Debug)]
#[repr(packed, C)]
pub(crate) struct ZipLocalEntryBlock {
    magic: spec::Magic,
    pub version_made_by: u16,
    pub flags: u16,
    pub compression_method: u16,
    pub last_mod_time: u16,
    pub last_mod_date: u16,
    pub crc32: u32,
    pub compressed_size: u32,
    pub uncompressed_size: u32,
    pub file_name_length: u16,
    pub extra_field_length: u16,
}

unsafe impl Pod for ZipLocalEntryBlock {}

impl FixedSizeBlock for ZipLocalEntryBlock {
    type Magic = Magic;
    const MAGIC: Magic = Magic::LOCAL_FILE_HEADER_SIGNATURE;

    #[inline(always)]
    fn magic(self) -> Magic {
        self.magic
    }

    const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid local file header");

    to_and_from_le![
        (magic, Magic),
        (version_made_by, u16),
        (flags, u16),
        (compression_method, u16),
        (last_mod_time, u16),
        (last_mod_date, u16),
        (crc32, u32),
        (compressed_size, u32),
        (uncompressed_size, u32),
        (file_name_length, u16),
        (extra_field_length, u16),
    ];
}

#[derive(Copy, Clone, Debug)]
pub(crate) struct Zip64ExtraFieldBlock {
    magic: spec::ExtraFieldMagic,
    size: u16,
    uncompressed_size: Option<u64>,
    compressed_size: Option<u64>,
    header_start: Option<u64>,
    // Excluded fields:
    // u32: disk start number
}

impl Zip64ExtraFieldBlock {
    pub(crate) fn maybe_new(
        large_file: bool,
        uncompressed_size: u64,
        compressed_size: u64,
        header_start: u64,
    ) -> Option<Zip64ExtraFieldBlock> {
        let mut size: u16 = 0;
        let uncompressed_size = if uncompressed_size >= ZIP64_BYTES_THR || large_file {
            size += mem::size_of::<u64>() as u16;
            Some(uncompressed_size)
        } else {
            None
        };
        let compressed_size = if compressed_size >= ZIP64_BYTES_THR || large_file {
            size += mem::size_of::<u64>() as u16;
            Some(compressed_size)
        } else {
            None
        };
        let header_start = if header_start >= ZIP64_BYTES_THR {
            size += mem::size_of::<u64>() as u16;
            Some(header_start)
        } else {
            None
        };
        if size == 0 {
            return None;
        }

        Some(Zip64ExtraFieldBlock {
            magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG,
            size,
            uncompressed_size,
            compressed_size,
            header_start,
        })
    }
}

impl Zip64ExtraFieldBlock {
    pub fn full_size(&self) -> usize {
        assert!(self.size > 0);
        self.size as usize + mem::size_of::<spec::ExtraFieldMagic>() + mem::size_of::<u16>()
    }

    pub fn serialize(self) -> Box<[u8]> {
        let Self {
            magic,
            size,
            uncompressed_size,
            compressed_size,
            header_start,
        } = self;

        let full_size = self.full_size();

        let mut ret = Vec::with_capacity(full_size);
        ret.extend(magic.to_le_bytes());
        ret.extend(u16::to_le_bytes(size));

        if let Some(uncompressed_size) = uncompressed_size {
            ret.extend(u64::to_le_bytes(uncompressed_size));
        }
        if let Some(compressed_size) = compressed_size {
            ret.extend(u64::to_le_bytes(compressed_size));
        }
        if let Some(header_start) = header_start {
            ret.extend(u64::to_le_bytes(header_start));
        }
        debug_assert_eq!(ret.len(), full_size);

        ret.into_boxed_slice()
    }
}

#[derive(Copy, Clone, Debug)]
#[repr(packed, C)]
pub(crate) struct ZipDataDescriptorBlock {
    magic: spec::Magic,
    pub crc32: u32,
    pub compressed_size: u32,
    pub uncompressed_size: u32,
}

unsafe impl Pod for ZipDataDescriptorBlock {}

impl FixedSizeBlock for ZipDataDescriptorBlock {
    type Magic = Magic;
    const MAGIC: Magic = Magic::DATA_DESCRIPTOR_SIGNATURE;

    #[inline(always)]
    fn magic(self) -> Magic {
        self.magic
    }

    const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid data descriptor header");

    to_and_from_le![
        (magic, Magic),
        (crc32, u32),
        (compressed_size, u32),
        (uncompressed_size, u32),
    ];
}

#[derive(Copy, Clone, Debug)]
#[repr(packed, C)]
pub(crate) struct Zip64DataDescriptorBlock {
    magic: spec::Magic,
    pub crc32: u32,
    pub compressed_size: u64,
    pub uncompressed_size: u64,
}

unsafe impl Pod for Zip64DataDescriptorBlock {}

impl FixedSizeBlock for Zip64DataDescriptorBlock {
    type Magic = Magic;
    const MAGIC: spec::Magic = spec::Magic::DATA_DESCRIPTOR_SIGNATURE;

    #[inline(always)]
    fn magic(self) -> spec::Magic {
        self.magic
    }

    const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid zip64 data descriptor header");

    to_and_from_le![
        (magic, spec::Magic),
        (crc32, u32),
        (compressed_size, u64),
        (uncompressed_size, u64),
    ];
}

/// The encryption specification used to encrypt a file with AES.
///
/// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2
/// does not make use of the CRC check.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u16)]
pub enum AesVendorVersion {
    Ae1 = 0x0001,
    Ae2 = 0x0002,
}

/// AES variant used.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "_arbitrary", derive(arbitrary::Arbitrary))]
#[repr(u8)]
pub enum AesMode {
    /// 128-bit AES encryption.
    Aes128 = 0x01,
    /// 192-bit AES encryption.
    Aes192 = 0x02,
    /// 256-bit AES encryption.
    Aes256 = 0x03,
}

#[cfg(feature = "aes-crypto")]
impl AesMode {
    /// Length of the salt for the given AES mode.
    #[must_use]
    pub const fn salt_length(&self) -> usize {
        self.key_length() / 2
    }

    /// Length of the key for the given AES mode.
    #[must_use]
    pub const fn key_length(&self) -> usize {
        match self {
            Self::Aes128 => 16,
            Self::Aes192 => 24,
            Self::Aes256 => 32,
        }
    }
}

#[derive(Copy, Clone)]
#[repr(packed, C)]
pub(crate) struct AesExtraField {
    header_id: u16,
    data_size: u16,
    version: u16,
    vendor_id: u16,
    aes_mode: u8,
    compression_method: u16,
}

unsafe impl Pod for AesExtraField {}

impl FixedSizeBlock for AesExtraField {
    type Magic = u16;
    const MAGIC: Self::Magic = UsedExtraField::AeXEncryption as u16;

    fn magic(self) -> Self::Magic {
        Self::MAGIC
    }

    const WRONG_MAGIC_ERROR: ZipError = invalid!("Wrong AES header ID");

    to_and_from_le![
        (header_id, u16),
        (data_size, u16),
        (version, u16),
        (vendor_id, u16),
        (aes_mode, u8),
        (compression_method, u16)
    ];
}

impl AesExtraField {
    pub(crate) fn new(
        version: AesVendorVersion,
        aes_mode: AesMode,
        compression_method: CompressionMethod,
    ) -> Self {
        Self {
            header_id: UsedExtraField::AeXEncryption as u16,
            data_size: 7,
            version: version as u16,
            vendor_id: u16::from_le_bytes(*b"AE"),
            aes_mode: aes_mode as u8,
            compression_method: compression_method.serialize_to_u16(),
        }
    }
}

#[cfg(test)]
mod test {
    #[test]
    fn system() {
        use super::System;
        assert_eq!(u8::from(System::Dos), 0u8);
        assert_eq!(System::Dos as u8, 0u8);
        assert_eq!(System::Unix as u8, 3u8);
        assert_eq!(u8::from(System::Unix), 3u8);
        assert_eq!(System::from(0), System::Dos);
        assert_eq!(System::from(3), System::Unix);
        assert_eq!(u8::from(System::Unknown), 255u8);
        assert_eq!(System::Unknown as u8, 255u8);
    }

    #[test]
    fn unix_mode_robustness() {
        use super::{System, ZipFileData};
        use crate::types::ffi::S_IFLNK;
        let mut data = ZipFileData {
            system: System::Dos,
            external_attributes: (S_IFLNK | 0o777) << 16,
            ..ZipFileData::default()
        };
        assert_eq!(data.unix_mode(), Some(S_IFLNK | 0o777));

        data.system = System::Unknown;
        assert_eq!(data.unix_mode(), Some(S_IFLNK | 0o777));

        data.external_attributes = 0x10; // DOS directory bit
        data.system = System::Dos;
        assert_eq!(
            data.unix_mode().unwrap() & 0o170000,
            crate::types::ffi::S_IFDIR
        );
    }

    #[test]
    fn sanitize() {
        let file_name = "/path/../../../../etc/./passwd\0/etc/shadow".to_string();
        let data = ZipFileData {
            system: System::Dos,
            version_made_by: 0,
            flags: 0,
            encrypted: false,
            using_data_descriptor: false,
            is_utf8: true,
            compression_method: crate::compression::CompressionMethod::Stored,
            compression_level: None,
            last_modified_time: None,
            crc32: 0,
            compressed_size: 0,
            uncompressed_size: 0,
            file_name: file_name.clone().into_boxed_str(),
            file_name_raw: file_name.into_bytes().into_boxed_slice(),
            extra_field: None,
            central_extra_field: None,
            file_comment: String::with_capacity(0).into_boxed_str(),
            header_start: 0,
            extra_data_start: None,
            data_start: OnceLock::new(),
            central_header_start: 0,
            external_attributes: 0,
            large_file: false,
            aes_mode: None,
            aes_extra_data_start: 0,
            extra_fields: Vec::new(),
        };
        assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd"));
    }

    #[test]
    #[allow(clippy::unusual_byte_groupings)]
    fn datetime_default() {
        use super::DateTime;
        let dt = DateTime::default();
        assert_eq!(dt.timepart(), 0);
        assert_eq!(dt.datepart(), 0b0000000_0001_00001);
    }

    #[test]
    #[allow(clippy::unusual_byte_groupings)]
    fn datetime_max() {
        use super::DateTime;
        let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap();
        assert_eq!(dt.timepart(), 0b10111_111011_11101);
        assert_eq!(dt.datepart(), 0b1111111_1100_11111);
    }

    #[test]
    fn datetime_equality() {
        use super::DateTime;

        let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
        assert_eq!(
            dt,
            DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()
        );
        assert_ne!(dt, DateTime::default());
    }

    #[test]
    fn datetime_order() {
        use std::cmp::Ordering;

        use super::DateTime;

        let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
        assert_eq!(
            dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()),
            Ordering::Equal
        );
        // year
        assert!(dt < DateTime::from_date_and_time(2019, 11, 17, 10, 38, 30).unwrap());
        assert!(dt > DateTime::from_date_and_time(2017, 11, 17, 10, 38, 30).unwrap());
        // month
        assert!(dt < DateTime::from_date_and_time(2018, 12, 17, 10, 38, 30).unwrap());
        assert!(dt > DateTime::from_date_and_time(2018, 10, 17, 10, 38, 30).unwrap());
        // day
        assert!(dt < DateTime::from_date_and_time(2018, 11, 18, 10, 38, 30).unwrap());
        assert!(dt > DateTime::from_date_and_time(2018, 11, 16, 10, 38, 30).unwrap());
        // hour
        assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 11, 38, 30).unwrap());
        assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 9, 38, 30).unwrap());
        // minute
        assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 39, 30).unwrap());
        assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 37, 30).unwrap());
        // second
        assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 38, 32).unwrap());
        assert_eq!(
            dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 31).unwrap()),
            Ordering::Equal
        );
        assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 29).unwrap());
        assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 28).unwrap());
    }

    #[test]
    fn datetime_display() {
        use super::DateTime;

        assert_eq!(format!("{}", DateTime::default()), "1980-01-01 00:00:00");
        assert_eq!(
            format!(
                "{}",
                DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()
            ),
            "2018-11-17 10:38:30"
        );
        assert_eq!(
            format!(
                "{}",
                DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap()
            ),
            "2107-12-31 23:59:58"
        );
    }

    #[test]
    fn datetime_bounds() {
        use super::DateTime;

        assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok());
        assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err());
        assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err());
        assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err());

        assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err());
        assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err());
        assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err());
        assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err());
        assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err());
        assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err());

        assert!(DateTime::from_date_and_time(2018, 1, 31, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(2018, 2, 28, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(2018, 2, 29, 0, 0, 0).is_err());
        assert!(DateTime::from_date_and_time(2018, 3, 31, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(2018, 4, 30, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(2018, 4, 31, 0, 0, 0).is_err());
        assert!(DateTime::from_date_and_time(2018, 5, 31, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(2018, 6, 30, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(2018, 6, 31, 0, 0, 0).is_err());
        assert!(DateTime::from_date_and_time(2018, 7, 31, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(2018, 8, 31, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(2018, 9, 30, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(2018, 9, 31, 0, 0, 0).is_err());
        assert!(DateTime::from_date_and_time(2018, 10, 31, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(2018, 11, 30, 0, 0, 0).is_ok());
        assert!(DateTime::from_date_and_time(2018, 11, 31, 0, 0, 0).is_err());
        assert!(DateTime::from_date_and_time(2018, 12, 31, 0, 0, 0).is_ok());

        // leap year: divisible by 4
        assert!(DateTime::from_date_and_time(2024, 2, 29, 0, 0, 0).is_ok());
        // leap year: divisible by 100 and by 400
        assert!(DateTime::from_date_and_time(2000, 2, 29, 0, 0, 0).is_ok());
        // common year: divisible by 100 but not by 400
        assert!(DateTime::from_date_and_time(2100, 2, 29, 0, 0, 0).is_err());
    }

    use std::{path::PathBuf, sync::OnceLock};

    use crate::types::{System, ZipFileData};

    #[cfg(all(feature = "time", feature = "deprecated-time"))]
    #[test]
    fn datetime_try_from_offset_datetime() {
        use time::macros::datetime;

        use super::DateTime;

        // 2018-11-17 10:38:30
        let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30 UTC)).unwrap();
        assert_eq!(dt.year(), 2018);
        assert_eq!(dt.month(), 11);
        assert_eq!(dt.day(), 17);
        assert_eq!(dt.hour(), 10);
        assert_eq!(dt.minute(), 38);
        assert_eq!(dt.second(), 30);
    }

    #[cfg(feature = "time")]
    #[test]
    fn datetime_try_from_primitive_datetime() {
        use time::macros::datetime;

        use super::DateTime;

        // 2018-11-17 10:38:30
        let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30)).unwrap();
        assert_eq!(dt.year(), 2018);
        assert_eq!(dt.month(), 11);
        assert_eq!(dt.day(), 17);
        assert_eq!(dt.hour(), 10);
        assert_eq!(dt.minute(), 38);
        assert_eq!(dt.second(), 30);
    }

    #[cfg(feature = "time")]
    #[test]
    fn datetime_try_from_bounds() {
        use super::DateTime;
        use time::macros::datetime;

        // 1979-12-31 23:59:59
        assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59)).is_err());

        // 1980-01-01 00:00:00
        assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00)).is_ok());

        // 2107-12-31 23:59:59
        assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59)).is_ok());

        // 2108-01-01 00:00:00
        assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00)).is_err());
    }

    #[cfg(all(feature = "time", feature = "deprecated-time"))]
    #[test]
    fn offset_datetime_try_from_datetime() {
        use time::OffsetDateTime;
        use time::macros::datetime;

        use super::DateTime;

        // 2018-11-17 10:38:30 UTC
        let dt =
            OffsetDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
        assert_eq!(dt, datetime!(2018-11-17 10:38:30 UTC));
    }

    #[cfg(feature = "time")]
    #[test]
    fn primitive_datetime_try_from_datetime() {
        use time::PrimitiveDateTime;
        use time::macros::datetime;

        use super::DateTime;

        // 2018-11-17 10:38:30
        let dt =
            PrimitiveDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
        assert_eq!(dt, datetime!(2018-11-17 10:38:30));
    }

    #[cfg(all(feature = "time", feature = "deprecated-time"))]
    #[test]
    fn offset_datetime_try_from_bounds() {
        use super::DateTime;
        use time::OffsetDateTime;

        // 1980-00-00 00:00:00
        assert!(
            OffsetDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) })
                .is_err()
        );

        // 2107-15-31 31:63:62
        assert!(
            OffsetDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) })
                .is_err()
        );
    }

    #[cfg(feature = "time")]
    #[test]
    fn primitive_datetime_try_from_bounds() {
        use super::DateTime;
        use time::PrimitiveDateTime;

        // 1980-00-00 00:00:00
        assert!(
            PrimitiveDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) })
                .is_err()
        );

        // 2107-15-31 31:63:62
        assert!(
            PrimitiveDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) })
                .is_err()
        );
    }

    #[cfg(feature = "jiff-02")]
    #[test]
    fn datetime_try_from_civil_datetime() {
        use jiff::civil;

        use super::DateTime;

        // 2018-11-17 10:38:30
        let dt = DateTime::try_from(civil::datetime(2018, 11, 17, 10, 38, 30, 0)).unwrap();
        assert_eq!(dt.year(), 2018);
        assert_eq!(dt.month(), 11);
        assert_eq!(dt.day(), 17);
        assert_eq!(dt.hour(), 10);
        assert_eq!(dt.minute(), 38);
        assert_eq!(dt.second(), 30);
    }

    #[cfg(feature = "jiff-02")]
    #[test]
    fn datetime_try_from_civil_datetime_bounds() {
        use jiff::civil;

        use super::DateTime;

        // 1979-12-31 23:59:59
        assert!(DateTime::try_from(civil::datetime(1979, 12, 31, 23, 59, 59, 0)).is_err());

        // 1980-01-01 00:00:00
        assert!(DateTime::try_from(civil::datetime(1980, 1, 1, 0, 0, 0, 0)).is_ok());

        // 2107-12-31 23:59:59
        assert!(DateTime::try_from(civil::datetime(2107, 12, 31, 23, 59, 59, 0)).is_ok());

        // 2108-01-01 00:00:00
        assert!(DateTime::try_from(civil::datetime(2108, 1, 1, 0, 0, 0, 0)).is_err());
    }

    #[cfg(feature = "jiff-02")]
    #[test]
    fn civil_datetime_try_from_datetime() {
        use jiff::civil;

        use super::DateTime;

        // 2018-11-17 10:38:30 UTC
        let dt =
            civil::DateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap();
        assert_eq!(dt, civil::datetime(2018, 11, 17, 10, 38, 30, 0));
    }

    #[cfg(feature = "jiff-02")]
    #[test]
    fn civil_datetime_try_from_datetime_bounds() {
        use jiff::civil;

        use super::DateTime;

        // 1980-00-00 00:00:00
        assert!(
            civil::DateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) })
                .is_err()
        );

        // 2107-15-31 31:63:62
        assert!(
            civil::DateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) })
                .is_err()
        );
    }

    #[test]
    fn time_conversion() {
        use super::DateTime;
        let dt = DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap();
        assert_eq!(dt.year(), 2018);
        assert_eq!(dt.month(), 11);
        assert_eq!(dt.day(), 17);
        assert_eq!(dt.hour(), 10);
        assert_eq!(dt.minute(), 38);
        assert_eq!(dt.second(), 30);

        let dt = DateTime::try_from((0x4D71, 0x54CF)).unwrap();
        assert_eq!(dt.year(), 2018);
        assert_eq!(dt.month(), 11);
        assert_eq!(dt.day(), 17);
        assert_eq!(dt.hour(), 10);
        assert_eq!(dt.minute(), 38);
        assert_eq!(dt.second(), 30);

        assert_eq!(<(u16, u16)>::from(dt), (0x4D71, 0x54CF));
    }

    #[test]
    fn time_out_of_bounds() {
        use super::DateTime;
        let dt = unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) };
        assert_eq!(dt.year(), 2107);
        assert_eq!(dt.month(), 15);
        assert_eq!(dt.day(), 31);
        assert_eq!(dt.hour(), 31);
        assert_eq!(dt.minute(), 63);
        assert_eq!(dt.second(), 62);

        let dt = unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) };
        assert_eq!(dt.year(), 1980);
        assert_eq!(dt.month(), 0);
        assert_eq!(dt.day(), 0);
        assert_eq!(dt.hour(), 0);
        assert_eq!(dt.minute(), 0);
        assert_eq!(dt.second(), 0);
    }

    #[cfg(feature = "time")]
    #[test]
    fn time_at_january() {
        use super::DateTime;
        use time::{OffsetDateTime, PrimitiveDateTime};

        // 2020-01-01 00:00:00
        let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap();

        assert!(DateTime::try_from(PrimitiveDateTime::new(clock.date(), clock.time())).is_ok());
    }
}
