From fb8a9526e9316dc6806c2b117ec67cd60b962625 Mon Sep 17 00:00:00 2001 From: Chris Connelly Date: Fri, 29 Oct 2021 15:44:21 +0100 Subject: [PATCH 1/4] Switch from `chrono` to `time` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is unsoundness in `chrono` (https://rustsec.org/advisories/RUSTSEC-2020-0159), and although we don't use the affected APIs, the advisory still fires. `time` ≥3 does not have this unsoundness, and supports almost‡ all the necessary APIs, so we can quite simply swap it out. Since the `chrono` types are used in some APIs, those signatures have had to change, making this a breaking change. Since it's already a breaking change, the feature is also renamed from `chrono` to `time`. ‡ `time` does not support leap seconds in their representation or parsing. In this commit, we simply ignore leap seconds, which also constitutes a breaking change since values with leap seconds will parse differently (they parse to the previous second). --- .github/workflows/test.yml | 4 +- Cargo.toml | 6 +- src/deserializer/mod.rs | 6 +- src/models/mod.rs | 4 +- src/models/time.rs | 197 ++++++++++++++++--------------------- src/reader/mod.rs | 18 ++-- src/serializer/mod.rs | 6 +- src/writer/mod.rs | 24 ++--- 8 files changed, 120 insertions(+), 145 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 421ae63d..e1e8472f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,8 +29,8 @@ jobs: run: | cargo build --verbose cargo test --verbose --no-default-features - cargo test --verbose --no-default-features --features="num-bigint bit-vec chrono std" - cargo doc --features="num-bigint bit-vec chrono" + cargo test --verbose --no-default-features --features="num-bigint bit-vec time std" + cargo doc --features="num-bigint bit-vec time" continue-on-error: ${{ matrix.rust == 'nightly' }} - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 diff --git a/Cargo.toml b/Cargo.toml index 15b83b27..87d66bfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ default = [] std = [] [package.metadata.docs.rs] -features = ["num-bigint", "bit-vec", "chrono", "std"] +features = ["num-bigint", "bit-vec", "time", "std"] [dependencies] @@ -39,8 +39,8 @@ default-features = false features = ["std"] optional = true -[dependencies.chrono] -version = "0.4" +[dependencies.time] +version = "0.3" optional = true default-features = false features = ["std"] diff --git a/src/deserializer/mod.rs b/src/deserializer/mod.rs index 4a26599d..abdcbbb8 100644 --- a/src/deserializer/mod.rs +++ b/src/deserializer/mod.rs @@ -18,7 +18,7 @@ use bit_vec::BitVec; use super::{ASN1Result,BERMode,BERReader,parse_ber_general}; use super::models::{ObjectIdentifier,TaggedDerValue}; -#[cfg(feature = "chrono")] +#[cfg(feature = "time")] use super::models::{UTCTime,GeneralizedTime}; /// Types decodable in BER. @@ -233,14 +233,14 @@ impl BERDecodable for ObjectIdentifier { } } -#[cfg(feature = "chrono")] +#[cfg(feature = "time")] impl BERDecodable for UTCTime { fn decode_ber(reader: BERReader) -> ASN1Result { reader.read_utctime() } } -#[cfg(feature = "chrono")] +#[cfg(feature = "time")] impl BERDecodable for GeneralizedTime { fn decode_ber(reader: BERReader) -> ASN1Result { reader.read_generalized_time() diff --git a/src/models/mod.rs b/src/models/mod.rs index 9970bc59..a31e41e3 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -11,11 +11,11 @@ #![forbid(missing_docs)] mod oid; -#[cfg(feature = "chrono")] +#[cfg(feature = "time")] mod time; mod der; pub use self::oid::{ObjectIdentifier, ParseOidError}; -#[cfg(feature = "chrono")] +#[cfg(feature = "time")] pub use self::time::{UTCTime,GeneralizedTime}; pub use self::der::TaggedDerValue; diff --git a/src/models/time.rs b/src/models/time.rs index 03c656d9..77ea9fd8 100644 --- a/src/models/time.rs +++ b/src/models/time.rs @@ -8,9 +8,8 @@ use alloc::string::String; use alloc::vec::Vec; -use chrono::{DateTime,FixedOffset,NaiveDate,NaiveTime,NaiveDateTime}; -use chrono::offset::Utc; -use chrono::{TimeZone,Datelike,Timelike,LocalResult}; +use core::convert::TryFrom; +use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}; /// Date and time between 1950-01-01T00:00:00Z and 2049-12-31T23:59:59Z. /// It cannot express fractional seconds and leap seconds. @@ -21,11 +20,11 @@ use chrono::{TimeZone,Datelike,Timelike,LocalResult}; /// /// # Features /// -/// This struct is enabled by `chrono` feature. +/// This struct is enabled by `time` feature. /// /// ```toml /// [dependencies] -/// yasna = { version = "*", features = ["chrono"] } +/// yasna = { version = "*", features = ["time"] } /// ``` /// /// # Examples @@ -33,10 +32,9 @@ use chrono::{TimeZone,Datelike,Timelike,LocalResult}; /// ``` /// # fn main() { /// use yasna::models::UTCTime; -/// use chrono::{Datelike,Timelike}; /// let datetime = *UTCTime::parse(b"8201021200Z").unwrap().datetime(); /// assert_eq!(datetime.year(), 1982); -/// assert_eq!(datetime.month(), 1); +/// assert_eq!(datetime.month() as u8, 1); /// assert_eq!(datetime.day(), 2); /// assert_eq!(datetime.hour(), 12); /// assert_eq!(datetime.minute(), 0); @@ -46,7 +44,7 @@ use chrono::{TimeZone,Datelike,Timelike,LocalResult}; /// ``` #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct UTCTime { - datetime: DateTime, + datetime: OffsetDateTime, } impl UTCTime { @@ -85,7 +83,8 @@ impl UTCTime { return None; } if !buf[..i].iter().all(|&b| b'0' <= b && b <= b'9') || - !buf[i+1..].iter().all(|&b| b'0' <= b && b <= b'9') { + !buf[i+1..].iter().all(|&b| b'0' <= b && b <= b'9') + { return None; } let year_short: i32 = @@ -95,44 +94,37 @@ impl UTCTime { } else { year_short + 1900 }; - let month: u32 = - ((buf[2] - b'0') as u32) * 10 + ((buf[3] - b'0') as u32); - let day: u32 = - ((buf[4] - b'0') as u32) * 10 + ((buf[5] - b'0') as u32); - let hour: u32 = - ((buf[6] - b'0') as u32) * 10 + ((buf[7] - b'0') as u32); - let minute: u32 = - ((buf[8] - b'0') as u32) * 10 + ((buf[9] - b'0') as u32); - let second : u32 = if i == 12 { - ((buf[10] - b'0') as u32) * 10 + ((buf[11] - b'0') as u32) + let month = Month::try_from((buf[2] - b'0') * 10 + (buf[3] - b'0')).ok()?; + let day = (buf[4] - b'0') * 10 + (buf[5] - b'0'); + let hour = (buf[6] - b'0') * 10 + (buf[7] - b'0'); + let minute = (buf[8] - b'0') * 10 + (buf[9] - b'0'); + let second = if i == 12 { + (buf[10] - b'0') * 10 + (buf[11] - b'0') } else { 0 }; - let offset_hour: i32 = if buf[i] == b'Z' { + let offset_hour: i8 = if buf[i] == b'Z' { 0 } else { - ((buf[i+1] - b'0') as i32) * 10 + ((buf[i+2] - b'0') as i32) + ((buf[i+1] - b'0') as i8) * 10 + ((buf[i+2] - b'0') as i8) }; - let offset_minute: i32 = if buf[i] == b'Z' { + let offset_minute: i8 = if buf[i] == b'Z' { 0 } else { - ((buf[i+3] - b'0') as i32) * 10 + ((buf[i+4] - b'0') as i32) + ((buf[i+3] - b'0') as i8) * 10 + ((buf[i+4] - b'0') as i8) }; - let date = if let Some(date) = NaiveDate::from_ymd_opt( - year, month, day) { date } else { return None; }; - let time = if let Some(time) = NaiveTime::from_hms_opt( - hour, minute, second) { time } else { return None; }; - let datetime = NaiveDateTime::new(date, time); + let date = Date::from_calendar_date(year, month, day).ok()?; + let time = Time::from_hms(hour, minute, second).ok()?; + let datetime = PrimitiveDateTime::new(date, time); if !(offset_hour < 24 && offset_minute < 60) { return None; } let offset = if buf[i] == b'+' { - FixedOffset::east((offset_hour * 60 + offset_minute) * 60) + UtcOffset::from_hms(offset_hour, offset_minute, 0).ok()? } else { - FixedOffset::west((offset_hour * 60 + offset_minute) * 60) + UtcOffset::from_hms(-offset_hour, -offset_minute, 0).ok()? }; - let datetime = offset.from_local_datetime(&datetime).unwrap(); - let datetime = datetime.with_timezone(&Utc); + let datetime = datetime.assume_offset(offset).to_offset(UtcOffset::UTC); // While the given local datatime is in [1950, 2050) by definition, // the UTC datetime can be out of bounds. We check this. if !(1950 <= datetime.year() && datetime.year() < 2050) { @@ -152,8 +144,8 @@ impl UTCTime { /// - The year is not between 1950 and 2049. /// - It is in a leap second. /// - It has a non-zero nanosecond value. - pub fn from_datetime(datetime: &DateTime) -> Self { - let datetime = datetime.with_timezone(&Utc); + pub fn from_datetime(datetime: OffsetDateTime) -> Self { + let datetime = datetime.to_offset(UtcOffset::UTC); assert!(1950 <= datetime.year() && datetime.year() < 2050, "Can't express a year {:?} in UTCTime", datetime.year()); assert!(datetime.nanosecond() < 1_000_000_000, @@ -174,9 +166,8 @@ impl UTCTime { /// - The year is not between 1950 and 2049. /// - It is in a leap second. /// - It has a non-zero nanosecond value. - pub fn from_datetime_opt - (datetime: &DateTime) -> Option { - let datetime = datetime.with_timezone(&Utc); + pub fn from_datetime_opt(datetime: OffsetDateTime) -> Option { + let datetime = datetime.to_offset(UtcOffset::UTC); if !(1950 <= datetime.year() && datetime.year() < 2050) { return None; } @@ -189,7 +180,7 @@ impl UTCTime { } /// Returns the datetime it represents. - pub fn datetime(&self) -> &DateTime { + pub fn datetime(&self) -> &OffsetDateTime { &self.datetime } @@ -198,8 +189,8 @@ impl UTCTime { let mut buf = Vec::with_capacity(13); buf.push((self.datetime.year() / 10 % 10) as u8 + b'0'); buf.push((self.datetime.year() % 10) as u8 + b'0'); - buf.push((self.datetime.month() / 10 % 10) as u8 + b'0'); - buf.push((self.datetime.month() % 10) as u8 + b'0'); + buf.push((self.datetime.month() as u8 / 10 % 10) + b'0'); + buf.push((self.datetime.month() as u8 % 10) + b'0'); buf.push((self.datetime.day() / 10 % 10) as u8 + b'0'); buf.push((self.datetime.day() % 10) as u8 + b'0'); buf.push((self.datetime.hour() / 10 % 10) as u8 + b'0'); @@ -232,11 +223,11 @@ impl UTCTime { /// /// # Features /// -/// This struct is enabled by `chrono` feature. +/// This struct is enabled by `time` feature. /// /// ```toml /// [dependencies] -/// yasna = { version = "*", features = ["chrono"] } +/// yasna = { version = "*", features = ["time"] } /// ``` /// /// # Examples @@ -244,11 +235,10 @@ impl UTCTime { /// ``` /// # fn main() { /// use yasna::models::GeneralizedTime; -/// use chrono::{Datelike,Timelike}; /// let datetime = /// *GeneralizedTime::parse(b"19851106210627.3Z").unwrap().datetime(); /// assert_eq!(datetime.year(), 1985); -/// assert_eq!(datetime.month(), 11); +/// assert_eq!(datetime.month() as u8, 11); /// assert_eq!(datetime.day(), 6); /// assert_eq!(datetime.hour(), 21); /// assert_eq!(datetime.minute(), 6); @@ -258,16 +248,15 @@ impl UTCTime { /// ``` #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct GeneralizedTime { - datetime: DateTime, + datetime: OffsetDateTime, sub_nano: Vec, } impl GeneralizedTime { - /// Almost same as `parse`. It takes `default_tz` however. - /// GeneralizedTime value can omit timezone in local time. - /// In that case, `default_tz` is used instead. - fn parse_general(buf: &[u8], default_tz: Option<&Tz>) - -> Option { + /// Almost same as `parse`. It takes `default_offset` however. + /// GeneralizedTime value can omit offset in local time. + /// In that case, `default_offset` is used instead. + fn parse_general(buf: &[u8], default_offset: Option) -> Option { if buf.len() < 10 { return None; } @@ -277,31 +266,26 @@ impl GeneralizedTime { let year: i32 = ((buf[0] - b'0') as i32) * 1000 + ((buf[1] - b'0') as i32) * 100 + ((buf[2] - b'0') as i32) * 10 + ((buf[3] - b'0') as i32); - let month: u32 = - ((buf[4] - b'0') as u32) * 10 + ((buf[5] - b'0') as u32); - let day: u32 = - ((buf[6] - b'0') as u32) * 10 + ((buf[7] - b'0') as u32); - let hour: u32 = - ((buf[8] - b'0') as u32) * 10 + ((buf[9] - b'0') as u32); + let month = Month::try_from((buf[4] - b'0') * 10 + (buf[5] - b'0')).ok()?; + let day = (buf[6] - b'0') * 10 + (buf[7] - b'0'); + let hour = (buf[8] - b'0') * 10 + (buf[9] - b'0'); // i: current position on `buf` let mut i = 10; // The factor to scale the fraction part to nanoseconds. let mut fraction_scale : i64 = 1_000_000_000; - let mut minute : u32; + let mut minute: u8; if i+2 <= buf.len() && buf[i..i+2].iter().all(|&b| b'0' <= b && b <= b'9') { - minute = - ((buf[i] - b'0') as u32) * 10 + ((buf[i+1] - b'0') as u32); + minute = (buf[i] - b'0') * 10 + (buf[i + 1] - b'0'); i += 2; } else { fraction_scale = 3_600_000_000_000; minute = 0; } - let mut second : u32; + let mut second: u8; if i+2 <= buf.len() && buf[i..i+2].iter().all(|&b| b'0' <= b && b <= b'9') { - second = - ((buf[i] - b'0') as u32) * 10 + ((buf[i+1] - b'0') as u32); + second = (buf[i] - b'0') * 10 + (buf[i + 1] - b'0'); i += 2; } else { if fraction_scale == 1_000_000_000 { @@ -327,8 +311,8 @@ impl GeneralizedTime { sub_nano[k] = b'0' + ((sum % 10) as u8); } nanosecond = (carry % 1_000_000_000) as u32; - second += (carry / 1_000_000_000 % 60) as u32; - minute += (carry / 60_000_000_000) as u32; + second += (carry / 1_000_000_000 % 60) as u8; + minute += (carry / 60_000_000_000) as u8; while let Some(&digit) = sub_nano.last() { if digit == b'0' { sub_nano.pop(); @@ -338,32 +322,22 @@ impl GeneralizedTime { } i += j; } - // Cope with leap seconds. + // `time` doesn't accept leap seconds, so we ignore them if second == 60 { second = 59; - nanosecond += 1_000_000_000; } - let date = if let Some(date) = NaiveDate::from_ymd_opt( - year, month, day) { date } else { return None; }; - let time = if let Some(time) = NaiveTime::from_hms_nano_opt( - hour, minute, second, nanosecond) { time } else { return None; }; - let naive_datetime = NaiveDateTime::new(date, time); - let datetime : DateTime; + let date = Date::from_calendar_date(year, month, day).ok()?; + let time = Time::from_hms_nano(hour, minute, second, nanosecond).ok()?; + let naive_datetime = PrimitiveDateTime::new(date, time); + let datetime: OffsetDateTime; if i == buf.len() { // Local datetime with no timezone information. - if let Some(default_tz) = default_tz { - if let LocalResult::Single(dt) = - default_tz.from_local_datetime(&naive_datetime) { - datetime = dt.with_timezone(&Utc); - } else { - return None; - } - } else { - return None; - } + datetime = naive_datetime + .assume_offset(default_offset?) + .to_offset(UtcOffset::UTC); } else if i < buf.len() && buf[i] == b'Z' { // UTC time. - datetime = DateTime::from_utc(naive_datetime, Utc); + datetime = naive_datetime.assume_utc(); i += 1; } else if i < buf.len() && (buf[i] == b'+' || buf[i] == b'-') { // Local datetime with offset information. @@ -374,13 +348,13 @@ impl GeneralizedTime { return None; } let offset_hour = - ((buf[i] - b'0') as i32) * 10 + ((buf[i+1] - b'0') as i32); + ((buf[i] - b'0') as i8) * 10 + ((buf[i+1] - b'0') as i8); i += 2; let offset_minute; if i+2 <= buf.len() && buf[i..i+2].iter().all(|&b| b'0' <= b && b <= b'9') { offset_minute = - ((buf[i] - b'0') as i32) * 10 + ((buf[i+1] - b'0') as i32); + ((buf[i] - b'0') as i8) * 10 + ((buf[i+1] - b'0') as i8); i += 2; } else { offset_minute = 0; @@ -389,13 +363,13 @@ impl GeneralizedTime { return None; } let offset = if offset_sign == b'+' { - FixedOffset::east((offset_hour * 60 + offset_minute) * 60) + UtcOffset::from_hms(offset_hour, offset_minute, 0) } else { - FixedOffset::west((offset_hour * 60 + offset_minute) * 60) + UtcOffset::from_hms(-offset_hour, -offset_minute, 0) }; - datetime = - offset.from_local_datetime(&naive_datetime).unwrap() - .with_timezone(&Utc); + datetime = naive_datetime + .assume_offset(offset.ok()?) + .to_offset(UtcOffset::UTC); } else { return None; } @@ -428,7 +402,7 @@ impl GeneralizedTime { /// It returns `None` if the given string does not specify a correct /// datetime. pub fn parse(buf: &[u8]) -> Option { - Self::parse_general::(buf, None) + Self::parse_general(buf, None) } /// Parses ASN.1 string representation of GeneralizedTime, with the @@ -446,9 +420,8 @@ impl GeneralizedTime { /// /// It returns `None` if the given string does not specify a correct /// datetime. - pub fn parse_with_timezone - (buf: &[u8], default_tz: &Tz) -> Option { - Self::parse_general(buf, Some(default_tz)) + pub fn parse_with_offset(buf: &[u8], default_offset: UtcOffset) -> Option { + Self::parse_general(buf, Some(default_offset)) } /// Constructs `GeneralizedTime` from a datetime. @@ -458,8 +431,8 @@ impl GeneralizedTime { /// Panics when GeneralizedTime can't represent the datetime. That is: /// /// - The year is not between 0 and 9999. - pub fn from_datetime(datetime: &DateTime) -> Self { - let datetime = datetime.with_timezone(&Utc); + pub fn from_datetime(datetime: OffsetDateTime) -> Self { + let datetime = datetime.to_offset(UtcOffset::UTC); assert!(0 <= datetime.year() && datetime.year() < 10000, "Can't express a year {:?} in GeneralizedTime", datetime.year()); return GeneralizedTime { @@ -476,9 +449,8 @@ impl GeneralizedTime { /// That is: /// /// - The year is not between 0 and 9999. - pub fn from_datetime_opt(datetime: &DateTime) - -> Option { - let datetime = datetime.with_timezone(&Utc); + pub fn from_datetime_opt(datetime: OffsetDateTime) -> Option { + let datetime = datetime.to_offset(UtcOffset::UTC); if !(0 <= datetime.year() && datetime.year() < 10000) { return None; } @@ -498,9 +470,8 @@ impl GeneralizedTime { /// - The year is not between 0 and 9999. /// /// It also panics if `sub_nano` contains a non-digit character. - pub fn from_datetime_and_sub_nano - (datetime: &DateTime, sub_nano: &[u8]) -> Self { - let datetime = datetime.with_timezone(&Utc); + pub fn from_datetime_and_sub_nano(datetime: OffsetDateTime, sub_nano: &[u8]) -> Self { + let datetime = datetime.to_offset(UtcOffset::UTC); assert!(0 <= datetime.year() && datetime.year() < 10000, "Can't express a year {:?} in GeneralizedTime", datetime.year()); assert!(sub_nano.iter().all(|&b| b'0' <= b && b <= b'9'), @@ -526,9 +497,11 @@ impl GeneralizedTime { /// - The year is not between 0 and 9999. /// /// It also returns `None` if `sub_nano` contains a non-digit character. - pub fn from_datetime_and_sub_nano_opt - (datetime: &DateTime, sub_nano: &[u8]) -> Option { - let datetime = datetime.with_timezone(&Utc); + pub fn from_datetime_and_sub_nano_opt( + datetime: OffsetDateTime, + sub_nano: &[u8], + ) -> Option { + let datetime = datetime.to_offset(UtcOffset::UTC); if !(0 <= datetime.year() && datetime.year() < 10000) { return None; } @@ -546,7 +519,7 @@ impl GeneralizedTime { } /// Returns the datetime it represents, discarding sub-nanoseconds digits. - pub fn datetime(&self) -> &DateTime { + pub fn datetime(&self) -> &OffsetDateTime { &self.datetime } @@ -562,8 +535,8 @@ impl GeneralizedTime { buf.push((self.datetime.year() / 100 % 10) as u8 + b'0'); buf.push((self.datetime.year() / 10 % 10) as u8 + b'0'); buf.push((self.datetime.year() % 10) as u8 + b'0'); - buf.push((self.datetime.month() / 10 % 10) as u8 + b'0'); - buf.push((self.datetime.month() % 10) as u8 + b'0'); + buf.push((self.datetime.month() as u8 / 10 % 10) + b'0'); + buf.push((self.datetime.month() as u8 % 10) + b'0'); buf.push((self.datetime.day() / 10 % 10) as u8 + b'0'); buf.push((self.datetime.day() % 10) as u8 + b'0'); buf.push((self.datetime.hour() / 10 % 10) as u8 + b'0'); @@ -610,7 +583,7 @@ impl GeneralizedTime { fn test_utctime_parse() { let datetime = *UTCTime::parse(b"8201021200Z").unwrap().datetime(); assert_eq!(datetime.year(), 1982); - assert_eq!(datetime.month(), 1); + assert_eq!(datetime.month() as u8, 1); assert_eq!(datetime.day(), 2); assert_eq!(datetime.hour(), 12); assert_eq!(datetime.minute(), 0); @@ -619,7 +592,7 @@ fn test_utctime_parse() { let datetime = *UTCTime::parse(b"0101021200Z").unwrap().datetime(); assert_eq!(datetime.year(), 2001); - assert_eq!(datetime.month(), 1); + assert_eq!(datetime.month() as u8, 1); assert_eq!(datetime.day(), 2); assert_eq!(datetime.hour(), 12); assert_eq!(datetime.minute(), 0); @@ -647,7 +620,7 @@ fn test_generalized_time_parse() { let datetime = *GeneralizedTime::parse(b"19851106210627.3Z").unwrap().datetime(); assert_eq!(datetime.year(), 1985); - assert_eq!(datetime.month(), 11); + assert_eq!(datetime.month() as u8, 11); assert_eq!(datetime.day(), 6); assert_eq!(datetime.hour(), 21); assert_eq!(datetime.minute(), 6); @@ -671,7 +644,7 @@ fn test_generalized_time_parse() { let datetime = GeneralizedTime::parse(b"19990101085960.1234+0900").unwrap(); - assert_eq!(&datetime.to_string(), "19981231235960.1234Z"); + assert_eq!(&datetime.to_string(), "19981231235959.1234Z"); let datetime = GeneralizedTime::parse( diff --git a/src/reader/mod.rs b/src/reader/mod.rs index 4fa08c94..073958ae 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -25,7 +25,7 @@ use super::tags::{TAG_EOC,TAG_BOOLEAN,TAG_INTEGER,TAG_OCTETSTRING}; use super::tags::{TAG_NULL,TAG_OID,TAG_UTF8STRING,TAG_SEQUENCE,TAG_SET,TAG_ENUM}; use super::tags::{TAG_NUMERICSTRING,TAG_PRINTABLESTRING,TAG_VISIBLESTRING,TAG_IA5STRING,TAG_BMPSTRING}; use super::models::{ObjectIdentifier,TaggedDerValue}; -#[cfg(feature = "chrono")] +#[cfg(feature = "time")] use super::models::{UTCTime,GeneralizedTime}; pub use self::error::*; @@ -1374,7 +1374,7 @@ impl<'a, 'b> BERReader<'a, 'b> { }) } - #[cfg(feature = "chrono")] + #[cfg(feature = "time")] /// Reads an ASN.1 UTCTime. /// /// # Examples @@ -1387,16 +1387,16 @@ impl<'a, 'b> BERReader<'a, 'b> { /// let asn = yasna::parse_ber(data, |reader| { /// reader.read_utctime() /// }).unwrap(); - /// assert_eq!(asn.datetime().timestamp(), 378820800); + /// assert_eq!(asn.datetime().unix_timestamp(), 378820800); /// ``` /// /// # Features /// - /// This method is enabled by `chrono` feature. + /// This method is enabled by `time` feature. /// /// ```toml /// [dependencies] - /// yasna = { version = "*", features = ["chrono"] } + /// yasna = { version = "*", features = ["time"] } /// ``` pub fn read_utctime(self) -> ASN1Result { use super::tags::TAG_UTCTIME; @@ -1412,7 +1412,7 @@ impl<'a, 'b> BERReader<'a, 'b> { }) } - #[cfg(feature = "chrono")] + #[cfg(feature = "time")] /// Reads an ASN.1 GeneralizedTime. /// /// # Examples @@ -1425,16 +1425,16 @@ impl<'a, 'b> BERReader<'a, 'b> { /// let asn = yasna::parse_ber(data, |reader| { /// reader.read_generalized_time() /// }).unwrap(); - /// assert_eq!(asn.datetime().timestamp(), 500159309); + /// assert_eq!(asn.datetime().unix_timestamp(), 500159309); /// ``` /// /// # Features /// - /// This method is enabled by `chrono` feature. + /// This method is enabled by `time` feature. /// /// ```toml /// [dependencies] - /// yasna = { version = "*", features = ["chrono"] } + /// yasna = { version = "*", features = ["time"] } /// ``` pub fn read_generalized_time(self) -> ASN1Result { use super::tags::TAG_GENERALIZEDTIME; diff --git a/src/serializer/mod.rs b/src/serializer/mod.rs index edf416e6..b58ea78e 100644 --- a/src/serializer/mod.rs +++ b/src/serializer/mod.rs @@ -18,7 +18,7 @@ use bit_vec::BitVec; use super::{DERWriter,construct_der}; use super::models::ObjectIdentifier; -#[cfg(feature = "chrono")] +#[cfg(feature = "time")] use super::models::{UTCTime,GeneralizedTime}; /// Types encodable in DER. @@ -204,14 +204,14 @@ impl DEREncodable for ObjectIdentifier { } } -#[cfg(feature = "chrono")] +#[cfg(feature = "time")] impl DEREncodable for UTCTime { fn encode_der(&self, writer: DERWriter) { writer.write_utctime(self) } } -#[cfg(feature = "chrono")] +#[cfg(feature = "time")] impl DEREncodable for GeneralizedTime{ fn encode_der(&self, writer: DERWriter) { writer.write_generalized_time(self) diff --git a/src/writer/mod.rs b/src/writer/mod.rs index 344d99d9..4ad6ea22 100644 --- a/src/writer/mod.rs +++ b/src/writer/mod.rs @@ -21,7 +21,7 @@ use super::tags::{TAG_BOOLEAN,TAG_INTEGER,TAG_OCTETSTRING}; use super::tags::{TAG_NULL,TAG_OID,TAG_UTF8STRING,TAG_SEQUENCE,TAG_SET,TAG_ENUM,TAG_IA5STRING,TAG_BMPSTRING}; use super::tags::{TAG_NUMERICSTRING,TAG_PRINTABLESTRING,TAG_VISIBLESTRING}; use super::models::{ObjectIdentifier,TaggedDerValue}; -#[cfg(feature = "chrono")] +#[cfg(feature = "time")] use super::models::{UTCTime,GeneralizedTime}; /// Constructs DER-encoded data as `Vec`. @@ -956,7 +956,7 @@ impl<'a> DERWriter<'a> { }); } - #[cfg(feature = "chrono")] + #[cfg(feature = "time")] /// Writes an ASN.1 UTCTime. /// /// # Examples @@ -965,10 +965,11 @@ impl<'a> DERWriter<'a> { /// # fn main() { /// use yasna; /// use yasna::models::UTCTime; - /// use chrono::{Utc,TimeZone}; + /// use time::OffsetDateTime; /// let der = yasna::construct_der(|writer| { /// writer.write_utctime( - /// &UTCTime::from_datetime(&Utc.timestamp(378820800, 0))) + /// &UTCTime::from_datetime( + /// OffsetDateTime::from_unix_timestamp(378820800).unwrap())) /// }); /// assert_eq!(&der, &[ /// 23, 13, 56, 50, 48, 49, 48, 50, 49, 50, 48, 48, 48, 48, 90]); @@ -977,11 +978,11 @@ impl<'a> DERWriter<'a> { /// /// # Features /// - /// This method is enabled by `chrono` feature. + /// This method is enabled by `time` feature. /// /// ```toml /// [dependencies] - /// yasna = { version = "*", features = ["chrono"] } + /// yasna = { version = "*", features = ["time"] } /// ``` pub fn write_utctime(self, datetime: &UTCTime) { use super::tags::TAG_UTCTIME; @@ -990,7 +991,7 @@ impl<'a> DERWriter<'a> { }); } - #[cfg(feature = "chrono")] + #[cfg(feature = "time")] /// Writes an ASN.1 GeneralizedTime. /// /// # Examples @@ -999,11 +1000,12 @@ impl<'a> DERWriter<'a> { /// # fn main() { /// use yasna; /// use yasna::models::GeneralizedTime; - /// use chrono::{Utc,TimeZone}; + /// use time::OffsetDateTime; /// let der = yasna::construct_der(|writer| { /// writer.write_generalized_time( /// &GeneralizedTime::from_datetime( - /// &Utc.timestamp(500159309, 724_000_000))) + /// OffsetDateTime::from_unix_timestamp_nanos( + /// 500_159_309_724_000_000).unwrap())) /// }); /// assert_eq!(&der, &[ /// 24, 19, 49, 57, 56, 53, 49, 49, 48, 54, 50, @@ -1013,11 +1015,11 @@ impl<'a> DERWriter<'a> { /// /// # Features /// - /// This method is enabled by `chrono` feature. + /// This method is enabled by `time` feature. /// /// ```toml /// [dependencies] - /// yasna = { version = "*", features = ["chrono"] } + /// yasna = { version = "*", features = ["time"] } /// ``` pub fn write_generalized_time(self, datetime: &GeneralizedTime) { use super::tags::TAG_GENERALIZEDTIME; From 9beb94b2b0b58f9340afe7404679ff443d174792 Mon Sep 17 00:00:00 2001 From: Chris Connelly Date: Fri, 29 Oct 2021 23:11:39 +0100 Subject: [PATCH 2/4] Exclude optional features from MSRV Some optional dependencies, for example `time`, have a more strict MSRV than this crate. Rather than having to upgrade our MSRV or pin outdated versions of dependencies, we can instead explicitly exclude optional features from the MSRV. The test CI workflow has been updated to not run tests with features on MSRV. --- .github/workflows/test.yml | 21 +++++++++++++++------ README.md | 1 + 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1e8472f..ffcef2e4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,11 @@ name: test on: [push, pull_request] +env: + # Minimum supported Rust version. + # Please also change README.md if you change this. + MSRV: 1.36.0 + jobs: test: runs-on: ubuntu-latest @@ -12,9 +17,7 @@ jobs: - stable - beta - nightly - # Minimum supported Rust version. - # Please also change README.md if you change this. - - 1.36.0 + - msrv env: RUSTFLAGS: -D warnings @@ -23,14 +26,20 @@ jobs: - name: Install toolchain uses: actions-rs/toolchain@v1 with: - toolchain: ${{ matrix.rust }} + toolchain: ${{ matrix.rust == 'msrv' && env.MSRV || matrix.rust }} override: true - name: Test run: | cargo build --verbose cargo test --verbose --no-default-features - cargo test --verbose --no-default-features --features="num-bigint bit-vec time std" - cargo doc --features="num-bigint bit-vec time" + + # don't test features on MSRV + if [[ '${{ matrix.rust }}' != 'msrv' ]]; then + cargo test --verbose --no-default-features --features="num-bigint bit-vec time std" + cargo doc --features="num-bigint bit-vec time" + else + cargo doc + fi continue-on-error: ${{ matrix.rust == 'nightly' }} - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 diff --git a/README.md b/README.md index 334998bb..cb5b5411 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ This library is currently specialized for on-memory serialization/deserializatio ## Compatibility The minimum supported Rust version (MSRV) of `yasna.rs` is Rust 1.36.0. +Optional feature flags that enable interoperability with third-party crates (e.g. `time`) follow the policy of that crate if stricter. ## License From 9532b3475e7779ae5a58c475a5e6a269fed1c2a7 Mon Sep 17 00:00:00 2001 From: Chris Connelly Date: Sat, 30 Oct 2021 13:15:25 +0100 Subject: [PATCH 3/4] Update datetime references in docs to `OffsetDateTime` This is a better match for the `time` terminology. --- src/models/time.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/models/time.rs b/src/models/time.rs index 77ea9fd8..52a9be20 100644 --- a/src/models/time.rs +++ b/src/models/time.rs @@ -135,7 +135,7 @@ impl UTCTime { }); } - /// Constructs `UTCTime` from a datetime. + /// Constructs `UTCTime` from an `OffsetDateTime`. /// /// # Panics /// @@ -157,7 +157,7 @@ impl UTCTime { }; } - /// Constructs `UTCTime` from a datetime. + /// Constructs `UTCTime` from an `OffsetDateTime`. /// /// # Errors /// @@ -179,7 +179,7 @@ impl UTCTime { }); } - /// Returns the datetime it represents. + /// Returns the `OffsetDateTime` it represents. pub fn datetime(&self) -> &OffsetDateTime { &self.datetime } @@ -424,7 +424,7 @@ impl GeneralizedTime { Self::parse_general(buf, Some(default_offset)) } - /// Constructs `GeneralizedTime` from a datetime. + /// Constructs `GeneralizedTime` from an `OffsetDateTime`. /// /// # Panics /// @@ -441,7 +441,7 @@ impl GeneralizedTime { }; } - /// Constructs `GeneralizedTime` from a datetime. + /// Constructs `GeneralizedTime` from an `OffsetDateTime`. /// /// # Errors /// @@ -460,7 +460,7 @@ impl GeneralizedTime { }); } - /// Constructs `GeneralizedTime` from a datetime and sub-nanoseconds + /// Constructs `GeneralizedTime` from an `OffsetDateTime` and sub-nanoseconds /// digits. /// /// # Panics @@ -486,7 +486,7 @@ impl GeneralizedTime { }; } - /// Constructs `GeneralizedTime` from a datetime and sub-nanoseconds + /// Constructs `GeneralizedTime` from an `OffsetDateTime` and sub-nanoseconds /// digits. /// /// # Errors @@ -518,7 +518,7 @@ impl GeneralizedTime { }); } - /// Returns the datetime it represents, discarding sub-nanoseconds digits. + /// Returns the `OffsetDateTime` it represents, discarding sub-nanoseconds digits. pub fn datetime(&self) -> &OffsetDateTime { &self.datetime } From cf112275ca2ee79ce67b82df1bddc662920046f1 Mon Sep 17 00:00:00 2001 From: Chris Connelly Date: Sat, 30 Oct 2021 13:25:32 +0100 Subject: [PATCH 4/4] Restore support for round tripping leap seconds This is a bit of hack until `time` has built-in support for leap seconds. When we detect a leap second in `GeneralizedTime`, we set the `is_leap_second` flag to true. When serializing to bytes, we check the flag and bump the seconds to 60 if it's set. A debug assert ensures we'll notice if the flag is set incorrectly. --- src/models/time.rs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/models/time.rs b/src/models/time.rs index 52a9be20..f20f0f97 100644 --- a/src/models/time.rs +++ b/src/models/time.rs @@ -250,6 +250,8 @@ impl UTCTime { pub struct GeneralizedTime { datetime: OffsetDateTime, sub_nano: Vec, + // TODO: time does not support leap seconds. This is a simple hack to support round-tripping. + is_leap_second: bool, } impl GeneralizedTime { @@ -322,8 +324,10 @@ impl GeneralizedTime { } i += j; } - // `time` doesn't accept leap seconds, so we ignore them + let mut is_leap_second = false; if second == 60 { + // TODO: `time` doesn't accept leap seconds, so we use a flag to preserve + is_leap_second = true; second = 59; } let date = Date::from_calendar_date(year, month, day).ok()?; @@ -384,6 +388,7 @@ impl GeneralizedTime { return Some(GeneralizedTime { datetime: datetime, sub_nano: sub_nano, + is_leap_second, }); } @@ -438,6 +443,7 @@ impl GeneralizedTime { return GeneralizedTime { datetime: datetime, sub_nano: Vec::new(), + is_leap_second: false, }; } @@ -457,6 +463,7 @@ impl GeneralizedTime { return Some(GeneralizedTime { datetime: datetime, sub_nano: Vec::new(), + is_leap_second: false, }); } @@ -483,6 +490,7 @@ impl GeneralizedTime { return GeneralizedTime { datetime: datetime, sub_nano: sub_nano, + is_leap_second: false, }; } @@ -515,10 +523,13 @@ impl GeneralizedTime { return Some(GeneralizedTime { datetime: datetime, sub_nano: sub_nano, + is_leap_second: false, }); } - /// Returns the `OffsetDateTime` it represents, discarding sub-nanoseconds digits. + /// Returns the `OffsetDateTime` it represents. + /// + /// Leap seconds and sub-nanoseconds digits will be discarded. pub fn datetime(&self) -> &OffsetDateTime { &self.datetime } @@ -543,14 +554,14 @@ impl GeneralizedTime { buf.push((self.datetime.hour() % 10) as u8 + b'0'); buf.push((self.datetime.minute() / 10 % 10) as u8 + b'0'); buf.push((self.datetime.minute() % 10) as u8 + b'0'); - let second = self.datetime.second(); + let mut second = self.datetime.second(); let nanosecond = self.datetime.nanosecond(); // Cope with leap seconds. - let (second, nanosecond) = if nanosecond < 1_000_000_000 { - (second, nanosecond) - } else { - (second + 1, nanosecond - 1_000_000_000) - }; + if self.is_leap_second { + debug_assert!(second == 59, + "is_leap_second is set, but second = {}", second); + second += 1; + } buf.push((second / 10 % 10) as u8 + b'0'); buf.push((second % 10) as u8 + b'0'); buf.push(b'.'); @@ -644,7 +655,7 @@ fn test_generalized_time_parse() { let datetime = GeneralizedTime::parse(b"19990101085960.1234+0900").unwrap(); - assert_eq!(&datetime.to_string(), "19981231235959.1234Z"); + assert_eq!(&datetime.to_string(), "19981231235960.1234Z"); let datetime = GeneralizedTime::parse(