Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GeneralizedTime #492

Merged
merged 15 commits into from
Nov 3, 2024
8 changes: 4 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,10 @@ pub use crate::tag::Tag;
pub use crate::types::{
Asn1DefinedByReadable, Asn1DefinedByWritable, Asn1Readable, Asn1Writable, BMPString, BigInt,
BigUint, Choice1, Choice2, Choice3, DateTime, DefinedByMarker, Enumerated, Explicit,
GeneralizedTime, IA5String, Implicit, Null, OctetStringEncoded, OwnedBigInt, OwnedBigUint,
PrintableString, Sequence, SequenceOf, SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter,
SimpleAsn1Readable, SimpleAsn1Writable, Tlv, UniversalString, UtcTime, Utf8String,
VisibleString,
GeneralizedTime, GeneralizedTimeFractional, IA5String, Implicit, Null, OctetStringEncoded,
OwnedBigInt, OwnedBigUint, PrintableString, Sequence, SequenceOf, SequenceOfWriter,
SequenceWriter, SetOf, SetOfWriter, SimpleAsn1Readable, SimpleAsn1Writable, Tlv,
UniversalString, UtcTime, Utf8String, VisibleString,
};
pub use crate::writer::{write, write_single, WriteBuf, WriteError, WriteResult, Writer};

Expand Down
61 changes: 57 additions & 4 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,10 @@ mod tests {
use crate::types::Asn1Readable;
use crate::{
BMPString, BigInt, BigUint, BitString, Choice1, Choice2, Choice3, DateTime, Enumerated,
Explicit, GeneralizedTime, IA5String, Implicit, ObjectIdentifier, OctetStringEncoded,
OwnedBigInt, OwnedBigUint, OwnedBitString, ParseError, ParseErrorKind, ParseLocation,
ParseResult, PrintableString, Sequence, SequenceOf, SetOf, Tag, Tlv, UniversalString,
UtcTime, Utf8String, VisibleString,
Explicit, GeneralizedTime, GeneralizedTimeFractional, IA5String, Implicit,
ObjectIdentifier, OctetStringEncoded, OwnedBigInt, OwnedBigUint, OwnedBitString,
ParseError, ParseErrorKind, ParseLocation, ParseResult, PrintableString, Sequence,
SequenceOf, SetOf, Tag, Tlv, UniversalString, UtcTime, Utf8String, VisibleString,
};
#[cfg(not(feature = "std"))]
use alloc::boxed::Box;
Expand Down Expand Up @@ -1588,6 +1588,59 @@ mod tests {
]);
}

#[test]
fn test_generalized_time_fractional() {
assert_parses::<GeneralizedTimeFractional>(&[
(
// General case
Ok(GeneralizedTimeFractional::new(
DateTime::new(2010, 1, 2, 3, 4, 5).unwrap(),
Some(123_456_000),
)
.unwrap()),
b"\x18\x1620100102030405.123456Z",
),
(
// No fractional time
Ok(GeneralizedTimeFractional::new(
DateTime::new(2010, 1, 2, 3, 4, 5).unwrap(),
None,
)
.unwrap()),
b"\x18\x0f20100102030405Z",
),
(
// Starting with 0 is ok
Ok(GeneralizedTimeFractional::new(
DateTime::new(2010, 1, 2, 3, 4, 5).unwrap(),
Some(12_375_600),
)
.unwrap()),
b"\x18\x1720100102030405.0123756Z",
),
(
// But ending with 0 is not OK
Err(ParseError::new(ParseErrorKind::InvalidValue)),
b"\x18\x1220100102030405.10Z",
),
(
// Too many digits
Err(ParseError::new(ParseErrorKind::InvalidValue)),
b"\x18\x1a20100102030405.0123456789Z",
),
(
// Missing timezone
Err(ParseError::new(ParseErrorKind::InvalidValue)),
b"\x18\x1520100102030405.123456",
),
(
// Invalid fractional second
Err(ParseError::new(ParseErrorKind::InvalidValue)),
b"\x18\x1020100102030405.Z",
),
])
}

