Skip to content

Commit b4610a6

Browse files
committed
feat(neon_framework): add custom http date parser to support rfc822 time zone offsets
Signed-off-by: Nikolas Rimikis <leptopoda@users.noreply.github.com>
1 parent ea177b1 commit b4610a6

File tree

10 files changed

+570
-10
lines changed

10 files changed

+570
-10
lines changed

.cspell/misc.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
asctime
12
browsable
23
cleartext
34
codegen

packages/neon_framework/lib/src/utils/request_manager.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import 'package:built_value/serializer.dart';
55
import 'package:dynamite_runtime/http_client.dart';
66
import 'package:flutter/foundation.dart';
77
import 'package:http/http.dart' as http;
8-
import 'package:http_parser/http_parser.dart';
98
import 'package:logging/logging.dart';
109
import 'package:meta/meta.dart';
1110
import 'package:neon_framework/models.dart';
1211
import 'package:neon_framework/src/bloc/result.dart';
1312
import 'package:neon_framework/src/models/account.dart';
1413
import 'package:neon_framework/storage.dart';
1514
import 'package:nextcloud/nextcloud.dart';
15+
import 'package:nextcloud/utils.dart';
1616
import 'package:rxdart/rxdart.dart';
1717
import 'package:timezone/timezone.dart' as tz;
1818
import 'package:xml/xml.dart' as xml;
@@ -368,8 +368,7 @@ class CacheParameters {
368368
factory CacheParameters.parseHeaders(Map<String, dynamic> headers) {
369369
tz.TZDateTime? expiry;
370370
if (headers.containsKey('expires')) {
371-
final parsed = parseHttpDate(headers['expires']! as String);
372-
expiry = tz.TZDateTime.from(parsed, tz.UTC);
371+
expiry = parseHttpDate(headers['expires']! as String);
373372
}
374373

375374
return CacheParameters(

packages/neon_framework/pubspec.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ dependencies:
3131
flutter_zxing: ^1.0.0
3232
go_router: ^13.0.0
3333
http: ^1.0.0
34-
http_parser: ^4.0.0
3534
image: ^4.0.0
3635
intersperse: ^2.0.0
3736
intl: ^0.18.0

packages/neon_framework/test/request_manager_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import 'package:built_value/serializer.dart';
88
import 'package:dynamite_runtime/http_client.dart';
99
import 'package:flutter_test/flutter_test.dart';
1010
import 'package:http/http.dart';
11-
import 'package:http_parser/http_parser.dart';
1211
import 'package:mocktail/mocktail.dart';
1312
import 'package:neon_framework/src/bloc/result.dart';
1413
import 'package:neon_framework/src/utils/request_manager.dart';
1514
import 'package:neon_framework/testing.dart';
15+
import 'package:nextcloud/utils.dart';
1616
import 'package:rxdart/rxdart.dart';
1717
import 'package:timezone/timezone.dart' as tz;
1818

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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+
}

packages/nextcloud/lib/src/webdav/file.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import 'package:http_parser/http_parser.dart';
21
import 'package:nextcloud/src/utils/date_time.dart';
2+
import 'package:nextcloud/src/utils/http_parser.dart';
33
import 'package:nextcloud/src/webdav/client.dart';
44
import 'package:nextcloud/src/webdav/path_uri.dart';
55
import 'package:nextcloud/src/webdav/props.dart';
@@ -65,8 +65,7 @@ class WebDavFile {
6565
/// Last modified date of the file
6666
late final tz.TZDateTime? lastModified = () {
6767
if (props.davgetlastmodified != null) {
68-
final parsed = parseHttpDate(props.davgetlastmodified!);
69-
return tz.TZDateTime.from(parsed, tz.UTC);
68+
return parseHttpDate(props.davgetlastmodified!);
7069
}
7170
return null;
7271
}();

packages/nextcloud/lib/utils.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
library;
33

44
export 'src/utils/date_time.dart';
5+
export 'src/utils/http_parser.dart';

packages/nextcloud/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ dependencies:
1919
crypton: ^2.0.0
2020
dynamite_runtime: ^0.2.0
2121
http: ^1.2.0
22-
http_parser: ^4.0.0
2322
json_annotation: ^4.8.1
2423
meta: ^1.0.0
24+
string_scanner: ^1.1.0
2525
timezone: ^0.9.2
2626
universal_io: ^2.0.0
2727
uri: ^1.0.0

0 commit comments

Comments
 (0)