Skip to content

Commit 62a5a41

Browse files
committed
fix(core): Parse date-only timestamps with timezones
Preserve ISO8601 parser compatibility for date-only values that include a timezone suffix. Keep modern date-only timezone parsing on the fast path and add parity coverage against the previous parser.
1 parent 9174e18 commit 62a5a41

2 files changed

Lines changed: 82 additions & 3 deletions

File tree

sentry/src/main/java/io/sentry/vendor/SentryIso8601Utils.java

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@ public static long parseTimestamp(final @NotNull String timestamp) {
3939
final int day = parseInt(timestamp, offset, offset += 2);
4040

4141
if (!checkOffset(timestamp, offset, 'T')) {
42-
if (offset != length) {
43-
throw new IllegalArgumentException("Invalid date separator");
42+
if (offset == length) {
43+
return dateOnlyEpochMillis(year, month, day);
4444
}
45-
return dateOnlyEpochMillis(year, month, day);
45+
final char timezoneIndicator = timestamp.charAt(offset);
46+
if (timezoneIndicator == 'Z' || timezoneIndicator == '+' || timezoneIndicator == '-') {
47+
return dateOnlyEpochMillisWithTimezone(timestamp, length, offset, year, month, day);
48+
}
49+
throw new IllegalArgumentException("Invalid date separator");
4650
}
4751
validateDate(year, month, day);
4852
offset++;
@@ -171,6 +175,50 @@ private static long dateOnlyEpochMillis(final int year, final int month, final i
171175
return new GregorianCalendar(year, month - 1, day).getTimeInMillis();
172176
}
173177

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+
174222
private static long epochMillisWithCalendar(
175223
final int year,
176224
final int month,

sentry/src/test/java/io/sentry/DateUtilsTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,37 @@ class DateUtilsTest {
222222
}
223223
}
224224

225+
@Test
226+
fun `Fast timestamp parser matches previous ISO8601 parser for date-only values with timezone`() {
227+
val input =
228+
listOf(
229+
"2020-03-27Z",
230+
"2020-03-27+02:00",
231+
"2020-03-27+0200",
232+
"2020-03-27+02",
233+
"2020-03-27-03:30",
234+
"20200327Z",
235+
"20200327+02:00",
236+
"20200327-0330",
237+
)
238+
239+
input.forEach {
240+
assertEquals(
241+
ISO8601Utils.parse(it, ParsePosition(0)).time,
242+
DateUtils.getDateTime(it).time,
243+
"timestamp=$it",
244+
)
245+
}
246+
}
247+
248+
@Test
249+
fun `Fast timestamp parser rejects invalid date-only values with timezone like previous ISO8601 parser`() {
250+
val timestamp = "2020-02-30Z"
251+
252+
assertFailsWith<Exception> { ISO8601Utils.parse(timestamp, ParsePosition(0)) }
253+
assertFailsWith<IllegalArgumentException> { DateUtils.getDateTime(timestamp) }
254+
}
255+
225256
@Test
226257
fun `Fast timestamp parser rejects date-time without timezone like previous ISO8601 parser`() {
227258
val input = listOf("2020-03-27T08:52", "2020-03-27T08:52:58", "2020-03-27T08:52:58.015")

0 commit comments

Comments
 (0)