#[test]
fn test_enumerated() {
assert_parses::<Enumerated>(&[
Expand Down
185 changes: 181 additions & 4 deletions src/types.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#[cfg(not(feature = "std"))]
use alloc::boxed::Box;
use alloc::format;
#[cfg(not(feature = "std"))]
use alloc::vec;
#[cfg(not(feature = "std"))]
Expand Down Expand Up @@ -914,7 +915,8 @@ fn push_four_digits(dest: &mut WriteBuf, val: u16) -> WriteResult {
}

/// A structure representing a (UTC timezone) date and time.
/// Wrapped by `UtcTime` and `GeneralizedTime`.
/// Wrapped by `UtcTime` and `GeneralizedTime` and used in
/// `GeneralizedTimeFractional`.
#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd)]
pub struct DateTime {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should DateTime just have an optional nanoseconds field?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, missed this earlier. I don't have a super strong opinion -- I think @DarkaMaul put the nanos on GeneralizedTime since UTCTime has no fractional time support at all, so having it on the top-level DateTime used by both seems (slightly) off.

OTOH having it on GeneralizedTime also seems slightly off, since it's now (DateTime, nanos). So I'm happy to move if you'd prefer.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, let's get the docs right on the generalized tiem strcut and that'll be good enough. (See my other comment)

year: u16,
Expand Down Expand Up @@ -1086,6 +1088,129 @@ impl SimpleAsn1Writable for GeneralizedTime {
}
}

/// Used for parsing and writing ASN.1 `GENERALIZED TIME` values accepting
/// fractional seconds value.
/// See https://github.com/alex/rust-asn1/issues/491 for discussion.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should be updated to just describe the type, not reference an issue about how things used to be.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

woodruffw marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq)]
pub struct GeneralizedTimeFractional {
DarkaMaul marked this conversation as resolved.
Show resolved Hide resolved
datetime: DateTime,
nanoseconds: Option<u32>, // Up to 1 ns precision (10^9)
DarkaMaul marked this conversation as resolved.
Show resolved Hide resolved
}

impl GeneralizedTimeFractional {
pub fn new(dt: DateTime, nanoseconds: Option<u32>) -> ParseResult<GeneralizedTimeFractional> {
if let Some(val) = nanoseconds {
if val < 1 || val >= 1e9 as u32 {
return Err(ParseError::new(ParseErrorKind::InvalidValue));
}
}

Ok(GeneralizedTimeFractional {
datetime: dt,
nanoseconds,
})
}

pub fn as_datetime(&self) -> &DateTime {
&self.datetime
}

pub fn nanoseconds(&self) -> Option<u32> {
self.nanoseconds
}
}

fn read_fractional_time(data: &mut &[u8]) -> ParseResult<Option<u32>> {
// We cannot use read_byte here because it will advance the pointer
// However, we know that the is suffixed by 'Z' so reading into an empty
// data should lead to an error.
if data.first() == Some(&b'.') {
*data = &data[1..];

let mut fraction = 0u32;
// Read up to 9 digits
let mut digits = 0;
while digits < 9 {
match data.first() {
Some(b) => {
if !b.is_ascii_digit() {
if digits == 0 {
// We must have at least one digit
return Err(ParseError::new(ParseErrorKind::InvalidValue));
}
break;
}

*data = &data[1..];
fraction = fraction * 10 + (b - b'0') as u32;
}
None => {
return Err(ParseError::new(ParseErrorKind::InvalidValue));
}
}
digits += 1;
}
DarkaMaul marked this conversation as resolved.
Show resolved Hide resolved

// No trailing zero
if fraction % 10 == 0 {
return Err(ParseError::new(ParseErrorKind::InvalidValue));
}

// Now let scale up in nanoseconds
let nanoseconds: u32 = fraction * 10u32.pow(9 - digits);
Ok(Some(nanoseconds))
} else {
Ok(None)
}
}

impl SimpleAsn1Readable<'_> for GeneralizedTimeFractional {
const TAG: Tag = Tag::primitive(0x18);
fn parse_data(mut data: &[u8]) -> ParseResult<GeneralizedTimeFractional> {
let year = read_4_digits(&mut data)?;
let month = read_2_digits(&mut data)?;
let day = read_2_digits(&mut data)?;
let hour = read_2_digits(&mut data)?;
let minute = read_2_digits(&mut data)?;
let second = read_2_digits(&mut data)?;

let fraction = read_fractional_time(&mut data)?;
read_tz_and_finish(&mut data)?;

GeneralizedTimeFractional::new(
DateTime::new(year, month, day, hour, minute, second)?,
fraction,
)
}
}

impl SimpleAsn1Writable for GeneralizedTimeFractional {
const TAG: Tag = Tag::primitive(0x18);
fn write_data(&self, dest: &mut WriteBuf) -> WriteResult {
let dt = self.as_datetime();
push_four_digits(dest, dt.year())?;
push_two_digits(dest, dt.month())?;
push_two_digits(dest, dt.day())?;

push_two_digits(dest, dt.hour())?;
push_two_digits(dest, dt.minute())?;
push_two_digits(dest, dt.second())?;

if let Some(nanoseconds) = self.nanoseconds() {
dest.push_byte(b'.')?;

for digit in format!("{:09}", nanoseconds)
alex marked this conversation as resolved.
Show resolved Hide resolved
.trim_end_matches('0')
.as_bytes()
{
dest.push_byte(*digit)?;
}
}

dest.push_byte(b'Z')
}
}

