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();