netty5/codec/src/main/java/io/netty/handler/codec/DateFormatter.java

447 lines
14 KiB
Java

/*
* Copyright 2016 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.codec;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.util.AsciiString;
import io.netty.util.concurrent.FastThreadLocal;
import java.util.BitSet;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
/**
* A formatter for HTTP header dates, such as "Expires" and "Date" headers, or "expires" field in "Set-Cookie".
*
* On the parsing side, it honors RFC6265 (so it supports RFC1123).
* Note that:
* <ul>
* <li>Day of week is ignored and not validated</li>
* <li>Timezone is ignored, as RFC6265 assumes UTC</li>
* </ul>
* If you're looking for a date format that validates day of week, or supports other timezones, consider using
* java.util.DateTimeFormatter.RFC_1123_DATE_TIME.
*
* On the formatting side, it uses RFC1123 format.
*
* @see <a href="https://tools.ietf.org/html/rfc6265#section-5.1.1">RFC6265</a> for the parsing side
* @see <a href="https://tools.ietf.org/html/rfc1123#page-55">RFC1123</a> for the encoding side.
*/
public final class DateFormatter {
private static final BitSet DELIMITERS = new BitSet();
static {
DELIMITERS.set(0x09);
for (char c = 0x20; c <= 0x2F; c++) {
DELIMITERS.set(c);
}
for (char c = 0x3B; c <= 0x40; c++) {
DELIMITERS.set(c);
}
for (char c = 0x5B; c <= 0x60; c++) {
DELIMITERS.set(c);
}
for (char c = 0x7B; c <= 0x7E; c++) {
DELIMITERS.set(c);
}
}
private static final String[] DAY_OF_WEEK_TO_SHORT_NAME =
new String[]{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
private static final String[] CALENDAR_MONTH_TO_SHORT_NAME =
new String[]{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
private static final FastThreadLocal<DateFormatter> INSTANCES =
new FastThreadLocal<DateFormatter>() {
@Override
protected DateFormatter initialValue() {
return new DateFormatter();
}
};
/**
* Parse some text into a {@link Date}, according to RFC6265
* @param txt text to parse
* @return a {@link Date}, or null if text couldn't be parsed
*/
public static Date parseHttpDate(CharSequence txt) {
return parseHttpDate(txt, 0, txt.length());
}
/**
* Parse some text into a {@link Date}, according to RFC6265
* @param txt text to parse
* @param start the start index inside <code>txt</code>
* @param end the end index inside <code>txt</code>
* @return a {@link Date}, or null if text couldn't be parsed
*/
public static Date parseHttpDate(CharSequence txt, int start, int end) {
int length = end - start;
if (length == 0) {
return null;
} else if (length < 0) {
throw new IllegalArgumentException("Can't have end < start");
} else if (length > 64) {
throw new IllegalArgumentException("Can't parse more than 64 chars," +
"looks like a user error or a malformed header");
}
return formatter().parse0(checkNotNull(txt, "txt"), start, end);
}
/**
* Format a {@link Date} into RFC1123 format
* @param date the date to format
* @return a RFC1123 string
*/
public static String format(Date date) {
return formatter().format0(checkNotNull(date, "date"));
}
/**
* Append a {@link Date} to a {@link StringBuilder} into RFC1123 format
* @param date the date to format
* @param sb the StringBuilder
* @return the same StringBuilder
*/
public static StringBuilder append(Date date, StringBuilder sb) {
return formatter().append0(checkNotNull(date, "date"), checkNotNull(sb, "sb"));
}
private static DateFormatter formatter() {
DateFormatter formatter = INSTANCES.get();
formatter.reset();
return formatter;
}
// delimiter = %x09 / %x20-2F / %x3B-40 / %x5B-60 / %x7B-7E
private static boolean isDelim(char c) {
return DELIMITERS.get(c);
}
private static boolean isDigit(char c) {
return c >= 48 && c <= 57;
}
private static int getNumericalValue(char c) {
return c - 48;
}
private final GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
private final StringBuilder sb = new StringBuilder(29); // Sun, 27 Nov 2016 19:37:15 GMT
private boolean timeFound;
private int hours;
private int minutes;
private int seconds;
private boolean dayOfMonthFound;
private int dayOfMonth;
private boolean monthFound;
private int month;
private boolean yearFound;
private int year;
private DateFormatter() {
reset();
}
public void reset() {
timeFound = false;
hours = -1;
minutes = -1;
seconds = -1;
dayOfMonthFound = false;
dayOfMonth = -1;
monthFound = false;
month = -1;
yearFound = false;
year = -1;
cal.clear();
sb.setLength(0);
}
private boolean tryParseTime(CharSequence txt, int tokenStart, int tokenEnd) {
int len = tokenEnd - tokenStart;
// h:m:s to hh:mm:ss
if (len < 5 || len > 8) {
return false;
}
int localHours = -1;
int localMinutes = -1;
int localSeconds = -1;
int currentPartNumber = 0;
int currentPartValue = 0;
int numDigits = 0;
for (int i = tokenStart; i < tokenEnd; i++) {
char c = txt.charAt(i);
if (isDigit(c)) {
currentPartValue = currentPartValue * 10 + getNumericalValue(c);
if (++numDigits > 2) {
return false; // too many digits in this part
}
} else if (c == ':') {
if (numDigits == 0) {
// no digits between separators
return false;
}
switch (currentPartNumber) {
case 0:
// flushing hours
localHours = currentPartValue;
break;
case 1:
// flushing minutes
localMinutes = currentPartValue;
break;
default:
// invalid, too many :
return false;
}
currentPartValue = 0;
currentPartNumber++;
numDigits = 0;
} else {
// invalid char
return false;
}
}
if (numDigits > 0) {
// pending seconds
localSeconds = currentPartValue;
}
if (localHours >= 0 && localMinutes >= 0 && localSeconds >= 0) {
hours = localHours;
minutes = localMinutes;
seconds = localSeconds;
return true;
}
return false;
}
private boolean tryParseDayOfMonth(CharSequence txt, int tokenStart, int tokenEnd) {
int len = tokenEnd - tokenStart;
if (len == 1) {
char c0 = txt.charAt(tokenStart);
if (isDigit(c0)) {
dayOfMonth = getNumericalValue(c0);
return true;
}
} else if (len == 2) {
char c0 = txt.charAt(tokenStart);
char c1 = txt.charAt(tokenStart + 1);
if (isDigit(c0) && isDigit(c1)) {
dayOfMonth = getNumericalValue(c0) * 10 + getNumericalValue(c1);
return true;
}
}
return false;
}
private static boolean matchMonth(String month, CharSequence txt, int tokenStart) {
return AsciiString.regionMatchesAscii(month, true, 0, txt, tokenStart, 3);
}
private boolean tryParseMonth(CharSequence txt, int tokenStart, int tokenEnd) {
int len = tokenEnd - tokenStart;
if (len != 3) {
return false;
}
if (matchMonth("Jan", txt, tokenStart)) {
month = Calendar.JANUARY;
} else if (matchMonth("Feb", txt, tokenStart)) {
month = Calendar.FEBRUARY;
} else if (matchMonth("Mar", txt, tokenStart)) {
month = Calendar.MARCH;
} else if (matchMonth("Apr", txt, tokenStart)) {
month = Calendar.APRIL;
} else if (matchMonth("May", txt, tokenStart)) {
month = Calendar.MAY;
} else if (matchMonth("Jun", txt, tokenStart)) {
month = Calendar.JUNE;
} else if (matchMonth("Jul", txt, tokenStart)) {
month = Calendar.JULY;
} else if (matchMonth("Aug", txt, tokenStart)) {
month = Calendar.AUGUST;
} else if (matchMonth("Sep", txt, tokenStart)) {
month = Calendar.SEPTEMBER;
} else if (matchMonth("Oct", txt, tokenStart)) {
month = Calendar.OCTOBER;
} else if (matchMonth("Nov", txt, tokenStart)) {
month = Calendar.NOVEMBER;
} else if (matchMonth("Dec", txt, tokenStart)) {
month = Calendar.DECEMBER;
} else {
return false;
}
return true;
}
private boolean tryParseYear(CharSequence txt, int tokenStart, int tokenEnd) {
int len = tokenEnd - tokenStart;
if (len == 2) {
char c0 = txt.charAt(tokenStart);
char c1 = txt.charAt(tokenStart + 1);
if (isDigit(c0) && isDigit(c1)) {
year = getNumericalValue(c0) * 10 + getNumericalValue(c1);
return true;
}
} else if (len == 4) {
char c0 = txt.charAt(tokenStart);
char c1 = txt.charAt(tokenStart + 1);
char c2 = txt.charAt(tokenStart + 2);
char c3 = txt.charAt(tokenStart + 3);
if (isDigit(c0) && isDigit(c1) && isDigit(c2) && isDigit(c3)) {
year = getNumericalValue(c0) * 1000 +
getNumericalValue(c1) * 100 +
getNumericalValue(c2) * 10 +
getNumericalValue(c3);
return true;
}
}
return false;
}
private boolean parseToken(CharSequence txt, int tokenStart, int tokenEnd) {
// return true if all parts are found
if (!timeFound) {
timeFound = tryParseTime(txt, tokenStart, tokenEnd);
if (timeFound) {
return dayOfMonthFound && monthFound && yearFound;
}
}
if (!dayOfMonthFound) {
dayOfMonthFound = tryParseDayOfMonth(txt, tokenStart, tokenEnd);
if (dayOfMonthFound) {
return timeFound && monthFound && yearFound;
}
}
if (!monthFound) {
monthFound = tryParseMonth(txt, tokenStart, tokenEnd);
if (monthFound) {
return timeFound && dayOfMonthFound && yearFound;
}
}
if (!yearFound) {
yearFound = tryParseYear(txt, tokenStart, tokenEnd);
}
return timeFound && dayOfMonthFound && monthFound && yearFound;
}
private Date parse0(CharSequence txt, int start, int end) {
boolean allPartsFound = parse1(txt, start, end);
return allPartsFound && normalizeAndValidate() ? computeDate() : null;
}
private boolean parse1(CharSequence txt, int start, int end) {
// return true if all parts are found
int tokenStart = -1;
for (int i = start; i < end; i++) {
char c = txt.charAt(i);
if (isDelim(c)) {
if (tokenStart != -1) {
// terminate token
if (parseToken(txt, tokenStart, i)) {
return true;
}
tokenStart = -1;
}
} else if (tokenStart == -1) {
// start new token
tokenStart = i;
}
}
// terminate trailing token
return tokenStart != -1 && parseToken(txt, tokenStart, txt.length());
}
private boolean normalizeAndValidate() {
if (dayOfMonth < 1
|| dayOfMonth > 31
|| hours > 23
|| minutes > 59
|| seconds > 59) {
return false;
}
if (year >= 70 && year <= 99) {
year += 1900;
} else if (year >= 0 && year < 70) {
year += 2000;
} else if (year < 1601) {
// invalid value
return false;
}
return true;
}
private Date computeDate() {
cal.set(Calendar.DAY_OF_MONTH, dayOfMonth);
cal.set(Calendar.MONTH, month);
cal.set(Calendar.YEAR, year);
cal.set(Calendar.HOUR_OF_DAY, hours);
cal.set(Calendar.MINUTE, minutes);
cal.set(Calendar.SECOND, seconds);
return cal.getTime();
}
private String format0(Date date) {
append0(date, sb);
return sb.toString();
}
private StringBuilder append0(Date date, StringBuilder sb) {
cal.setTime(date);
sb.append(DAY_OF_WEEK_TO_SHORT_NAME[cal.get(Calendar.DAY_OF_WEEK) - 1]).append(", ");
sb.append(cal.get(Calendar.DAY_OF_MONTH)).append(' ');
sb.append(CALENDAR_MONTH_TO_SHORT_NAME[cal.get(Calendar.MONTH)]).append(' ');
sb.append(cal.get(Calendar.YEAR)).append(' ');
appendZeroLeftPadded(cal.get(Calendar.HOUR_OF_DAY), sb).append(':');
appendZeroLeftPadded(cal.get(Calendar.MINUTE), sb).append(':');
return appendZeroLeftPadded(cal.get(Calendar.SECOND), sb).append(" GMT");
}
private static StringBuilder appendZeroLeftPadded(int value, StringBuilder sb) {
if (value < 10) {
sb.append('0');
}
return sb.append(value);
}
}