/// An ASN.1 `ENUMERATED` value.
#[derive(Debug, PartialEq, Eq)]
pub struct Enumerated(u32);
Expand Down Expand Up @@ -1724,9 +1849,9 @@ impl<T> DefinedByMarker<T> {
mod tests {
use crate::{
parse_single, BigInt, BigUint, DateTime, DefinedByMarker, Enumerated, GeneralizedTime,
IA5String, ObjectIdentifier, OctetStringEncoded, OwnedBigInt, OwnedBigUint, ParseError,
ParseErrorKind, PrintableString, SequenceOf, SequenceOfWriter, SetOf, SetOfWriter, Tag,
Tlv, UtcTime, Utf8String, VisibleString,
GeneralizedTimeFractional, IA5String, ObjectIdentifier, OctetStringEncoded, OwnedBigInt,
OwnedBigUint, ParseError, ParseErrorKind, PrintableString, SequenceOf, SequenceOfWriter,
SetOf, SetOfWriter, Tag, Tlv, UtcTime, Utf8String, VisibleString,
};
use crate::{Explicit, Implicit};
#[cfg(not(feature = "std"))]
Expand Down Expand Up @@ -2003,6 +2128,58 @@ mod tests {
assert!(GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap()).is_ok());
}

#[test]
fn test_generalized_time_fractional_new() {
assert!(GeneralizedTimeFractional::new(
DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(),
Some(1234)
)
.is_ok());
assert!(GeneralizedTimeFractional::new(
DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(),
None
)
.is_ok());
assert!(GeneralizedTimeFractional::new(
DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(),
Some(1e9 as u32 + 1)
)
.is_err());
}

#[test]
fn test_generalized_time_fractional_partial_ord() {
let point = GeneralizedTimeFractional::new(
DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(),
Some(1234),
)
.unwrap();
assert!(
point
< GeneralizedTimeFractional::new(
DateTime::new(2023, 6, 30, 23, 59, 59).unwrap(),
Some(1234)
)
.unwrap()
);
assert!(
point
< GeneralizedTimeFractional::new(
DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(),
Some(1235)
)
.unwrap()
);
assert!(
point
> GeneralizedTimeFractional::new(
DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(),
None
)
.unwrap()
);
}

#[test]
fn test_enumerated_value() {
assert_eq!(Enumerated::new(4).value(), 4);
Expand Down
51 changes: 47 additions & 4 deletions src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,10 +216,10 @@ mod tests {
use crate::types::Asn1Writable;
use crate::{
parse_single, BMPString, BigInt, BigUint, BitString, Choice1, Choice2, Choice3, DateTime,
Enumerated, Explicit, GeneralizedTime, IA5String, Implicit, ObjectIdentifier,
OctetStringEncoded, OwnedBigInt, OwnedBigUint, OwnedBitString, PrintableString, Sequence,
SequenceOf, SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter, Tlv, UniversalString,
UtcTime, Utf8String, VisibleString, WriteError,
Enumerated, Explicit, GeneralizedTime, GeneralizedTimeFractional, IA5String, Implicit,
ObjectIdentifier, OctetStringEncoded, OwnedBigInt, OwnedBigUint, OwnedBitString,
PrintableString, Sequence, SequenceOf, SequenceOfWriter, SequenceWriter, SetOf,
SetOfWriter, Tlv, UniversalString, UtcTime, Utf8String, VisibleString, WriteError,
};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
Expand Down Expand Up @@ -570,6 +570,49 @@ mod tests {
]);
}

#[test]
fn test_write_generalizedtime_fractional() {
assert_writes(&[
(
GeneralizedTimeFractional::new(
DateTime::new(1991, 5, 6, 23, 45, 40).unwrap(),
Some(1_234),
)
.unwrap(),
b"\x18\x1919910506234540.000001234Z",
),
(
GeneralizedTimeFractional::new(
DateTime::new(1991, 5, 6, 23, 45, 40).unwrap(),
Some(1),
)
.unwrap(),
b"\x18\x1919910506234540.000000001Z",
),
(
GeneralizedTimeFractional::new(DateTime::new(1970, 1, 1, 0, 0, 0).unwrap(), None)
.unwrap(),
b"\x18\x0f19700101000000Z",
),
(
GeneralizedTimeFractional::new(
DateTime::new(2009, 11, 15, 22, 56, 16).unwrap(),
Some(100_000_000),
)
.unwrap(),
b"\x18\x1120091115225616.1Z",
),
(
GeneralizedTimeFractional::new(
DateTime::new(2009, 11, 15, 22, 56, 16).unwrap(),
Some(999_999_999),
)
.unwrap(),
b"\x18\x1920091115225616.999999999Z",
),
]);
}

#[test]
fn test_write_enumerated() {
assert_writes::<Enumerated>(&[
Expand Down