Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion java/org/apache/tomcat/util/http/ConcurrentDateFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
* A thread safe wrapper around {@link SimpleDateFormat} that does not make use of ThreadLocal and - broadly - only
* creates enough SimpleDateFormat objects to satisfy the concurrency requirements.
*/
public class ConcurrentDateFormat {
public class ConcurrentDateFormat implements FastHttpDateFormat.TryParseDateToTimestamp {

private final String format;
private final Locale locale;
Expand Down Expand Up @@ -88,6 +88,15 @@ public Date parse(String source) throws ParseException {
}
}

@Override
public long tryParseDate(String dateString) {
try {
return parse(dateString).getTime();
} catch (ParseException e) {
return -1;
}
}

private SimpleDateFormat createInstance() {
SimpleDateFormat sdf = new SimpleDateFormat(format, locale);
sdf.setTimeZone(timezone);
Expand Down
35 changes: 26 additions & 9 deletions java/org/apache/tomcat/util/http/FastHttpDateFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ private FastHttpDateFormat() {
private static final ConcurrentDateFormat FORMAT_RFC5322;
private static final ConcurrentDateFormat FORMAT_OBSOLETE_RFC850;
private static final ConcurrentDateFormat FORMAT_OBSOLETE_ASCTIME;
private static final TryParseDateToTimestamp FORMAT_RFC9651;

private static final ConcurrentDateFormat[] httpParseFormats;
private static final TryParseDateToTimestamp[] httpParseFormats;

static {
// All the formats that use a timezone use GMT
Expand All @@ -60,9 +61,20 @@ private FastHttpDateFormat() {
FORMAT_RFC5322 = new ConcurrentDateFormat(DATE_RFC5322, Locale.US, tz);
FORMAT_OBSOLETE_RFC850 = new ConcurrentDateFormat(DATE_OBSOLETE_RFC850, Locale.US, tz);
FORMAT_OBSOLETE_ASCTIME = new ConcurrentDateFormat(DATE_OBSOLETE_ASCTIME, Locale.US, tz);
FORMAT_RFC9651 = dateString -> {
if(dateString == null || !dateString.startsWith("@")) {
return -1;
}
Comment thread
sabberworm marked this conversation as resolved.
try {
// An RFC 9651 timestamp is in seconds, not milliseconds.
return Long.parseLong(dateString.substring(1)) * 1_000;
} catch (NumberFormatException e) {
return -1;
}
};

httpParseFormats =
new ConcurrentDateFormat[] { FORMAT_RFC5322, FORMAT_OBSOLETE_RFC850, FORMAT_OBSOLETE_ASCTIME };
new TryParseDateToTimestamp[] { FORMAT_RFC5322, FORMAT_OBSOLETE_RFC850, FORMAT_OBSOLETE_ASCTIME, FORMAT_RFC9651 };
}

/**
Expand Down Expand Up @@ -146,13 +158,9 @@ public static long parseDate(String value) {

long date = -1;
for (int i = 0; (date == -1) && (i < httpParseFormats.length); i++) {
try {
date = httpParseFormats[i].parse(value).getTime();
updateParseCache(value, Long.valueOf(date));
} catch (ParseException e) {
// Ignore
}
date = httpParseFormats[i].tryParseDate(value);
}
updateParseCache(value, Long.valueOf(date));

@sabberworm sabberworm Aug 27, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now this is also called for values that failed to parse. Not sure if that’s better or worse. IMHO it makes sense to also cache these, since they’re the most expensive to calculate as they have to be tried (and rejected) by all the parsers.


return date;
}
Expand Down Expand Up @@ -185,5 +193,14 @@ private static void updateParseCache(String key, Long value) {
parseCache.put(key, value);
}


@FunctionalInterface
interface TryParseDateToTimestamp {
/**
* Tries to parse the given string as a date and returns it as a timestamp.
*
* @param dateString the string representation of the date trying to be parsed
* @return the number of *milli*seconds since January 1, 1970, 00:00:00 GMT or -1 if parsing failed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @return the number of *milli*seconds since January 1, 1970, 00:00:00 GMT or -1 if parsing failed
* @return the number of <em>milli</em>seconds since January 1, 1970, 00:00:00 GMT or -1 if parsing failed

*/
long tryParseDate(String dateString);
}
}
22 changes: 22 additions & 0 deletions test/org/apache/tomcat/util/http/TestFastHttpDateFormat.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
Expand Down Expand Up @@ -72,4 +73,25 @@ public void testGetCurrentDate() {
Assert.assertTrue("Saw [" + changes + "] changes in formatted date", changes > 1 && changes < 5);
}
}

@Test
public void testRfc9651Date_valid() {
Assert.assertEquals(0L, FastHttpDateFormat.parseDate("@0"));
Assert.assertEquals(1000L, FastHttpDateFormat.parseDate("@1"));
Assert.assertEquals(-1000L, FastHttpDateFormat.parseDate("@-1"));
Assert.assertEquals(1659578233000L, FastHttpDateFormat.parseDate("@1659578233")); // Example from RFC
Assert.assertEquals(-62135596800000L, FastHttpDateFormat.parseDate("@-62135596800")); // Example from RFC
Assert.assertEquals(253402214400000L, FastHttpDateFormat.parseDate("@253402214400")); // Example from RFC
}

@Test
public void testRfc9651Date_invalid() {
Assert.assertEquals(-1L, FastHttpDateFormat.parseDate("@"));
Assert.assertEquals(-1L, FastHttpDateFormat.parseDate(" @1"));
Assert.assertEquals(-1L, FastHttpDateFormat.parseDate("@ 1"));
Assert.assertEquals(-1L, FastHttpDateFormat.parseDate("@0000-"));
Assert.assertEquals(-1L, FastHttpDateFormat.parseDate("@15+"));
Assert.assertEquals(-1L, FastHttpDateFormat.parseDate("@12p"));
Assert.assertEquals(-1L, FastHttpDateFormat.parseDate("@0x15"));
}
}