From f94d4b7a7863b8ee7ae11c4f1ec511b0f7f65e0d Mon Sep 17 00:00:00 2001
From: Mathijs van de Nes <git@mathijs.vd-nes.nl>
Date: Tue, 13 Nov 2018 19:07:19 +0100
Subject: [PATCH] Change date api

Remove msdos_time dependency, and introduce simplified DateTime object.
This object does bounds checking to see if dates are representable in
msdos.
---
 Cargo.toml   |   5 +-
 src/lib.rs   |   3 +-
 src/read.rs  |   9 +--
 src/types.rs | 201 +++++++++++++++++++++++++++++++++++++++++++--------
 src/write.rs |  51 +++++--------
 5 files changed, 198 insertions(+), 71 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 53b3736c..888b3d1b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,9 +12,8 @@ Library to support the reading and writing of zip files.
 """
 
 [dependencies]
-time = "0.1"
+time = { version = "0.1", optional = true }
 podio = "0.1"
-msdos_time = "0.1"
 bzip2 = { version = "0.3", optional = true }
 libflate = { version = "0.1.16", optional = true }
 crc32fast = "1.0"
@@ -26,7 +25,7 @@ walkdir = "1.0"
 
 [features]
 deflate = ["libflate"]
-default = ["bzip2", "deflate"]
+default = ["bzip2", "deflate", "time"]
 
 [[bench]]
 name = "read_entry"
diff --git a/src/lib.rs b/src/lib.rs
index 8d0de859..ef2fd049 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,13 +7,14 @@ extern crate bzip2;
 extern crate crc32fast;
 #[cfg(feature = "deflate")]
 extern crate libflate;
-extern crate msdos_time;
 extern crate podio;
+#[cfg(feature = "time")]
 extern crate time;
 
 pub use read::ZipArchive;
 pub use write::ZipWriter;
 pub use compression::CompressionMethod;
+pub use types::DateTime;
 
 mod spec;
 mod crc32;
diff --git a/src/read.rs b/src/read.rs
index 8ed01b59..8c70a9ff 100644
--- a/src/read.rs
+++ b/src/read.rs
@@ -12,7 +12,6 @@ use std::borrow::Cow;
 use podio::{ReadPodExt, LittleEndian};
 use types::{ZipFileData, System, DateTime};
 use cp437::FromCp437;
-use msdos_time::MsDosDateTime;
 
 #[cfg(feature = "deflate")]
 use libflate;
@@ -351,7 +350,7 @@ fn central_header_to_zip_file<R: Read+io::Seek>(reader: &mut R, archive_offset:
         version_made_by: version_made_by as u8,
         encrypted: encrypted,
         compression_method: CompressionMethod::from_u16(compression_method),
-        last_modified_time: DateTime::MsDos(MsDosDateTime::new(last_mod_time, last_mod_date)),
+        last_modified_time: DateTime::from_msdos(last_mod_time, last_mod_date),
         crc32: crc32,
         compressed_size: compressed_size as u64,
         uncompressed_size: uncompressed_size as u64,
@@ -463,8 +462,8 @@ impl<'a> ZipFile<'a> {
         self.data.uncompressed_size
     }
     /// Get the time the file was last modified
-    pub fn last_modified(&self) -> ::time::Tm {
-        self.data.last_modified_time.to_tm()
+    pub fn last_modified(&self) -> DateTime {
+        self.data.last_modified_time
     }
     /// Get unix mode for the file
     pub fn unix_mode(&self) -> Option<u32> {
@@ -592,7 +591,7 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>(reader: &'a mut R) -> ZipResult
         version_made_by: version_made_by as u8,
         encrypted: encrypted,
         compression_method: compression_method,
-        last_modified_time: DateTime::MsDos(MsDosDateTime::new(last_mod_time, last_mod_date)),
+        last_modified_time: DateTime::from_msdos(last_mod_time, last_mod_date),
         crc32: crc32,
         compressed_size: compressed_size as u64,
         uncompressed_size: uncompressed_size as u64,
diff --git a/src/types.rs b/src/types.rs
index ba7dce5b..cd77af61 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -1,8 +1,5 @@
 //! Types that specify what is contained in a ZIP.
 
-use time;
-use msdos_time::{TmMsDosExt, MsDosDateTime};
-
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub enum System
 {
@@ -26,40 +23,148 @@ impl System {
     }
 }
 
-const TM_1980_01_01 : time::Tm = time::Tm {
-	tm_sec: 0,
-	tm_min: 0,
-	tm_hour: 0,
-	tm_mday: 1,
-	tm_mon: 0,
-	tm_year: 80,
-	tm_wday: 2,
-	tm_yday: 0,
-	tm_isdst: -1,
-	tm_utcoff: 0,
-	tm_nsec: 0
-};
-
-#[derive(Debug, Clone)]
-pub enum DateTime {
-    Tm(time::Tm),
-    MsDos(MsDosDateTime)
+/// A DateTime field to be used for storing timestamps in a zip file
+///
+/// This structure does bounds checking to ensure the date is able to be stored in a zip file.
+#[derive(Debug, Clone, Copy)]
+pub struct DateTime {
+    year: u16,
+    month: u8,
+    day: u8,
+    hour: u8,
+    minute: u8,
+    second: u8,
 }
 
 impl DateTime {
-    pub fn to_tm(&self) -> time::Tm {
-        match self {
-            &DateTime::Tm(ref tm) => *tm,
-            &DateTime::MsDos(ref ms) => time::Tm::from_msdos(*ms).unwrap_or(TM_1980_01_01)
+    /// Constructs an 'default' datetime of 1980-01-01 00:00:00
+    pub fn default() -> DateTime {
+        DateTime {
+            year: 1980,
+            month: 1,
+            day: 1,
+            hour: 0,
+            minute: 0,
+            second: 0,
         }
     }
 
-    pub fn to_msdos(&self) -> Result<MsDosDateTime, ::std::io::Error> {
-        match self {
-            &DateTime::Tm(ref tm) => tm.to_msdos(),
-            &DateTime::MsDos(ref ms) => Ok(*ms)
+    /// Converts an msdos (u16, u16) pair to a DateTime object
+    pub fn from_msdos(datepart: u16, timepart: u16) -> DateTime {
+        let seconds = (timepart & 0b0000000000011111) << 1;
+        let minutes = (timepart & 0b0000011111100000) >> 5;
+        let hours =   (timepart & 0b1111100000000000) >> 11;
+        let days =    (datepart & 0b0000000000011111) >> 0;
+        let months =  (datepart & 0b0000000111100000) >> 5;
+        let years =   (datepart & 0b1111111000000000) >> 9;
+
+        DateTime {
+            year: (years + 1980) as u16,
+            month: months as u8,
+            day: days as u8,
+            hour: hours as u8,
+            minute: minutes as u8,
+            second: seconds as u8,
         }
     }
+
+    /// Constructs a DateTime from a specific date and time
+    ///
+    /// The bounds are:
+    /// * year: [1980, 2107]
+    /// * month: [1, 12]
+    /// * day: [1, 31]
+    /// * hour: [0, 23]
+    /// * minute: [0, 59]
+    /// * second: [0, 60]
+    pub fn from_date_and_time(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Result<DateTime, ()> {
+        (DateTime { year: 0, month: 0, day: 0, hour: 0, minute: 0, second: 0 })
+            .with_date(year, month, day)?
+            .with_time(hour, minute, second)
+    }
+
+    /// Constructs a DateTime with a specific date set.
+    ///
+    /// The bounds are:
+    /// * year: [1980, 2107]
+    /// * month: [1, 12]
+    /// * day: [1, 31]
+    pub fn with_date(self, year: u16, month: u8, day: u8) -> Result<DateTime, ()> {
+        if year >= 1980 && year <= 2107
+            && month >= 1 && month <= 12
+            && day >= 1 && day <= 31
+        {
+            Ok(DateTime {
+                year: year,
+                month: month,
+                day: day,
+                hour: self.hour,
+                minute: self.minute,
+                second: self.second,
+            })
+        }
+        else {
+            Err(())
+        }
+    }
+
+    /// Constructs a DateTime with a specific time set.
+    ///
+    /// The bounds are:
+    /// * hour: [0, 23]
+    /// * minute: [0, 59]
+    /// * second: [0, 60]
+    pub fn with_time(self, hour: u8, minute: u8, second: u8) -> Result<DateTime, ()> {
+        if hour <= 23 && minute <= 59 && second <= 60 {
+            Ok(DateTime {
+                year: self.year,
+                month: self.month,
+                day: self.day,
+                hour: hour,
+                minute: minute,
+                second: second,
+            })
+        }
+        else {
+            Err(())
+        }
+    }
+
+    #[cfg(feature = "time")]
+    /// Converts a ::time::Tm object to a DateTime
+    ///
+    /// Returns `Err` when this object is out of bounds
+    pub fn from_time(tm: ::time::Tm) -> Result<DateTime, ()> {
+        if tm.tm_year >= 1980 && tm.tm_year <= 2107
+            && tm.tm_mon >= 1 && tm.tm_mon <= 31
+            && tm.tm_mday >= 1 && tm.tm_mday <= 31
+            && tm.tm_hour >= 0 && tm.tm_hour <= 23
+            && tm.tm_min >= 0 && tm.tm_min <= 59
+            && tm.tm_sec >= 0 && tm.tm_sec <= 60
+        {
+            Ok(DateTime {
+                year: (tm.tm_year + 1900) as u16,
+                month: (tm.tm_mon + 1) as u8,
+                day: tm.tm_mday as u8,
+                hour: tm.tm_hour as u8,
+                minute: tm.tm_min as u8,
+                second: tm.tm_sec as u8,
+            })
+        }
+        else {
+            Err(())
+        }
+    }
+
+    /// Gets the time portion of this datetime in the msdos representation
+    pub fn timepart(&self) -> u16 {
+        ((self.second as u16) >> 1) | ((self.minute as u16) << 5) | ((self.hour as u16) << 11)
+    }
+
+    /// Gets the date portion of this datetime in the msdos representation
+    pub fn datepart(&self) -> u16 {
+        (self.day as u16) | ((self.month as u16) << 5) | ((self.year - 1980) << 9)
+    }
 }
 
 pub const DEFAULT_VERSION: u8 = 46;
@@ -157,7 +262,7 @@ mod test {
             version_made_by: 0,
             encrypted: false,
             compression_method: ::compression::CompressionMethod::Stored,
-            last_modified_time: DateTime::Tm(time::empty_tm()),
+            last_modified_time: DateTime::default(),
             crc32: 0,
             compressed_size: 0,
             uncompressed_size: 0,
@@ -170,4 +275,40 @@ mod test {
         };
         assert_eq!(data.file_name_sanitized(), ::std::path::PathBuf::from("path/etc/passwd"));
     }
+
+    #[test]
+    fn datetime_default() {
+        use super::DateTime;
+        let dt = DateTime::default();
+        assert_eq!(dt.timepart(), 0);
+        assert_eq!(dt.datepart(), 0b0000000_0001_00001);
+    }
+
+    #[test]
+    fn datetime_max() {
+        use super::DateTime;
+        let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 60).unwrap();
+        assert_eq!(dt.timepart(), 0b10111_111011_11110);
+        assert_eq!(dt.datepart(), 0b1111111_1100_11111);
+    }
+
+    #[test]
+    fn datetime_bounds() {
+        use super::DateTime;
+        let dt = DateTime::default();
+
+        assert!(dt.with_time(23, 59, 60).is_ok());
+        assert!(dt.with_time(24, 0, 0).is_err());
+        assert!(dt.with_time(0, 60, 0).is_err());
+        assert!(dt.with_time(0, 0, 61).is_err());
+
+        assert!(dt.with_date(2107, 12, 31).is_ok());
+        assert!(dt.with_date(1980, 1, 1).is_ok());
+        assert!(dt.with_date(1979, 1, 1).is_err());
+        assert!(dt.with_date(1980, 0, 1).is_err());
+        assert!(dt.with_date(1980, 1, 0).is_err());
+        assert!(dt.with_date(2108, 12, 31).is_err());
+        assert!(dt.with_date(2107, 13, 31).is_err());
+        assert!(dt.with_date(2107, 12, 32).is_err());
+    }
 }
diff --git a/src/write.rs b/src/write.rs
index 0f9f400d..ed0d4c91 100644
--- a/src/write.rs
+++ b/src/write.rs
@@ -9,6 +9,7 @@ use std::default::Default;
 use std::io;
 use std::io::prelude::*;
 use std::mem;
+#[cfg(feature = "time")]
 use time;
 use podio::{WritePodExt, LittleEndian};
 
@@ -75,27 +76,18 @@ struct ZipWriterStats
 #[derive(Copy, Clone)]
 pub struct FileOptions {
     compression_method: CompressionMethod,
-    last_modified_time: time::Tm,
+    last_modified_time: DateTime,
     permissions: Option<u32>,
 }
 
 impl FileOptions {
-    #[cfg(feature = "deflate")]
     /// Construct a new FileOptions object
     pub fn default() -> FileOptions {
         FileOptions {
-            compression_method: CompressionMethod::Deflated,
-            last_modified_time: time::now(),
-            permissions: None,
-        }
-    }
-
-    #[cfg(not(feature = "deflate"))]
-    /// Construct a new FileOptions object
-    pub fn default() -> FileOptions {
-        FileOptions {
-            compression_method: CompressionMethod::Stored,
-            last_modified_time: time::now(),
+            #[cfg(feature = "deflate")]      compression_method: CompressionMethod::Deflated,
+            #[cfg(not(feature = "deflate"))] compression_method: CompressionMethod::Stored,
+            #[cfg(feature = "time")]      last_modified_time: DateTime::from_time(time::now()).unwrap_or(DateTime::default()),
+            #[cfg(not(feature = "time"))] last_modified_time: DateTime::default(),
             permissions: None,
         }
     }
@@ -112,8 +104,9 @@ impl FileOptions {
 
     /// Set the last modified time
     ///
-    /// The default is the current timestamp
-    pub fn last_modified_time(mut self, mod_time: time::Tm) -> FileOptions {
+    /// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01
+    /// otherwise
+    pub fn last_modified_time(mut self, mod_time: DateTime) -> FileOptions {
         self.last_modified_time = mod_time;
         self
     }
@@ -201,7 +194,7 @@ impl<W: Write+io::Seek> ZipWriter<W>
                 version_made_by: DEFAULT_VERSION,
                 encrypted: false,
                 compression_method: options.compression_method,
-                last_modified_time: DateTime::Tm(options.last_modified_time),
+                last_modified_time: options.last_modified_time,
                 crc32: 0,
                 compressed_size: 0,
                 uncompressed_size: 0,
@@ -437,9 +430,8 @@ fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipR
     // Compression method
     writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
     // last mod file time and last mod file date
-    let msdos_datetime = file.last_modified_time.to_msdos()?;
-    writer.write_u16::<LittleEndian>(msdos_datetime.timepart)?;
-    writer.write_u16::<LittleEndian>(msdos_datetime.datepart)?;
+    writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
+    writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
     // crc-32
     writer.write_u32::<LittleEndian>(file.crc32)?;
     // compressed size
@@ -484,9 +476,8 @@ fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData)
     // compression method
     writer.write_u16::<LittleEndian>(file.compression_method.to_u16())?;
     // last mod file time + date
-    let msdos_datetime = file.last_modified_time.to_msdos()?;
-    writer.write_u16::<LittleEndian>(msdos_datetime.timepart)?;
-    writer.write_u16::<LittleEndian>(msdos_datetime.datepart)?;
+    writer.write_u16::<LittleEndian>(file.last_modified_time.timepart())?;
+    writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
     // crc-32
     writer.write_u32::<LittleEndian>(file.crc32)?;
     // compressed size
@@ -529,7 +520,7 @@ fn build_extra_field(_file: &ZipFileData) -> ZipResult<Vec<u8>>
 mod test {
     use std::io;
     use std::io::Write;
-    use time;
+    use types::DateTime;
     use super::{FileOptions, ZipWriter};
     use compression::CompressionMethod;
 
@@ -544,10 +535,9 @@ mod test {
     #[test]
     fn write_zip_dir() {
         let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
-        writer.add_directory("test", FileOptions::default().last_modified_time(time::Tm {
-            tm_year: 2018 - 1900, tm_mon: 8 - 1, tm_mday: 15, tm_hour: 20, tm_min: 45, tm_sec: 6,
-            tm_isdst: -1, tm_yday: 0, tm_wday: 0, tm_utcoff: 0, tm_nsec: 0,
-        })).unwrap();
+        writer.add_directory("test", FileOptions::default().last_modified_time(
+            DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap()
+        )).unwrap();
         let result = writer.finish().unwrap();
         assert_eq!(result.get_ref().len(), 114);
         assert_eq!(*result.get_ref(), &[
@@ -561,12 +551,9 @@ mod test {
     #[test]
     fn write_mimetype_zip() {
         let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
-        let mut mtime = time::empty_tm();
-        mtime.tm_year = 80;
-        mtime.tm_mday = 1;
         let options = FileOptions {
             compression_method: CompressionMethod::Stored,
-            last_modified_time: mtime,
+            last_modified_time: DateTime::default(),
             permissions: Some(33188),
         };
         writer.start_file("mimetype", options).unwrap();