1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty5.handler.codec;
17
18 import io.netty5.util.AsciiString;
19 import io.netty5.util.concurrent.FastThreadLocal;
20
21 import java.util.BitSet;
22 import java.util.Calendar;
23 import java.util.Date;
24 import java.util.GregorianCalendar;
25 import java.util.TimeZone;
26
27 import static java.util.Objects.requireNonNull;
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 public final class DateFormatter {
49
50 private static final BitSet DELIMITERS = new BitSet();
51 static {
52 DELIMITERS.set(0x09);
53 for (char c = 0x20; c <= 0x2F; c++) {
54 DELIMITERS.set(c);
55 }
56 for (char c = 0x3B; c <= 0x40; c++) {
57 DELIMITERS.set(c);
58 }
59 for (char c = 0x5B; c <= 0x60; c++) {
60 DELIMITERS.set(c);
61 }
62 for (char c = 0x7B; c <= 0x7E; c++) {
63 DELIMITERS.set(c);
64 }
65 }
66
67 private static final String[] DAY_OF_WEEK_TO_SHORT_NAME =
68 new String[]{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
69
70 private static final String[] CALENDAR_MONTH_TO_SHORT_NAME =
71 new String[]{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
72
73 private static final FastThreadLocal<DateFormatter> INSTANCES =
74 new FastThreadLocal<>() {
75 @Override
76 protected DateFormatter initialValue() {
77 return new DateFormatter();
78 }
79 };
80
81
82
83
84
85
86 public static Date parseHttpDate(CharSequence txt) {
87 return parseHttpDate(txt, 0, txt.length());
88 }
89
90
91
92
93
94
95
96
97 public static Date parseHttpDate(CharSequence txt, int start, int end) {
98 int length = end - start;
99 if (length == 0) {
100 return null;
101 } else if (length < 0) {
102 throw new IllegalArgumentException("Can't have end < start");
103 } else if (length > 64) {
104 throw new IllegalArgumentException("Can't parse more than 64 chars, " +
105 "looks like a user error or a malformed header");
106 }
107 return formatter().parse0(requireNonNull(txt, "txt"), start, end);
108 }
109
110
111
112
113
114
115 public static String format(Date date) {
116 return formatter().format0(requireNonNull(date, "date"));
117 }
118
119
120
121
122
123
124
125 public static StringBuilder append(Date date, StringBuilder sb) {
126 return formatter().append0(requireNonNull(date, "date"), requireNonNull(sb, "sb"));
127 }
128
129 private static DateFormatter formatter() {
130 DateFormatter formatter = INSTANCES.get();
131 formatter.reset();
132 return formatter;
133 }
134
135
136 private static boolean isDelim(char c) {
137 return DELIMITERS.get(c);
138 }
139
140 private static boolean isDigit(char c) {
141 return c >= 48 && c <= 57;
142 }
143
144 private static int getNumericalValue(char c) {
145 return c - 48;
146 }
147
148 private final GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
149 private final StringBuilder sb = new StringBuilder(29);
150 private boolean timeFound;
151 private int hours;
152 private int minutes;
153 private int seconds;
154 private boolean dayOfMonthFound;
155 private int dayOfMonth;
156 private boolean monthFound;
157 private int month;
158 private boolean yearFound;
159 private int year;
160
161 private DateFormatter() {
162 reset();
163 }
164
165 public void reset() {
166 timeFound = false;
167 hours = -1;
168 minutes = -1;
169 seconds = -1;
170 dayOfMonthFound = false;
171 dayOfMonth = -1;
172 monthFound = false;
173 month = -1;
174 yearFound = false;
175 year = -1;
176 cal.clear();
177 sb.setLength(0);
178 }
179
180 private boolean tryParseTime(CharSequence txt, int tokenStart, int tokenEnd) {
181 int len = tokenEnd - tokenStart;
182
183
184 if (len < 5 || len > 8) {
185 return false;
186 }
187
188 int localHours = -1;
189 int localMinutes = -1;
190 int localSeconds = -1;
191 int currentPartNumber = 0;
192 int currentPartValue = 0;
193 int numDigits = 0;
194
195 for (int i = tokenStart; i < tokenEnd; i++) {
196 char c = txt.charAt(i);
197 if (isDigit(c)) {
198 currentPartValue = currentPartValue * 10 + getNumericalValue(c);
199 if (++numDigits > 2) {
200 return false;
201 }
202 } else if (c == ':') {
203 if (numDigits == 0) {
204
205 return false;
206 }
207 switch (currentPartNumber) {
208 case 0:
209
210 localHours = currentPartValue;
211 break;
212 case 1:
213
214 localMinutes = currentPartValue;
215 break;
216 default:
217
218 return false;
219 }
220 currentPartValue = 0;
221 currentPartNumber++;
222 numDigits = 0;
223 } else {
224
225 return false;
226 }
227 }
228
229 if (numDigits > 0) {
230
231 localSeconds = currentPartValue;
232 }
233
234 if (localHours >= 0 && localMinutes >= 0 && localSeconds >= 0) {
235 hours = localHours;
236 minutes = localMinutes;
237 seconds = localSeconds;
238 return true;
239 }
240
241 return false;
242 }
243
244 private boolean tryParseDayOfMonth(CharSequence txt, int tokenStart, int tokenEnd) {
245 int len = tokenEnd - tokenStart;
246
247 if (len == 1) {
248 char c0 = txt.charAt(tokenStart);
249 if (isDigit(c0)) {
250 dayOfMonth = getNumericalValue(c0);
251 return true;
252 }
253
254 } else if (len == 2) {
255 char c0 = txt.charAt(tokenStart);
256 char c1 = txt.charAt(tokenStart + 1);
257 if (isDigit(c0) && isDigit(c1)) {
258 dayOfMonth = getNumericalValue(c0) * 10 + getNumericalValue(c1);
259 return true;
260 }
261 }
262
263 return false;
264 }
265
266 private boolean tryParseMonth(CharSequence txt, int tokenStart, int tokenEnd) {
267 int len = tokenEnd - tokenStart;
268
269 if (len != 3) {
270 return false;
271 }
272
273 char monthChar1 = AsciiString.toLowerCase(txt.charAt(tokenStart));
274 char monthChar2 = AsciiString.toLowerCase(txt.charAt(tokenStart + 1));
275 char monthChar3 = AsciiString.toLowerCase(txt.charAt(tokenStart + 2));
276
277 if (monthChar1 == 'j' && monthChar2 == 'a' && monthChar3 == 'n') {
278 month = Calendar.JANUARY;
279 } else if (monthChar1 == 'f' && monthChar2 == 'e' && monthChar3 == 'b') {
280 month = Calendar.FEBRUARY;
281 } else if (monthChar1 == 'm' && monthChar2 == 'a' && monthChar3 == 'r') {
282 month = Calendar.MARCH;
283 } else if (monthChar1 == 'a' && monthChar2 == 'p' && monthChar3 == 'r') {
284 month = Calendar.APRIL;
285 } else if (monthChar1 == 'm' && monthChar2 == 'a' && monthChar3 == 'y') {
286 month = Calendar.MAY;
287 } else if (monthChar1 == 'j' && monthChar2 == 'u' && monthChar3 == 'n') {
288 month = Calendar.JUNE;
289 } else if (monthChar1 == 'j' && monthChar2 == 'u' && monthChar3 == 'l') {
290 month = Calendar.JULY;
291 } else if (monthChar1 == 'a' && monthChar2 == 'u' && monthChar3 == 'g') {
292 month = Calendar.AUGUST;
293 } else if (monthChar1 == 's' && monthChar2 == 'e' && monthChar3 == 'p') {
294 month = Calendar.SEPTEMBER;
295 } else if (monthChar1 == 'o' && monthChar2 == 'c' && monthChar3 == 't') {
296 month = Calendar.OCTOBER;
297 } else if (monthChar1 == 'n' && monthChar2 == 'o' && monthChar3 == 'v') {
298 month = Calendar.NOVEMBER;
299 } else if (monthChar1 == 'd' && monthChar2 == 'e' && monthChar3 == 'c') {
300 month = Calendar.DECEMBER;
301 } else {
302 return false;
303 }
304
305 return true;
306 }
307
308 private boolean tryParseYear(CharSequence txt, int tokenStart, int tokenEnd) {
309 int len = tokenEnd - tokenStart;
310
311 if (len == 2) {
312 char c0 = txt.charAt(tokenStart);
313 char c1 = txt.charAt(tokenStart + 1);
314 if (isDigit(c0) && isDigit(c1)) {
315 year = getNumericalValue(c0) * 10 + getNumericalValue(c1);
316 return true;
317 }
318
319 } else if (len == 4) {
320 char c0 = txt.charAt(tokenStart);
321 char c1 = txt.charAt(tokenStart + 1);
322 char c2 = txt.charAt(tokenStart + 2);
323 char c3 = txt.charAt(tokenStart + 3);
324 if (isDigit(c0) && isDigit(c1) && isDigit(c2) && isDigit(c3)) {
325 year = getNumericalValue(c0) * 1000 +
326 getNumericalValue(c1) * 100 +
327 getNumericalValue(c2) * 10 +
328 getNumericalValue(c3);
329 return true;
330 }
331 }
332
333 return false;
334 }
335
336 private boolean parseToken(CharSequence txt, int tokenStart, int tokenEnd) {
337
338 if (!timeFound) {
339 timeFound = tryParseTime(txt, tokenStart, tokenEnd);
340 if (timeFound) {
341 return dayOfMonthFound && monthFound && yearFound;
342 }
343 }
344
345 if (!dayOfMonthFound) {
346 dayOfMonthFound = tryParseDayOfMonth(txt, tokenStart, tokenEnd);
347 if (dayOfMonthFound) {
348 return timeFound && monthFound && yearFound;
349 }
350 }
351
352 if (!monthFound) {
353 monthFound = tryParseMonth(txt, tokenStart, tokenEnd);
354 if (monthFound) {
355 return timeFound && dayOfMonthFound && yearFound;
356 }
357 }
358
359 if (!yearFound) {
360 yearFound = tryParseYear(txt, tokenStart, tokenEnd);
361 }
362 return timeFound && dayOfMonthFound && monthFound && yearFound;
363 }
364
365 private Date parse0(CharSequence txt, int start, int end) {
366 boolean allPartsFound = parse1(txt, start, end);
367 return allPartsFound && normalizeAndValidate() ? computeDate() : null;
368 }
369
370 private boolean parse1(CharSequence txt, int start, int end) {
371
372 int tokenStart = -1;
373
374 for (int i = start; i < end; i++) {
375 char c = txt.charAt(i);
376
377 if (isDelim(c)) {
378 if (tokenStart != -1) {
379
380 if (parseToken(txt, tokenStart, i)) {
381 return true;
382 }
383 tokenStart = -1;
384 }
385 } else if (tokenStart == -1) {
386
387 tokenStart = i;
388 }
389 }
390
391
392 return tokenStart != -1 && parseToken(txt, tokenStart, txt.length());
393 }
394
395 private boolean normalizeAndValidate() {
396 if (dayOfMonth < 1
397 || dayOfMonth > 31
398 || hours > 23
399 || minutes > 59
400 || seconds > 59) {
401 return false;
402 }
403
404 if (year >= 70 && year <= 99) {
405 year += 1900;
406 } else if (year >= 0 && year < 70) {
407 year += 2000;
408 } else if (year < 1601) {
409
410 return false;
411 }
412 return true;
413 }
414
415 private Date computeDate() {
416 cal.set(Calendar.DAY_OF_MONTH, dayOfMonth);
417 cal.set(Calendar.MONTH, month);
418 cal.set(Calendar.YEAR, year);
419 cal.set(Calendar.HOUR_OF_DAY, hours);
420 cal.set(Calendar.MINUTE, minutes);
421 cal.set(Calendar.SECOND, seconds);
422 return cal.getTime();
423 }
424
425 private String format0(Date date) {
426 append0(date, sb);
427 return sb.toString();
428 }
429
430 private StringBuilder append0(Date date, StringBuilder sb) {
431 cal.setTime(date);
432
433 sb.append(DAY_OF_WEEK_TO_SHORT_NAME[cal.get(Calendar.DAY_OF_WEEK) - 1]).append(", ");
434 appendZeroLeftPadded(cal.get(Calendar.DAY_OF_MONTH), sb).append(' ');
435 sb.append(CALENDAR_MONTH_TO_SHORT_NAME[cal.get(Calendar.MONTH)]).append(' ');
436 sb.append(cal.get(Calendar.YEAR)).append(' ');
437 appendZeroLeftPadded(cal.get(Calendar.HOUR_OF_DAY), sb).append(':');
438 appendZeroLeftPadded(cal.get(Calendar.MINUTE), sb).append(':');
439 return appendZeroLeftPadded(cal.get(Calendar.SECOND), sb).append(" GMT");
440 }
441
442 private static StringBuilder appendZeroLeftPadded(int value, StringBuilder sb) {
443 if (value < 10) {
444 sb.append('0');
445 }
446 return sb.append(value);
447 }
448 }