diff --git a/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java b/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java index 606d6c7146..099b38deaa 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java +++ b/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java @@ -178,9 +178,26 @@ public static Date parse(String date, ParsePosition pos) throws ParseException { if (!hasT && (date.length() <= offset)) { Calendar calendar = new GregorianCalendar(year, month - 1, day); calendar.setLenient(false); - - pos.setIndex(offset); - return calendar.getTime(); + try { + pos.setIndex(offset); + return calendar.getTime(); + } catch (IllegalArgumentException e) { + // The calendar date can be valid while local midnight does not exist in the default + // time zone, for example '1966-11-01' in America/Sao_Paulo where clocks were shifted + // forward from 0:00 to 1:00. In that case resolve the date like java.time's + // LocalDate.atStartOfDay(ZoneId) by using the first existing instant of that day. + // Truly invalid dates (such as '2021-02-30') also fail above, but lenient resolution + // would shift them to a different day, so they are re-thrown below. + Calendar lenientCalendar = new GregorianCalendar(year, month - 1, day); + Date resolved = lenientCalendar.getTime(); + if (lenientCalendar.get(Calendar.YEAR) != year + || lenientCalendar.get(Calendar.MONTH) != month - 1 + || lenientCalendar.get(Calendar.DAY_OF_MONTH) != day) { + throw e; + } + pos.setIndex(offset); + return resolved; + } } if (hasT) { diff --git a/gson/src/test/java/com/google/gson/internal/bind/util/ISO8601UtilsTest.java b/gson/src/test/java/com/google/gson/internal/bind/util/ISO8601UtilsTest.java index 1c2d254dec..1867917de6 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/util/ISO8601UtilsTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/util/ISO8601UtilsTest.java @@ -43,6 +43,50 @@ private static GregorianCalendar createUtcCalendar() { return calendar; } + @Test + public void testParseDateOnlyDSTGap() throws ParseException { + TimeZone defaultTimeZone = TimeZone.getDefault(); + try { + // 1966-11-01 00:00 does not exist in America/Sao_Paulo because clocks were shifted + // forward from 0:00 to 1:00 at midnight; parsing must not fail for this valid date + TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo")); + Date date = ISO8601Utils.parse("1966-11-01", new ParsePosition(0)); + + GregorianCalendar calendar = + new GregorianCalendar(TimeZone.getTimeZone("America/Sao_Paulo"), Locale.US); + // Calendar was created with current time, must clear it + calendar.clear(); + calendar.setTime(date); + assertThat(calendar.get(Calendar.YEAR)).isEqualTo(1966); + assertThat(calendar.get(Calendar.MONTH)).isEqualTo(Calendar.NOVEMBER); + assertThat(calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(1); + // The resolved hour depends on the DST rules of the JDK's bundled timezone data + // (midnight itself, or the first hour after the 0:00 -> 1:00 transition), so only + // assert that the date fields are preserved while the hour stays within the day + assertThat(calendar.get(Calendar.HOUR_OF_DAY)).isAnyOf(0, 1); + } finally { + TimeZone.setDefault(defaultTimeZone); + } + } + + @Test + public void testParseInvalidDateOnlyStillFails() { + TimeZone defaultTimeZone = TimeZone.getDefault(); + try { + // Strict parsing introduced for date-only values must keep rejecting dates which do + // not exist, even in time zones where midnight is skipped by a DST transition + TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo")); + assertThrows( + ParseException.class, () -> ISO8601Utils.parse("2021-02-30", new ParsePosition(0))); + assertThrows( + ParseException.class, () -> ISO8601Utils.parse("2021-13-01", new ParsePosition(0))); + assertThrows( + ParseException.class, () -> ISO8601Utils.parse("1966-00-01", new ParsePosition(0))); + } finally { + TimeZone.setDefault(defaultTimeZone); + } + } + @Test public void testDateFormatString() { GregorianCalendar calendar = new GregorianCalendar(utcTimeZone(), Locale.US);