Repository: sorairolake/nt-time (language: rust)

Issue:
Add `zip::DateTime` integration
### Checklist

- [X] I have searched the existing issues

### Summary

Supports mutual conversion between `nt_time::FileTime` and [`zip::DateTime`](https://docs.rs/zip/1.3.0/zip/struct.DateTime.html).

### Additional context

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index f77c673..3c194b6 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -16,6 +16,10 @@ project adheres to https://semver.org/[Semantic Versioning].
 
 == {compare-url}/v0.6.10\...HEAD[Unreleased]
 
+=== Added
+
+* Add `zip::DateTime` integration ({pull-request-url}/153[#153])
+
 === Changed
 
 * Use `TimeDelta::seconds` instead of `TimeDelta::try_seconds`
diff --git a/Cargo.lock b/Cargo.lock
index 84e6d23..346d7ac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -56,12 +56,27 @@ version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
 
+[[package]]
+name = "arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
+dependencies = [
+ "derive_arbitrary",
+]
+
 [[package]]
 name = "autocfg"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
 
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
 [[package]]
 name = "chrono"
 version = "0.4.38"
@@ -117,6 +132,21 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
+[[package]]
+name = "crc32fast"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+
 [[package]]
 name = "deranged"
 version = "0.3.11"
@@ -127,12 +157,56 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "derive_arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
 [[package]]
 name = "heck"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
+[[package]]
+name = "indexmap"
+version = "2.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.11"
@@ -150,6 +224,7 @@ dependencies = [
  "serde_json",
  "serde_test",
  "time",
+ "zip",
 ]
 
 [[package]]
@@ -254,6 +329,26 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "thiserror"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "time"
 version = "0.3.36"
@@ -369,3 +464,17 @@ name = "windows_x86_64_msvc"
 version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+
+[[package]]
+name = "zip"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1f4a27345eb6f7aa7bd015ba7eb4175fa4e1b462a29874b779e0bbcf96c6ac7"
+dependencies = [
+ "arbitrary",
+ "crc32fast",
+ "crossbeam-utils",
+ "displaydoc",
+ "indexmap",
+ "thiserror",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 6ac873b..fdd1bab 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -52,6 +52,7 @@ required-features = ["default"]
 chrono = { version = "0.4.38", default-features = false, optional = true }
 serde = { version = "1.0.202", default-features = false, features = ["derive"], optional = true }
 time = { version = "0.3.36", default-features = false, features = ["macros"] }
+zip = { version = "1.3.0", default-features = false, optional = true }
 
 [dev-dependencies]
 anyhow = "1.0.83"
@@ -64,3 +65,4 @@ default = ["std"]
 std = ["time/std", "chrono?/std"]
 large-dates = ["time/large-dates"]
 serde-human-readable = ["serde", "time/serde-human-readable"]
+zip = ["dep:zip", "std"]
diff --git a/README.md b/README.md
index 6eee05b..89b920c 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,10 @@ Enables the [`serde`] crate.
 Allows Serde representations to use a human-readable format. This implicitly
 enables the `serde` feature.
 
+#### `zip`
+
+Enables the [`zip`] crate. This implicitly enables the `std` feature.
+
 ### `no_std` support
 
 This supports `no_std` mode. Disables the `default` feature to enable this.
@@ -117,6 +121,7 @@ licensing information.
 [`time`]: https://crates.io/crates/time
 [`chrono`]: https://crates.io/crates/chrono
 [`serde`]: https://serde.rs/
+[`zip`]: https://crates.io/crates/zip
 [CHANGELOG.adoc]: CHANGELOG.adoc
 [CONTRIBUTING.adoc]: CONTRIBUTING.adoc
 [AUTHORS.adoc]: AUTHORS.adoc
diff --git a/src/file_time.rs b/src/file_time.rs
index 949ddf2..a10d576 100644
--- a/src/file_time.rs
+++ b/src/file_time.rs
@@ -1497,6 +1497,50 @@ impl From<FileTime> for chrono::DateTime<chrono::Utc> {
     }
 }
 
+#[cfg(feature = "zip")]
+impl TryFrom<FileTime> for zip::DateTime {
+    type Error = DosDateTimeRangeError;
+
+    /// Converts a `FileTime` to a [`zip::DateTime`].
+    ///
+    /// # Errors
+    ///
+    /// Returns [`Err`] if `ft` is out of range for [`zip::DateTime`].
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use nt_time::{zip::DateTime, FileTime};
+    /// #
+    /// {
+    ///     let dt = DateTime::try_from(FileTime::new(119_600_064_000_000_000)).unwrap();
+    ///     assert_eq!(dt.year(), 1980);
+    ///     assert_eq!(dt.month(), 1);
+    ///     assert_eq!(dt.day(), 1);
+    ///     assert_eq!(dt.hour(), 0);
+    ///     assert_eq!(dt.minute(), 0);
+    ///     assert_eq!(dt.second(), 0);
+    /// }
+    /// {
+    ///     let dt = DateTime::try_from(FileTime::new(159_992_927_990_000_000)).unwrap();
+    ///     assert_eq!(dt.year(), 2107);
+    ///     assert_eq!(dt.month(), 12);
+    ///     assert_eq!(dt.day(), 31);
+    ///     assert_eq!(dt.hour(), 23);
+    ///     assert_eq!(dt.minute(), 59);
+    ///     assert_eq!(dt.second(), 58);
+    /// }
+    ///
+    /// assert!(DateTime::try_from(FileTime::new(119_600_063_990_000_000)).is_err());
+    /// assert!(DateTime::try_from(FileTime::new(159_992_928_000_000_000)).is_err());
+    /// ```
+    fn try_from(ft: FileTime) -> Result<Self, Self::Error> {
+        let (date, time, ..) = ft.to_dos_date_time(None)?;
+        let dt = Self::from_msdos(date, time);
+        Ok(dt)
+    }
+}
+
 impl From<u64> for FileTime {
     /// Converts the file time to a `FileTime`.
     ///
@@ -1733,6 +1777,36 @@ impl TryFrom<chrono::DateTime<chrono::Utc>> for FileTime {
     }
 }
 
+#[cfg(feature = "zip")]
+impl From<zip::DateTime> for FileTime {
+    /// Converts a [`zip::DateTime`] to a `FileTime`.
+    ///
+    /// # Panics
+    ///
+    /// Panics if `dt` is an invalid [`zip::DateTime`].
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use nt_time::{zip::DateTime, FileTime};
+    /// #
+    /// assert_eq!(
+    ///     FileTime::from(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).unwrap()),
+    ///     FileTime::new(119_600_064_000_000_000)
+    /// );
+    /// assert_eq!(
+    ///     FileTime::from(DateTime::from_date_and_time(2107, 12, 31, 23, 59, 59).unwrap()),
+    ///     FileTime::new(159_992_927_980_000_000)
+    /// );
+    /// ```
+    fn from(dt: zip::DateTime) -> Self {
+        assert!(dt.is_valid());
+        let (date, time) = (dt.datepart(), dt.timepart());
+        Self::from_dos_date_time(date, time, None, None)
+            .expect("date and time should be valid as `zip::DateTime`")
+    }
+}
+
 impl FromStr for FileTime {
     type Err = ParseFileTimeError;
 
@@ -4330,6 +4404,85 @@ mod tests {
         );
     }
 
+    #[cfg(feature = "zip")]
+    #[test]
+    fn try_from_file_time_to_zip_date_time_before_zip_date_time() {
+        use zip::DateTime;
+
+        assert_eq!(
+            DateTime::try_from(FileTime::new(119_600_063_980_000_000)).unwrap_err(),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative)
+        );
+        assert_eq!(
+            DateTime::try_from(FileTime::new(119_600_063_990_000_000)).unwrap_err(),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Negative)
+        );
+    }
+
+    #[cfg(feature = "zip")]
+    #[test]
+    fn try_from_file_time_to_zip_date_time() {
+        use zip::DateTime;
+
+        {
+            let dt = DateTime::try_from(FileTime::new(119_600_064_000_000_000)).unwrap();
+            assert_eq!(dt.year(), 1980);
+            assert_eq!(dt.month(), 1);
+            assert_eq!(dt.day(), 1);
+            assert_eq!(dt.hour(), u8::MIN);
+            assert_eq!(dt.minute(), u8::MIN);
+            assert_eq!(dt.second(), u8::MIN);
+        }
+        {
+            let dt = DateTime::try_from(FileTime::new(119_600_064_010_000_000)).unwrap();
+            assert_eq!(dt.year(), 1980);
+            assert_eq!(dt.month(), 1);
+            assert_eq!(dt.day(), 1);
+            assert_eq!(dt.hour(), u8::MIN);
+            assert_eq!(dt.minute(), u8::MIN);
+            assert_eq!(dt.second(), u8::MIN);
+        }
+        {
+            // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
+            let dt = DateTime::try_from(FileTime::new(131_869_247_100_000_000)).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(FileTime::new(159_992_927_980_000_000)).unwrap();
+            assert_eq!(dt.year(), 2107);
+            assert_eq!(dt.month(), 12);
+            assert_eq!(dt.day(), 31);
+            assert_eq!(dt.hour(), 23);
+            assert_eq!(dt.minute(), 59);
+            assert_eq!(dt.second(), 58);
+        }
+        {
+            let dt = DateTime::try_from(FileTime::new(159_992_927_990_000_000)).unwrap();
+            assert_eq!(dt.year(), 2107);
+            assert_eq!(dt.month(), 12);
+            assert_eq!(dt.day(), 31);
+            assert_eq!(dt.hour(), 23);
+            assert_eq!(dt.minute(), 59);
+            assert_eq!(dt.second(), 58);
+        }
+    }
+
+    #[cfg(feature = "zip")]
+    #[test]
+    fn try_from_file_time_to_zip_date_time_after_zip_date_time() {
+        use zip::DateTime;
+
+        assert_eq!(
+            DateTime::try_from(FileTime::new(159_992_928_000_000_000)).unwrap_err(),
+            DosDateTimeRangeError::new(DosDateTimeRangeErrorKind::Overflow)
+        );
+    }
+
     #[test]
     fn from_u64_to_file_time() {
         assert_eq!(FileTime::from(u64::MIN), FileTime::NT_TIME_EPOCH);
@@ -4592,6 +4745,54 @@ mod tests {
         );
     }
 
+    #[cfg(feature = "zip")]
+    #[test]
+    #[should_panic(expected = "assertion failed: dt.is_valid()")]
+    fn from_zip_date_time_to_file_time_with_too_small_zip_date_time() {
+        use zip::DateTime;
+
+        let dt = DateTime::from_msdos(u16::MIN, u16::MIN);
+        let _: FileTime = FileTime::from(dt);
+    }
+
+    #[cfg(feature = "zip")]
+    #[test]
+    fn from_zip_date_time_to_file_time() {
+        use zip::DateTime;
+
+        {
+            let dt = DateTime::from_date_and_time(1980, 1, 1, u8::MIN, u8::MIN, u8::MIN).unwrap();
+            assert_eq!(FileTime::from(dt), FileTime::new(119_600_064_000_000_000));
+        }
+        {
+            let dt = DateTime::from_date_and_time(1980, 1, 1, u8::MIN, u8::MIN, 1).unwrap();
+            assert_eq!(FileTime::from(dt), FileTime::new(119_600_064_000_000_000));
+        }
+        {
+            // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
+            let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap();
+            assert_eq!(FileTime::from(dt), FileTime::new(131_869_247_100_000_000));
+        }
+        {
+            let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap();
+            assert_eq!(FileTime::from(dt), FileTime::new(159_992_927_980_000_000));
+        }
+        {
+            let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 59).unwrap();
+            assert_eq!(FileTime::from(dt), FileTime::new(159_992_927_980_000_000));
+        }
+    }
+
+    #[cfg(feature = "zip")]
+    #[test]
+    #[should_panic(expected = "assertion failed: dt.is_valid()")]
+    fn from_zip_date_time_to_file_time_with_too_big_zip_date_time() {
+        use zip::DateTime;
+
+        let dt = DateTime::from_msdos(u16::MAX, u16::MAX);
+        let _: FileTime = FileTime::from(dt);
+    }
+
     #[test]
     fn from_str() {
         assert_eq!(FileTime::from_str("0").unwrap(), FileTime::NT_TIME_EPOCH);
diff --git a/src/lib.rs b/src/lib.rs
index 5f4b66b..1752f3a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -113,5 +113,7 @@ pub use chrono;
 #[cfg(feature = "serde")]
 pub use serde;
 pub use time;
+#[cfg(feature = "zip")]
+pub use zip;
 
 pub use crate::file_time::FileTime;


Test patch: