diff --git a/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java
index b5dffe24fb..52f68371b1 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java
@@ -30,7 +30,6 @@
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
-import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
@@ -179,7 +178,7 @@ private Date deserializeToDate(JsonReader in) throws IOException {
}
try {
- return ISO8601Utils.parse(s, new ParsePosition(0));
+ return ISO8601Utils.parseFully(s);
} catch (ParseException e) {
throw new JsonSyntaxException(
"Failed parsing '" + s + "' as Date; at path " + in.getPreviousPath(), e);
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..89772a6f04 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
@@ -33,7 +33,7 @@
*
* @see this specification
*/
-// Date parsing code from Jackson databind ISO8601Utils.java
+// Date parsing code from Jackson databind ISO8601Utils.java, with modifications
// https://github.com/FasterXML/jackson-databind/blob/2.8/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
@SuppressWarnings("MemberName") // legacy class name
public final class ISO8601Utils {
@@ -143,7 +143,11 @@ public static String format(Date date, boolean millis, TimeZone tz) {
* @param pos The position to start parsing from, updated to where parsing stopped.
* @return the parsed date
* @throws ParseException if the date is not in the appropriate format
+ * @deprecated For Gson-internal usage prefer {@link #parseFully(String)}.
+ * For external users: {@code ISO8601Utils} is a Gson-internal class; prefer {@code java.time}
+ * for date parsing.
*/
+ @Deprecated
public static Date parse(String date, ParsePosition pos) throws ParseException {
Exception fail = null;
try {
@@ -237,14 +241,18 @@ public static Date parse(String date, ParsePosition pos) throws ParseException {
if (timezoneIndicator == 'Z') {
timezone = TIMEZONE_UTC;
offset += 1;
+
+ if (date.length() > offset) {
+ throw new IllegalArgumentException("Trailing data after time zone 'Z'");
+ }
} else if (timezoneIndicator == '+' || timezoneIndicator == '-') {
String timezoneOffset = date.substring(offset);
+ offset += timezoneOffset.length();
// When timezone has no minutes, we should append it, valid timezones are, for example:
// +00:00, +0000 and +00
timezoneOffset = timezoneOffset.length() >= 5 ? timezoneOffset : timezoneOffset + "00";
- offset += timezoneOffset.length();
// 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00"
if (timezoneOffset.equals("+0000") || timezoneOffset.equals("+00:00")) {
timezone = TIMEZONE_UTC;
@@ -308,6 +316,25 @@ public static Date parse(String date, ParsePosition pos) throws ParseException {
throw ex;
}
+ /**
+ * Fully parses a date string.
+ *
+ *
Delegates to {@link #parse(String, ParsePosition)} and verifies that the given string was + * fully parsed. + */ + public static Date parseFully(String dateStr) throws ParseException { + ParsePosition pos = new ParsePosition(0); + Date date = ISO8601Utils.parse(dateStr, pos); + + int parseEnd = pos.getIndex(); + if (parseEnd != dateStr.length()) { + // Note: Should be effectively unreachable because `ISO8601Utils#parse` already throws if + // there is unrelated trailing data; but keep this check here to be safe + throw new ParseException("Failed to fully parse date [" + dateStr + "]", parseEnd); + } + return date; + } + /** * Check if the expected character exist at the given offset in the value. * 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..51e4792e8a 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 @@ -20,7 +20,6 @@ import static org.junit.Assert.assertThrows; import java.text.ParseException; -import java.text.ParsePosition; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -79,7 +78,7 @@ public void testDateFormatWithTimezone() { @SuppressWarnings("UndefinedEquals") public void testDateParseWithDefaultTimezone() throws ParseException { String dateStr = "2018-06-25"; - Date date = ISO8601Utils.parse(dateStr, new ParsePosition(0)); + Date date = ISO8601Utils.parseFully(dateStr); Date expectedDate = new GregorianCalendar(2018, Calendar.JUNE, 25).getTime(); assertThat(date).isEqualTo(expectedDate); } @@ -87,20 +86,20 @@ public void testDateParseWithDefaultTimezone() throws ParseException { @Test public void testDateParseInvalidDay() { String dateStr = "2022-12-33"; - assertThrows(ParseException.class, () -> ISO8601Utils.parse(dateStr, new ParsePosition(0))); + assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr)); } @Test public void testDateParseInvalidMonth() { String dateStr = "2022-14-30"; - assertThrows(ParseException.class, () -> ISO8601Utils.parse(dateStr, new ParsePosition(0))); + assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr)); } @Test @SuppressWarnings("UndefinedEquals") public void testDateParseWithTimezone() throws ParseException { String dateStr = "2018-06-25T00:00:00-03:00"; - Date date = ISO8601Utils.parse(dateStr, new ParsePosition(0)); + Date date = ISO8601Utils.parseFully(dateStr); GregorianCalendar calendar = createUtcCalendar(); calendar.set(2018, Calendar.JUNE, 25, 3, 0); Date expectedDate = calendar.getTime(); @@ -111,16 +110,55 @@ public void testDateParseWithTimezone() throws ParseException { @SuppressWarnings("UndefinedEquals") public void testDateParseSpecialTimezone() throws ParseException { String dateStr = "2018-06-25T00:02:00-02:58"; - Date date = ISO8601Utils.parse(dateStr, new ParsePosition(0)); + Date date = ISO8601Utils.parseFully(dateStr); GregorianCalendar calendar = createUtcCalendar(); calendar.set(2018, Calendar.JUNE, 25, 3, 0); Date expectedDate = calendar.getTime(); assertThat(date).isEqualTo(expectedDate); } + @Test + @SuppressWarnings("UndefinedEquals") + public void testDateParseShortZeroTimezone() throws ParseException { + // timezone "+00" is extended by parser to "+0000" + String dateStr = "2018-06-25T00:00:00+00"; + Date date = ISO8601Utils.parseFully(dateStr); + GregorianCalendar calendar = createUtcCalendar(); + calendar.set(2018, Calendar.JUNE, 25, 0, 0); + Date expectedDate = calendar.getTime(); + assertThat(date).isEqualTo(expectedDate); + } + @Test public void testDateParseInvalidTime() { String dateStr = "2018-06-25T61:60:62-03:00"; - assertThrows(ParseException.class, () -> ISO8601Utils.parse(dateStr, new ParsePosition(0))); + assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr)); + } + + @Test + public void testDateParseInvalidZTimezone() { + String dateStr = "2018-06-25T00:02:00Z10"; + var e = assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr)); + assertThat(e) + .hasMessageThat() + .isEqualTo("Failed to parse date [\"" + dateStr + "\"]: Trailing data after time zone 'Z'"); + } + + @Test + public void testDateParseNoTimezoneIndicator() { + String dateStr = "2018-06-25T00:02:00"; + var e = assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr)); + assertThat(e) + .hasMessageThat() + .isEqualTo("Failed to parse date [\"" + dateStr + "\"]: No time zone indicator"); + } + + @Test + public void testDateParseInvalidTimezoneIndicator() { + String dateStr = "2018-06-25T00:02:00X"; + var e = assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr)); + assertThat(e) + .hasMessageThat() + .isEqualTo("Failed to parse date [\"" + dateStr + "\"]: Invalid time zone indicator 'X'"); } }