44
55package io .sentry .vendor ;
66
7+ import java .util .Calendar ;
8+ import java .util .GregorianCalendar ;
9+ import java .util .SimpleTimeZone ;
710import org .jetbrains .annotations .ApiStatus ;
811import org .jetbrains .annotations .NotNull ;
912
@@ -14,6 +17,7 @@ public final class SentryIso8601Utils {
1417 private static final long MILLIS_PER_MINUTE = 60L * MILLIS_PER_SECOND ;
1518 private static final long MILLIS_PER_HOUR = 60L * MILLIS_PER_MINUTE ;
1619 private static final long MILLIS_PER_DAY = 24L * MILLIS_PER_HOUR ;
20+ private static final long GREGORIAN_CUTOVER_MILLIS = -12219292800000L ;
1721 private static final int DAYS_0000_TO_1970 = 719468 ;
1822
1923 private SentryIso8601Utils () {}
@@ -33,14 +37,18 @@ public static long parseTimestamp(final @NotNull String timestamp) {
3337 }
3438
3539 final int day = parseInt (timestamp , offset , offset += 2 );
36- validateDate (year , month , day );
3740
3841 if (!checkOffset (timestamp , offset , 'T' )) {
39- if (offset != length ) {
40- throw new IllegalArgumentException ("Invalid date separator" );
42+ if (offset == length ) {
43+ return dateOnlyEpochMillis (year , month , day );
44+ }
45+ final char timezoneIndicator = timestamp .charAt (offset );
46+ if (timezoneIndicator == 'Z' || timezoneIndicator == '+' || timezoneIndicator == '-' ) {
47+ return dateOnlyEpochMillisWithTimezone (timestamp , length , offset , year , month , day );
4148 }
42- return epochMillis ( year , month , day , 0 , 0 , 0 , 0 , 0 );
49+ throw new IllegalArgumentException ( "Invalid date separator" );
4350 }
51+ validateDate (year , month , day );
4452 offset ++;
4553
4654 final int hour = parseInt (timestamp , offset , offset += 2 );
@@ -92,10 +100,12 @@ public static long parseTimestamp(final @NotNull String timestamp) {
92100 }
93101
94102 final int timezoneOffsetMillis ;
103+ final boolean allowTrailingCharacters ;
95104 final char timezoneIndicator = timestamp .charAt (offset );
96105 if (timezoneIndicator == 'Z' ) {
97106 timezoneOffsetMillis = 0 ;
98107 offset ++;
108+ allowTrailingCharacters = true ;
99109 } else if (timezoneIndicator == '+' || timezoneIndicator == '-' ) {
100110 final int sign = timezoneIndicator == '+' ? 1 : -1 ;
101111 offset ++;
@@ -110,18 +120,28 @@ public static long parseTimestamp(final @NotNull String timestamp) {
110120 validateTimezone (timezoneHour , timezoneMinute );
111121 timezoneOffsetMillis =
112122 sign * (int ) (timezoneHour * MILLIS_PER_HOUR + timezoneMinute * MILLIS_PER_MINUTE );
123+ allowTrailingCharacters = false ;
113124 } else {
114125 throw new IllegalArgumentException ("Invalid time zone indicator" );
115126 }
116127
117- if (offset != length ) {
128+ if (! allowTrailingCharacters && offset != length ) {
118129 throw new IllegalArgumentException ("Invalid trailing characters" );
119130 }
120131
132+ if (isBeforeGregorianCutover (year , month , day )) {
133+ return epochMillisWithCalendar (
134+ year , month , day , hour , minute , second , millisecond , timezoneOffsetMillis );
135+ }
136+
121137 return epochMillis (year , month , day , hour , minute , second , millisecond , timezoneOffsetMillis );
122138 }
123139
124140 public static @ NotNull String formatTimestamp (final long millis ) {
141+ if (millis < GREGORIAN_CUTOVER_MILLIS ) {
142+ return formatTimestampWithCalendar (millis );
143+ }
144+
125145 final long epochDay = Math .floorDiv (millis , MILLIS_PER_DAY );
126146 int millisOfDay = (int ) Math .floorMod (millis , MILLIS_PER_DAY );
127147
@@ -151,6 +171,97 @@ public static long parseTimestamp(final @NotNull String timestamp) {
151171 return timestamp .toString ();
152172 }
153173
174+ private static long dateOnlyEpochMillis (final int year , final int month , final int day ) {
175+ return new GregorianCalendar (year , month - 1 , day ).getTimeInMillis ();
176+ }
177+
178+ private static long dateOnlyEpochMillisWithTimezone (
179+ final @ NotNull String timestamp ,
180+ final int length ,
181+ int offset ,
182+ final int year ,
183+ final int month ,
184+ final int day ) {
185+ final int timezoneOffsetMillis ;
186+ final boolean allowTrailingCharacters ;
187+ final char timezoneIndicator = timestamp .charAt (offset );
188+ if (timezoneIndicator == 'Z' ) {
189+ timezoneOffsetMillis = 0 ;
190+ offset ++;
191+ allowTrailingCharacters = true ;
192+ } else if (timezoneIndicator == '+' || timezoneIndicator == '-' ) {
193+ final int sign = timezoneIndicator == '+' ? 1 : -1 ;
194+ offset ++;
195+ final int timezoneHour = parseInt (timestamp , offset , offset += 2 );
196+ int timezoneMinute = 0 ;
197+ if (checkOffset (timestamp , offset , ':' )) {
198+ offset ++;
199+ }
200+ if (length >= offset + 2 ) {
201+ timezoneMinute = parseInt (timestamp , offset , offset += 2 );
202+ }
203+ validateTimezone (timezoneHour , timezoneMinute );
204+ timezoneOffsetMillis =
205+ sign * (int ) (timezoneHour * MILLIS_PER_HOUR + timezoneMinute * MILLIS_PER_MINUTE );
206+ allowTrailingCharacters = false ;
207+ } else {
208+ throw new IllegalArgumentException ("Invalid time zone indicator" );
209+ }
210+
211+ if (!allowTrailingCharacters && offset != length ) {
212+ throw new IllegalArgumentException ("Invalid trailing characters" );
213+ }
214+
215+ if (isBeforeGregorianCutover (year , month , day )) {
216+ return epochMillisWithCalendar (year , month , day , 0 , 0 , 0 , 0 , timezoneOffsetMillis );
217+ }
218+ validateDate (year , month , day );
219+ return epochMillis (year , month , day , 0 , 0 , 0 , 0 , timezoneOffsetMillis );
220+ }
221+
222+ private static long epochMillisWithCalendar (
223+ final int year ,
224+ final int month ,
225+ final int day ,
226+ final int hour ,
227+ final int minute ,
228+ final int second ,
229+ final int millisecond ,
230+ final int timezoneOffsetMillis ) {
231+ final GregorianCalendar calendar = new GregorianCalendar (new SimpleTimeZone (timezoneOffsetMillis , "GMT" ));
232+ calendar .setLenient (false );
233+ calendar .set (Calendar .YEAR , year );
234+ calendar .set (Calendar .MONTH , month - 1 );
235+ calendar .set (Calendar .DAY_OF_MONTH , day );
236+ calendar .set (Calendar .HOUR_OF_DAY , hour );
237+ calendar .set (Calendar .MINUTE , minute );
238+ calendar .set (Calendar .SECOND , second );
239+ calendar .set (Calendar .MILLISECOND , millisecond );
240+ return calendar .getTimeInMillis ();
241+ }
242+
243+ private static @ NotNull String formatTimestampWithCalendar (final long millis ) {
244+ final GregorianCalendar calendar = new GregorianCalendar (new SimpleTimeZone (0 , "UTC" ));
245+ calendar .setTimeInMillis (millis );
246+
247+ final StringBuilder timestamp = new StringBuilder ("yyyy-MM-ddThh:mm:ss.sssZ" .length ());
248+ padInt (timestamp , calendar .get (Calendar .YEAR ), "yyyy" .length ());
249+ timestamp .append ('-' );
250+ padInt (timestamp , calendar .get (Calendar .MONTH ) + 1 , "MM" .length ());
251+ timestamp .append ('-' );
252+ padInt (timestamp , calendar .get (Calendar .DAY_OF_MONTH ), "dd" .length ());
253+ timestamp .append ('T' );
254+ padInt (timestamp , calendar .get (Calendar .HOUR_OF_DAY ), "hh" .length ());
255+ timestamp .append (':' );
256+ padInt (timestamp , calendar .get (Calendar .MINUTE ), "mm" .length ());
257+ timestamp .append (':' );
258+ padInt (timestamp , calendar .get (Calendar .SECOND ), "ss" .length ());
259+ timestamp .append ('.' );
260+ padInt (timestamp , calendar .get (Calendar .MILLISECOND ), "sss" .length ());
261+ timestamp .append ('Z' );
262+ return timestamp .toString ();
263+ }
264+
154265 private static long epochMillis (
155266 final int year ,
156267 final int month ,
@@ -190,6 +301,10 @@ private static int[] epochDayToYearMonthDay(long epochDay) {
190301 return new int [] {year + (month <= 2 ? 1 : 0 ), month , day };
191302 }
192303
304+ private static boolean isBeforeGregorianCutover (final int year , final int month , final int day ) {
305+ return year < 1582 || (year == 1582 && (month < 10 || (month == 10 && day < 15 )));
306+ }
307+
193308 private static void validateDate (final int year , final int month , final int day ) {
194309 if (year < 1 || month < 1 || month > 12 || day < 1 || day > daysInMonth (year , month )) {
195310 throw new IllegalArgumentException ("Invalid date" );
0 commit comments