Skip to content

Commit 0f0c85e

Browse files
committed
addressed #18 and added unit tests
1 parent 702b6d2 commit 0f0c85e

File tree

2 files changed

+125
-46
lines changed

2 files changed

+125
-46
lines changed

Asn1Parser/Utils/DateTimeUtils.cs

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Globalization;
3+
using System.IO;
34
using System.Linq;
45
using System.Text;
56

@@ -46,43 +47,56 @@ public static DateTime Decode(Asn1Reader asn, out TimeZoneInfo? zone) {
4647
return extractDateTime(SB.ToString(), out zone);
4748
}
4849

49-
static DateTime extractDateTime(String strValue, out TimeZoneInfo zone) {
50-
Int32 delimiterIndex;
51-
zone = TimeZoneInfo.FindSystemTimeZoneById("Greenwich Standard Time");
52-
if (strValue.ToUpper().Contains("Z")) {
53-
delimiterIndex = strValue.ToUpper().IndexOf('Z');
54-
return extractZulu(strValue, delimiterIndex);
55-
}
56-
Boolean hasZone = extractZoneShift(strValue, out Int32 hours, out Int32 minutes, out delimiterIndex);
57-
Int32 milliseconds = extractMilliseconds(strValue, delimiterIndex, out Int32 msDelimiter);
58-
DateTime retValue = extractDateTime(strValue, msDelimiter, delimiterIndex);
50+
static DateTime extractDateTime(String strValue, out TimeZoneInfo? zone) {
51+
zone = null;
52+
Boolean hasZone = extractZoneShift(strValue, out Int32 hours, out Int32 minutes, out Int32 zoneDelimiter);
53+
Int32 milliseconds = extractMilliseconds(strValue, zoneDelimiter, out Int32 msDelimiter);
54+
DateTime retValue = extractDateTime(strValue, msDelimiter, zoneDelimiter);
5955
if (hasZone) {
6056
zone = bindZone(hours, minutes);
61-
retValue = retValue.AddHours(hours);
62-
retValue = retValue.AddMinutes(minutes);
57+
//retValue = retValue.AddHours(hours);
58+
//retValue = retValue.AddMinutes(minutes);
59+
} else {
60+
retValue = DateTime.SpecifyKind(retValue, DateTimeKind.Utc).ToLocalTime();
6361
}
6462
retValue = retValue.AddMilliseconds(milliseconds);
63+
6564
return retValue;
6665
}
67-
static DateTime extractZulu(String strValue, Int32 zoneDelimiter) {
68-
return zoneDelimiter switch {
69-
12 => parseExactUtc(strValue.Replace("Z", null), UTCFormat).ToLocalTime(),
70-
16 => parseExactUtc(strValue.Replace("Z", null), UTCPreciseFormat).ToLocalTime(),
71-
14 => DateTime.ParseExact(strValue.Replace("Z", null), GtFormat, null).ToLocalTime(),
72-
18 => DateTime.ParseExact(strValue.Replace("Z", null), GtPreciseFormat, null).ToLocalTime(),
73-
_ => throw new ArgumentException("Time zone suffix is not valid.")
66+
static DateTime extractDateTime(String strValue, Int32 msDelimiter, Int32 zoneDelimiter) {
67+
String rawString;
68+
if (msDelimiter < 0 && zoneDelimiter < 0) {
69+
// Zulu time zone, no milliseconds
70+
rawString = strValue;
71+
} else if (msDelimiter < 0) {
72+
// Custom time zone, no milliseconds
73+
rawString = strValue.Substring(0, zoneDelimiter);
74+
} else {
75+
// Milliseconds
76+
rawString = strValue.Substring(0, msDelimiter);
77+
}
78+
79+
return rawString.Length switch {
80+
12 => parseExactUtc(rawString, UTCFormat),
81+
14 => DateTime.ParseExact(rawString, GtFormat, null),
82+
_ => throw new ArgumentException("Time zone suffix is not valid.")
7483
};
7584
}
7685
static Boolean extractZoneShift(String strValue, out Int32 hours, out Int32 minutes, out Int32 delimiterIndex) {
86+
if (strValue.EndsWith("Z")) {
87+
delimiterIndex = strValue.IndexOf('Z');
88+
hours = minutes = 0;
89+
return false;
90+
}
91+
7792
if (strValue.Contains('+')) {
7893
delimiterIndex = strValue.IndexOf('+');
7994
hours = Int32.Parse(strValue.Substring(delimiterIndex, 3));
8095
} else if (strValue.Contains('-')) {
8196
delimiterIndex = strValue.IndexOf('-');
8297
hours = -Int32.Parse(strValue.Substring(delimiterIndex, 3));
8398
} else {
84-
hours = minutes = delimiterIndex = 0;
85-
return false;
99+
throw new InvalidDataException("ASN.1 DateTime has missing time zone identifier.");
86100
}
87101
minutes = strValue.Length > delimiterIndex + 3
88102
? -Int32.Parse(strValue.Substring(delimiterIndex + 3, 2))
@@ -97,40 +111,27 @@ static Int32 extractMilliseconds(String strValue, Int32 zoneDelimiter, out Int32
97111
Int32 precisionLength = zoneDelimiter > 0
98112
? zoneDelimiter - msDelimiter - 1
99113
: strValue.Length - msDelimiter - 1;
100-
return Int32.Parse(strValue.Substring(msDelimiter + 1, precisionLength));
114+
// milliseconds decimal part
115+
Int32 msNumber = Int32.Parse(strValue.Substring(msDelimiter + 1, precisionLength));
116+
// if precision length is 1, then msNumber represents milliseconds * 100
117+
// if precision length is 2, then msNumber represents milliseconds * 10
118+
// if precision length is 3, then msNumber represents milliseconds * 1
119+
// we can get this by: 100 * msNumber / 10 ^ precisionLength
120+
return (Int32)(msNumber / Math.Pow(10, precisionLength) * 1000);
101121
}
102-
static DateTime parseExactUtc(String strValue, String format) {
122+
static DateTime parseExactUtc(String strValue, params String[] format) {
103123
// fix: .NET 'yy' format works in range between 1930-2030. As per RFC5280,
104124
// dates must be between 1950-2049. In .NET, years between 30 and 50 are treated
105125
// as 1930-1950, while it should be 2030-2050. So, fix the range between 30 and 50
106126
// by adding a century.
107-
var dateTime = DateTime.ParseExact(strValue, format, null);
127+
var dateTime = DateTime.ParseExact(strValue, format, null, DateTimeStyles.None);
108128
// not inclusive. Starting with 2050, GeneralizedTime is used, so 50+ values will go
109-
// to 20th century as in .NET
129+
// to 21st century as in .NET
110130
if (dateTime.Year < 1950) {
111131
dateTime = dateTime.AddYears(100);
112132
}
113133
return dateTime;
114134
}
115-
static DateTime extractDateTime(String strValue, Int32 msDelimiter, Int32 zoneDelimiter) {
116-
String rawString;
117-
if (msDelimiter < 0 && zoneDelimiter < 0) {
118-
// Zulu time zone, no milliseconds
119-
rawString = strValue;
120-
} else if (msDelimiter < 0) {
121-
// Custom time zone, no milliseconds
122-
rawString = strValue.Substring(0, zoneDelimiter);
123-
} else {
124-
// Milliseconds
125-
rawString = strValue.Substring(0, msDelimiter);
126-
}
127-
128-
return rawString.Length switch {
129-
12 => parseExactUtc(rawString, UTCFormat),
130-
14 => DateTime.ParseExact(rawString, GtFormat, null),
131-
_ => throw new ArgumentException("Time zone suffix is not valid.")
132-
};
133-
}
134135
static TimeZoneInfo bindZone(Int32 hours, Int32 minutes) {
135136
foreach (TimeZoneInfo zone in TimeZoneInfo.GetSystemTimeZones().Where(zone => zone.BaseUtcOffset.Hours == hours && zone.BaseUtcOffset.Minutes == minutes)) {
136137
return zone;
@@ -140,8 +141,6 @@ static TimeZoneInfo bindZone(Int32 hours, Int32 minutes) {
140141

141142
#region Constants
142143
const String UTCFormat = "yyMMddHHmmss";
143-
const String UTCPreciseFormat = "yyMMddHHmmss.FFF";
144144
const String GtFormat = "yyyyMMddHHmmss";
145-
const String GtPreciseFormat = "yyyyMMddHHmmss.FFF";
146145
#endregion
147146
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System;
2+
using System.Linq;
3+
using System.Text;
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
using SysadminsLV.Asn1Parser;
6+
using SysadminsLV.Asn1Parser.Universal;
7+
8+
namespace Asn1Parser.Tests;
9+
10+
[TestClass]
11+
public class Asn1DateTimeTests {
12+
[TestMethod, Description("Tests date/time parsing using ")]
13+
public void TestZuluSimple() {
14+
var dt = DateTime.ParseExact("2024-08-07 16:12:37", "yyyy-MM-dd HH:mm:ss", null);
15+
var gt = new Asn1GeneralizedTime(dt);
16+
assertDateTimeEncode(Asn1Type.GeneralizedTime, gt, dt, "yyyyMMddHHmmssZ");
17+
}
18+
[TestMethod, Description("Test date/time with fractions and fraction is zero")]
19+
public void TestZuluFraction0() {
20+
var dt = DateTime.ParseExact("2024-08-07 16:12:37.0", "yyyy-MM-dd HH:mm:ss.f", null);
21+
var gt = new Asn1GeneralizedTime(dt, true);
22+
assertDateTimeEncode(Asn1Type.GeneralizedTime, gt, dt, "yyyyMMddHHmmssZ");
23+
}
24+
[TestMethod]
25+
public void TestZuluFraction1() {
26+
var dt = DateTime.ParseExact("2024-08-07 16:12:37.1", "yyyy-MM-dd HH:mm:ss.f", null);
27+
var gt = new Asn1GeneralizedTime(dt, true);
28+
assertDateTimeEncode(Asn1Type.GeneralizedTime, gt, dt, "yyyyMMddHHmmss.fZ");
29+
}
30+
[TestMethod]
31+
public void TestZuluFraction2() {
32+
var dt = DateTime.ParseExact("2024-08-07 16:12:37.15", "yyyy-MM-dd HH:mm:ss.ff", null);
33+
var gt = new Asn1GeneralizedTime(dt, true);
34+
assertDateTimeEncode(Asn1Type.GeneralizedTime, gt, dt, "yyyyMMddHHmmss.ffZ");
35+
}
36+
[TestMethod]
37+
public void TestZuluFraction3() {
38+
var dt = DateTime.ParseExact("2024-08-07 16:12:37.153", "yyyy-MM-dd HH:mm:ss.fff", null);
39+
var gt = new Asn1GeneralizedTime(dt, true);
40+
assertDateTimeEncode(Asn1Type.GeneralizedTime, gt, dt, "yyyyMMddHHmmss.fffZ");
41+
}
42+
[TestMethod]
43+
public void TestTimeZone() {
44+
var zone = TimeZoneInfo.FindSystemTimeZoneById("FLE Standard Time");
45+
var dt = DateTime.ParseExact("2024-08-07 16:12:37", "yyyy-MM-dd HH:mm:ss", null);
46+
var gt = new Asn1GeneralizedTime(dt, zone);
47+
assertDateTimeEncode(Asn1Type.GeneralizedTime, gt, dt, "yyyyMMddHHmmss+0200");
48+
}
49+
[TestMethod]
50+
public void TestTimeZoneFraction() {
51+
var zone = TimeZoneInfo.FindSystemTimeZoneById("FLE Standard Time");
52+
var dt = DateTime.ParseExact("2024-08-07 16:12:37.15", "yyyy-MM-dd HH:mm:ss.ff", null);
53+
var gt = new Asn1GeneralizedTime(dt, zone, true);
54+
assertDateTimeEncode(Asn1Type.GeneralizedTime, gt, dt, "yyyyMMddHHmmss.ff+0200");
55+
}
56+
57+
static void assertDateTimeEncode(Asn1Type expectedType, Asn1DateTime adt, DateTime dt, String expectedFormat, Boolean decode = false) {
58+
// assert type
59+
Assert.AreEqual((Byte)expectedType, adt.Tag);
60+
61+
String gts = Encoding.ASCII.GetString(adt.GetRawData().Skip(2).ToArray());
62+
Assert.AreEqual(dt, adt.Value);
63+
if (adt.ZoneInfo == null) {
64+
dt = dt.ToUniversalTime();
65+
}
66+
Assert.AreEqual(dt.ToString(expectedFormat), gts);
67+
if (!decode) {
68+
if (adt.ZoneInfo == null) {
69+
dt = dt.ToLocalTime();
70+
}
71+
assertDateTimeDecode(expectedType, adt, dt, expectedFormat);
72+
}
73+
}
74+
static void assertDateTimeDecode(Asn1Type expectedTime, Asn1DateTime adt, DateTime dt, String expectedFormat) {
75+
adt = expectedTime == Asn1Type.UTCTime
76+
? new Asn1UtcTime(adt.GetRawData())
77+
: new Asn1GeneralizedTime(adt.GetRawData());
78+
assertDateTimeEncode(expectedTime, adt, dt, expectedFormat, true);
79+
}
80+
}

0 commit comments

Comments
 (0)