Skip to content

Commit

Permalink
der: expose DateTime through public API (#75)
Browse files Browse the repository at this point in the history
Debatably we should switch to something like `chrono`, but since we have
a rich built-in date-and-time type we might as well expose it.
  • Loading branch information
tarcieri committed Oct 7, 2021
1 parent 448d5fb commit 0ee952c
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 80 deletions.
10 changes: 4 additions & 6 deletions der/src/asn1/generalized_time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ impl DecodeValue<'_> for GeneralizedTime {

DateTime::new(year, month, day, hour, minute, second)
.and_then(|dt| dt.unix_duration())
.ok_or_else(|| Self::TAG.value_error())
.map_err(|_| Self::TAG.value_error())
.and_then(Self::new)
}
_ => Err(Self::TAG.value_error()),
Expand All @@ -95,9 +95,7 @@ impl Encodable for GeneralizedTime {
fn encode(&self, encoder: &mut Encoder<'_>) -> Result<()> {
Header::new(Self::TAG, Self::LENGTH)?.encode(encoder)?;

let datetime =
DateTime::from_unix_duration(self.0).ok_or_else(|| Self::TAG.value_error())?;

let datetime = DateTime::from_unix_duration(self.0).map_err(|_| Self::TAG.value_error())?;
let year_hi = datetime.year() / 100;
let year_lo = datetime.year() % 100;

Expand All @@ -106,8 +104,8 @@ impl Encodable for GeneralizedTime {
datetime::encode_decimal(encoder, Self::TAG, datetime.month())?;
datetime::encode_decimal(encoder, Self::TAG, datetime.day())?;
datetime::encode_decimal(encoder, Self::TAG, datetime.hour())?;
datetime::encode_decimal(encoder, Self::TAG, datetime.minute())?;
datetime::encode_decimal(encoder, Self::TAG, datetime.second())?;
datetime::encode_decimal(encoder, Self::TAG, datetime.minutes())?;
datetime::encode_decimal(encoder, Self::TAG, datetime.seconds())?;
encoder.byte(b'Z')
}
}
Expand Down
10 changes: 4 additions & 6 deletions der/src/asn1/utc_time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl DecodeValue<'_> for UtcTime {

DateTime::new(year, month, day, hour, minute, second)
.and_then(|dt| dt.unix_duration())
.ok_or_else(|| Self::TAG.value_error())
.map_err(|_| Self::TAG.value_error())
.and_then(Self::new)
}
_ => Err(Self::TAG.value_error()),
Expand All @@ -104,9 +104,7 @@ impl Encodable for UtcTime {
fn encode(&self, encoder: &mut Encoder<'_>) -> Result<()> {
Header::new(Self::TAG, Self::LENGTH)?.encode(encoder)?;

let datetime =
DateTime::from_unix_duration(self.0).ok_or_else(|| Self::TAG.value_error())?;

let datetime = DateTime::from_unix_duration(self.0).map_err(|_| Self::TAG.value_error())?;
debug_assert!((1950..2050).contains(&datetime.year()));

let year = match datetime.year() {
Expand All @@ -119,8 +117,8 @@ impl Encodable for UtcTime {
datetime::encode_decimal(encoder, Self::TAG, datetime.month())?;
datetime::encode_decimal(encoder, Self::TAG, datetime.day())?;
datetime::encode_decimal(encoder, Self::TAG, datetime.hour())?;
datetime::encode_decimal(encoder, Self::TAG, datetime.minute())?;
datetime::encode_decimal(encoder, Self::TAG, datetime.second())?;
datetime::encode_decimal(encoder, Self::TAG, datetime.minutes())?;
datetime::encode_decimal(encoder, Self::TAG, datetime.seconds())?;
encoder.byte(b'Z')
}
}
Expand Down
136 changes: 68 additions & 68 deletions der/src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Copyright (c) 2016 The humantime Developers
// Released under the MIT OR Apache 2.0 licenses

use crate::{Encoder, Result, Tag};
use crate::{Encoder, ErrorKind, Result, Tag};
use core::time::Duration;

/// Minimum year allowed in [`DateTime`] values.
Expand All @@ -15,35 +15,14 @@ const MIN_YEAR: u16 = 1970;
/// [`DateTime`] (non-inclusive).
const MAX_UNIX_DURATION: Duration = Duration::from_secs(253_402_300_800);

/// Decode 2-digit decimal value
pub(crate) fn decode_decimal(tag: Tag, hi: u8, lo: u8) -> Result<u16> {
if (b'0'..=b'9').contains(&hi) && (b'0'..=b'9').contains(&lo) {
Ok((hi - b'0') as u16 * 10 + (lo - b'0') as u16)
} else {
Err(tag.value_error())
}
}

/// Encode 2-digit decimal value
pub(crate) fn encode_decimal(encoder: &mut Encoder<'_>, tag: Tag, value: u16) -> Result<()> {
let hi_val = value / 10;

if hi_val >= 10 {
return Err(tag.value_error());
}

encoder.byte(hi_val as u8 + b'0')?;
encoder.byte((value % 10) as u8 + b'0')
}

/// Inner date/time type shared by multiple ASN.1 types
/// Date-and-time type shared by multiple ASN.1 types
/// (e.g. `GeneralizedTime`, `UTCTime`).
///
/// Following conventions from RFC 5280, this type is always Z-normalized
/// (i.e. represents a UTC time). However, it isn't named "UTC time" in order
/// to prevent confusion with ASN.1 `UTCTime`.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) struct DateTime {
pub struct DateTime {
/// Full year (e.g. 2000).
///
/// Must be >=1970 to permit positive conversions to Unix time.
Expand All @@ -58,54 +37,54 @@ pub(crate) struct DateTime {
/// Hour (0-23)
hour: u16,

/// Minute (0-59)
minute: u16,
/// Minutes (0-59)
minutes: u16,

/// Second (0-59)
second: u16,
/// Seconds (0-59)
seconds: u16,
}

impl DateTime {
/// Create a new [`DateTime`] from the given UTC time components.
///
/// Note that this does not fully validate the components of the date.
/// To ensure the date is valid, it must be converted to a Unix timestamp
/// by calling [`DateTime::unix_timestamp`].
pub(crate) fn new(
pub fn new(
year: u16,
month: u16,
day: u16,
hour: u16,
minute: u16,
second: u16,
) -> Option<Self> {
) -> Result<Self> {
// Basic validation of the components.
if year >= MIN_YEAR
&& (1..=12).contains(&month)
&& (1..=31).contains(&day)
&& (0..=23).contains(&hour)
&& (0..=59).contains(&minute)
&& (0..=59).contains(&second)
if year < MIN_YEAR
|| !(1..=12).contains(&month)
|| !(1..=31).contains(&day)
|| !(0..=23).contains(&hour)
|| !(0..=59).contains(&minute)
|| !(0..=59).contains(&second)
{
Some(Self {
year,
month,
day,
hour,
minute,
second,
})
} else {
None
return Err(ErrorKind::DateTime.into());
}

let result = Self {
year,
month,
day,
hour,
minutes: minute,
seconds: second,
};

// Validate time maps to a Unix duration
result.unix_duration()?;
Ok(result)
}

/// Compute a [`DateTime`] from the given [`Duration`] since the `UNIX_EPOCH`.
///
/// Returns `None` if the value is outside the supported date range.
pub fn from_unix_duration(unix_duration: Duration) -> Option<Self> {
pub fn from_unix_duration(unix_duration: Duration) -> Result<Self> {
if unix_duration > MAX_UNIX_DURATION {
return None;
return Err(ErrorKind::DateTime.into());
}

let secs_since_epoch = unix_duration.as_secs();
Expand Down Expand Up @@ -179,38 +158,38 @@ impl DateTime {
)
}

/// Get the year
/// Get the year.
pub fn year(&self) -> u16 {
self.year
}

/// Get the month
/// Get the month.
pub fn month(&self) -> u16 {
self.month
}

/// Get the day
/// Get the day.
pub fn day(&self) -> u16 {
self.day
}

/// Get the hour
/// Get the hour.
pub fn hour(&self) -> u16 {
self.hour
}

/// Get the minute
pub fn minute(&self) -> u16 {
self.minute
/// Get the minutes.
pub fn minutes(&self) -> u16 {
self.minutes
}

/// Get the second
pub fn second(&self) -> u16 {
self.second
/// Get the seconds.
pub fn seconds(&self) -> u16 {
self.seconds
}

/// Compute [`Duration`] since `UNIX_EPOCH` from the given calendar date.
pub(crate) fn unix_duration(&self) -> Option<Duration> {
pub fn unix_duration(&self) -> Result<Duration> {
let leap_years = ((self.year - 1) - 1968) / 4 - ((self.year - 1) - 1900) / 100
+ ((self.year - 1) - 1600) / 400;

Expand All @@ -230,11 +209,11 @@ impl DateTime {
10 => (273, 31),
11 => (304, 30),
12 => (334, 31),
_ => return None,
_ => return Err(ErrorKind::DateTime.into()),
};

if self.day > mdays || self.day == 0 {
return None;
return Err(ErrorKind::DateTime.into());
}

ydays += self.day - 1;
Expand All @@ -244,8 +223,8 @@ impl DateTime {
}

let days = (self.year - 1970) as u64 * 365 + leap_years as u64 + ydays as u64;
let time = self.second as u64 + (self.minute as u64 * 60) + (self.hour as u64 * 3600);
Some(Duration::from_secs(time + days * 86400))
let time = self.seconds as u64 + (self.minutes as u64 * 60) + (self.hour as u64 * 3600);
Ok(Duration::from_secs(time + days * 86400))
}

/// Is the year a leap year?
Expand All @@ -254,6 +233,27 @@ impl DateTime {
}
}

/// Decode 2-digit decimal value
pub(crate) fn decode_decimal(tag: Tag, hi: u8, lo: u8) -> Result<u16> {
if (b'0'..=b'9').contains(&hi) && (b'0'..=b'9').contains(&lo) {
Ok((hi - b'0') as u16 * 10 + (lo - b'0') as u16)
} else {
Err(tag.value_error())
}
}

/// Encode 2-digit decimal value
pub(crate) fn encode_decimal(encoder: &mut Encoder<'_>, tag: Tag, value: u16) -> Result<()> {
let hi_val = value / 10;

if hi_val >= 10 {
return Err(tag.value_error());
}

encoder.byte(hi_val as u8 + b'0')?;
encoder.byte((value % 10) as u8 + b'0')
}

#[cfg(test)]
mod tests {
use super::DateTime;
Expand All @@ -262,7 +262,7 @@ mod tests {
fn is_date_valid(year: u16, month: u16, day: u16, hour: u16, minute: u16, second: u16) -> bool {
DateTime::new(year, month, day, hour, minute, second)
.and_then(|dt| dt.unix_duration())
.is_some()
.is_ok()
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions der/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ impl std::error::Error for ErrorKind {}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ErrorKind {
/// Date-and-time related errors.
DateTime,

/// Indicates a field which is duplicated when only one is expected.
DuplicateField {
/// Tag of the duplicated field.
Expand Down Expand Up @@ -217,6 +220,7 @@ impl ErrorKind {
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorKind::DateTime => write!(f, "date/time error"),
ErrorKind::DuplicateField { tag } => write!(f, "duplicate field for {}", tag),
ErrorKind::Failed => write!(f, "operation failed"),
ErrorKind::Length { tag } => write!(f, "incorrect length for {}", tag),
Expand Down
1 change: 1 addition & 0 deletions der/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ mod value;

pub use crate::{
asn1::{Any, Choice},
datetime::DateTime,
decodable::Decodable,
decoder::Decoder,
encodable::Encodable,
Expand Down

0 comments on commit 0ee952c

Please sign in to comment.