|
| 1 | +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 | +// for details. All rights reserved. Use of this source code is governed by a |
| 3 | +// BSD-style license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +// ignore_for_file: unnecessary_raw_strings, cascade_invocations, parameter_assignments, always_put_control_body_on_new_line |
| 6 | + |
| 7 | +import 'package:string_scanner/string_scanner.dart'; |
| 8 | +import 'package:timezone/timezone.dart' as tz; |
| 9 | + |
| 10 | +const _weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; |
| 11 | +const _months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; |
| 12 | + |
| 13 | +final _shortWeekdayRegExp = RegExp(r'Mon|Tue|Wed|Thu|Fri|Sat|Sun'); |
| 14 | +final _longWeekdayRegExp = RegExp(r'Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday'); |
| 15 | +final _monthRegExp = RegExp(r'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec'); |
| 16 | +final _digitRegExp = RegExp(r'\d+'); |
| 17 | +final _zoneRegExp = RegExp(r'[+|-]'); |
| 18 | + |
| 19 | +/// Return a HTTP-formatted string representation of [date]. |
| 20 | +/// |
| 21 | +/// This follows [RFC 822](http://tools.ietf.org/html/rfc822) as updated by |
| 22 | +/// [RFC 1123](http://tools.ietf.org/html/rfc1123). |
| 23 | +String formatHttpDate(DateTime date) { |
| 24 | + date = date.toUtc(); |
| 25 | + final buffer = StringBuffer() |
| 26 | + ..write(_weekdays[date.weekday - 1]) |
| 27 | + ..write(', ') |
| 28 | + ..write(date.day <= 9 ? '0' : '') |
| 29 | + ..write(date.day.toString()) |
| 30 | + ..write(' ') |
| 31 | + ..write(_months[date.month - 1]) |
| 32 | + ..write(' ') |
| 33 | + ..write(date.year.toString()) |
| 34 | + ..write(date.hour <= 9 ? ' 0' : ' ') |
| 35 | + ..write(date.hour.toString()) |
| 36 | + ..write(date.minute <= 9 ? ':0' : ':') |
| 37 | + ..write(date.minute.toString()) |
| 38 | + ..write(date.second <= 9 ? ':0' : ':') |
| 39 | + ..write(date.second.toString()) |
| 40 | + ..write(' GMT'); |
| 41 | + return buffer.toString(); |
| 42 | +} |
| 43 | + |
| 44 | +/// Parses an HTTP-formatted date into a UTC [DateTime]. |
| 45 | +/// |
| 46 | +/// This follows [RFC 2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3). |
| 47 | +/// It will throw a [FormatException] if [date] is invalid. |
| 48 | +tz.TZDateTime parseHttpDate(String date) { |
| 49 | + final scanner = StringScanner(date); |
| 50 | + |
| 51 | + if (scanner.scan(_longWeekdayRegExp)) { |
| 52 | + // RFC 850 starts with a long weekday. |
| 53 | + scanner.expect(', '); |
| 54 | + final day = _parseInt(scanner, 2); |
| 55 | + scanner.expect('-'); |
| 56 | + final month = _parseMonth(scanner); |
| 57 | + scanner.expect('-'); |
| 58 | + final year = 1900 + _parseInt(scanner, 2); |
| 59 | + scanner.expect(' '); |
| 60 | + final time = _parseTime(scanner); |
| 61 | + scanner.expect(' GMT'); |
| 62 | + scanner.expectDone(); |
| 63 | + |
| 64 | + return _makeDateTime(year, month, day, time); |
| 65 | + } |
| 66 | + |
| 67 | + // RFC 1123 and asctime both start with a short weekday. |
| 68 | + scanner.expect(_shortWeekdayRegExp); |
| 69 | + if (scanner.scan(', ')) { |
| 70 | + // RFC 1123 follows the weekday with a comma. |
| 71 | + final day = _parseInt(scanner, 2); |
| 72 | + scanner.expect(' '); |
| 73 | + final month = _parseMonth(scanner); |
| 74 | + scanner.expect(' '); |
| 75 | + final year = _parseInt(scanner, 4); |
| 76 | + scanner.expect(' '); |
| 77 | + final time = _parseTime(scanner); |
| 78 | + scanner.expect(' '); |
| 79 | + final offset = _parseLocation(scanner); |
| 80 | + scanner.expectDone(); |
| 81 | + |
| 82 | + return _makeDateTime(year, month, day, time, offset); |
| 83 | + } |
| 84 | + |
| 85 | + // asctime follows the weekday with a space. |
| 86 | + scanner.expect(' '); |
| 87 | + final month = _parseMonth(scanner); |
| 88 | + scanner.expect(' '); |
| 89 | + final day = scanner.scan(' ') ? _parseInt(scanner, 1) : _parseInt(scanner, 2); |
| 90 | + scanner.expect(' '); |
| 91 | + final time = _parseTime(scanner); |
| 92 | + scanner.expect(' '); |
| 93 | + final year = _parseInt(scanner, 4); |
| 94 | + scanner.expectDone(); |
| 95 | + |
| 96 | + return _makeDateTime(year, month, day, time); |
| 97 | +} |
| 98 | + |
| 99 | +/// Parses a short-form month name to a form accepted by [DateTime]. |
| 100 | +int _parseMonth(StringScanner scanner) { |
| 101 | + scanner.expect(_monthRegExp); |
| 102 | + // DateTime uses 1-indexed months. |
| 103 | + return _months.indexOf(scanner.lastMatch![0]!) + 1; |
| 104 | +} |
| 105 | + |
| 106 | +/// Parses an int an enforces that it has exactly [digits] digits. |
| 107 | +int _parseInt(StringScanner scanner, int digits) { |
| 108 | + scanner.expect(_digitRegExp); |
| 109 | + if (scanner.lastMatch![0]!.length != digits) { |
| 110 | + scanner.error('expected a $digits-digit number.'); |
| 111 | + } |
| 112 | + |
| 113 | + return int.parse(scanner.lastMatch![0]!); |
| 114 | +} |
| 115 | + |
| 116 | +/// Parses an timestamp of the form "HH:MM:SS" on a 24-hour clock. |
| 117 | +tz.TZDateTime _parseTime(StringScanner scanner) { |
| 118 | + final hours = _parseInt(scanner, 2); |
| 119 | + if (hours >= 24) scanner.error('hours may not be greater than 24.'); |
| 120 | + scanner.expect(':'); |
| 121 | + |
| 122 | + final minutes = _parseInt(scanner, 2); |
| 123 | + if (minutes >= 60) scanner.error('minutes may not be greater than 60.'); |
| 124 | + scanner.expect(':'); |
| 125 | + |
| 126 | + final seconds = _parseInt(scanner, 2); |
| 127 | + if (seconds >= 60) scanner.error('seconds may not be greater than 60.'); |
| 128 | + |
| 129 | + return tz.TZDateTime.utc(1, 1, 1, hours, minutes, seconds); |
| 130 | +} |
| 131 | + |
| 132 | +Duration? _parseLocation(StringScanner scanner) { |
| 133 | + if (scanner.scan('GMT')) { |
| 134 | + return null; |
| 135 | + } else if (scanner.scan(_zoneRegExp)) { |
| 136 | + final modifier = scanner.lastMatch![0]!; |
| 137 | + |
| 138 | + scanner.expect(RegExp(r'\d{2}')); |
| 139 | + final hours = int.parse(scanner.lastMatch![0]!); |
| 140 | + |
| 141 | + scanner.expect(RegExp(r'\d{2}')); |
| 142 | + final minutes = int.parse(scanner.lastMatch![0]!); |
| 143 | + |
| 144 | + if (hours >= 99 && minutes > 59) { |
| 145 | + throw FormatException("invalid timezone offset '$hours$minutes'."); |
| 146 | + } |
| 147 | + |
| 148 | + var offset = Duration(hours: hours, minutes: minutes); |
| 149 | + if (modifier == '-') { |
| 150 | + offset *= -1; |
| 151 | + } |
| 152 | + |
| 153 | + return offset; |
| 154 | + } else { |
| 155 | + throw const FormatException('Parsing timezone can not be done unambiguously.'); |
| 156 | + } |
| 157 | +} |
| 158 | + |
| 159 | +/// Returns a UTC [tz.TZDateTime] from the given components. |
| 160 | +/// |
| 161 | +/// Validates that [day] is a valid day for [month]. If it's not, throws a |
| 162 | +/// [FormatException]. |
| 163 | +tz.TZDateTime _makeDateTime(int year, int month, int day, tz.TZDateTime time, [Duration? offset]) { |
| 164 | + var dateTime = tz.TZDateTime(tz.UTC, year, month, day, time.hour, time.minute, time.second); |
| 165 | + if (offset != null) { |
| 166 | + dateTime = dateTime.add(offset); |
| 167 | + } |
| 168 | + |
| 169 | + // If [day] was too large, it will cause [month] to overflow. |
| 170 | + if (dateTime.month != month) { |
| 171 | + throw FormatException("invalid day '$day' for month '$month'."); |
| 172 | + } |
| 173 | + return dateTime; |
| 174 | +} |
0 commit comments