Migrated range_header

This commit is contained in:
thomashii@dukefirehawk.com 2021-05-01 11:39:09 +08:00
parent 1e92db46a6
commit 3b2fc97da1
11 changed files with 82 additions and 73 deletions

View file

@ -26,7 +26,7 @@
* Migrated production to 3.0.0 (0/0 tests passed) * Migrated production to 3.0.0 (0/0 tests passed)
* Added html_builder and migrated to 2.0.0 (1/1 tests passed) * Added html_builder and migrated to 2.0.0 (1/1 tests passed)
* Migrated hot to 4.0.0 (0/0 tests passed) * Migrated hot to 4.0.0 (0/0 tests passed)
* Added range_header and migrated to 2.0.0 (16/16 tests passed) * Added range_header and migrated to 3.0.0 (12/12 tests passed)
* Updated static to 3.0.0 (in progress) * Updated static to 3.0.0 (in progress)
* Update basic-sdk-2.12.x boilerplate (in progress) * Update basic-sdk-2.12.x boilerplate (in progress)

View file

@ -6,7 +6,7 @@ var file = new File('some_video.mp4');
handleRequest(HttpRequest request) async { handleRequest(HttpRequest request) async {
// Parse the header // Parse the header
var header = var header =
new RangeHeader.parse(request.headers.value(HttpHeaders.rangeHeader)); new RangeHeader.parse(request.headers.value(HttpHeaders.rangeHeader)!);
// Optimize/canonicalize it // Optimize/canonicalize it
var items = RangeHeader.foldItems(header.items); var items = RangeHeader.foldItems(header.items);

View file

@ -1,16 +1,9 @@
import 'dart:async'; //import 'package:angel_framework/angel_framework.dart';
import 'dart:io' show HttpHeaders, HttpStatus; //import 'package:angel_framework/http.dart';
import 'dart:typed_data'; //import 'package:angel_static/angel_static.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_static/angel_static.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:http_parser/http_parser.dart';
import 'package:logging/logging.dart';
import 'package:range_header/range_header.dart';
main() async { main() async {
/*
var app = new Angel(); var app = new Angel();
var http = new AngelHttp(app); var http = new AngelHttp(app);
var fs = const LocalFileSystem(); var fs = const LocalFileSystem();
@ -32,8 +25,9 @@ main() async {
app.fallback((req, res) => throw new AngelHttpException.notFound()); app.fallback((req, res) => throw new AngelHttpException.notFound());
await http.startServer('127.0.0.1', 3000); await http.startServer('127.0.0.1', 3000);
print('Listening at ${http.uri}'); print('Listening at ${http.uri}');
*/
} }
/*
class _RangingVirtualDirectory extends VirtualDirectory { class _RangingVirtualDirectory extends VirtualDirectory {
_RangingVirtualDirectory(Angel app, Directory source) _RangingVirtualDirectory(Angel app, Directory source)
: super(app, source.fileSystem, : super(app, source.fileSystem,
@ -106,3 +100,4 @@ class _RangingVirtualDirectory extends VirtualDirectory {
} }
} }
} }
*/

View file

@ -16,9 +16,9 @@ class RangeHeaderTransformer
final int totalLength; final int totalLength;
RangeHeaderTransformer(this.header, this.mimeType, this.totalLength, RangeHeaderTransformer(this.header, this.mimeType, this.totalLength,
{String boundary}) {String? boundary})
: this.boundary = boundary ?? _randomString() { : this.boundary = boundary ?? _randomString() {
if (header == null || header.items.isEmpty) { if (header.items.isEmpty) {
throw new ArgumentError('`header` cannot be null or empty.'); throw new ArgumentError('`header` cannot be null or empty.');
} }
} }

View file

@ -6,3 +6,12 @@ class RangeHeaderParseException extends FormatException {
@override @override
String toString() => 'Range header parse exception: $message'; String toString() => 'Range header parse exception: $message';
} }
class InvalidRangeHeaderException implements Exception {
final String message;
InvalidRangeHeaderException(this.message);
@override
String toString() => 'Range header parse exception: $message';
}

View file

@ -13,7 +13,7 @@ enum TokenType { RANGE_UNIT, COMMA, INT, DASH, EQUALS }
class Token { class Token {
final TokenType type; final TokenType type;
final SourceSpan span; final SourceSpan? span;
Token(this.type, this.span); Token(this.type, this.span);
} }
@ -57,31 +57,31 @@ List<Token> scan(String text, List<String> allowedRangeUnits) {
} }
class Parser { class Parser {
Token _current; Token? _current;
int _index = -1; int _index = -1;
final List<Token> tokens; final List<Token> tokens;
Parser(this.tokens); Parser(this.tokens);
Token get current => _current; Token? get current => _current;
bool get done => _index >= tokens.length - 1; bool get done => _index >= tokens.length - 1;
RangeHeaderParseException _expected(String type) { RangeHeaderParseException _expected(String type) {
int offset = current?.span?.start?.offset; int? offset = current?.span?.start.offset;
if (offset == null) return new RangeHeaderParseException('Expected $type.'); if (offset == null) return new RangeHeaderParseException('Expected $type.');
Token peek; Token? peek;
if (_index < tokens.length - 1) peek = tokens[_index + 1]; if (_index < tokens.length - 1) peek = tokens[_index + 1];
if (peek != null && peek.span != null) { if (peek != null && peek.span != null) {
return new RangeHeaderParseException( return new RangeHeaderParseException(
'Expected $type at offset $offset, found "${peek.span.text}" instead. \nSource:\n${peek.span?.highlight() ?? peek.type}'); 'Expected $type at offset $offset, found "${peek.span!.text}" instead. \nSource:\n${peek.span?.highlight() ?? peek.type}');
} else } else
return new RangeHeaderParseException( return new RangeHeaderParseException(
'Expected $type at offset $offset, but the header string ended without one.\nSource:\n${current.span?.highlight() ?? current.type}'); 'Expected $type at offset $offset, but the header string ended without one.\nSource:\n${current!.span?.highlight() ?? current!.type}');
} }
bool next(TokenType type) { bool next(TokenType type) {
@ -95,13 +95,13 @@ class Parser {
return false; return false;
} }
RangeHeader parseRangeHeader() { RangeHeader? parseRangeHeader() {
if (next(TokenType.RANGE_UNIT)) { if (next(TokenType.RANGE_UNIT)) {
var unit = current.span.text; var unit = current!.span!.text;
next(TokenType.EQUALS); // Consume =, if any. next(TokenType.EQUALS); // Consume =, if any.
List<RangeHeaderItem> items = []; List<RangeHeaderItem> items = [];
RangeHeaderItem item = parseHeaderItem(); RangeHeaderItem? item = parseHeaderItem();
while (item != null) { while (item != null) {
items.add(item); items.add(item);
@ -120,13 +120,13 @@ class Parser {
return null; return null;
} }
RangeHeaderItem parseHeaderItem() { RangeHeaderItem? parseHeaderItem() {
if (next(TokenType.INT)) { if (next(TokenType.INT)) {
// i.e 500-544, or 600- // i.e 500-544, or 600-
var start = int.parse(current.span.text); var start = int.parse(current!.span!.text);
if (next(TokenType.DASH)) { if (next(TokenType.DASH)) {
if (next(TokenType.INT)) { if (next(TokenType.INT)) {
return new RangeHeaderItem(start, int.parse(current.span.text)); return new RangeHeaderItem(start, int.parse(current!.span!.text));
} else } else
return new RangeHeaderItem(start); return new RangeHeaderItem(start);
} else } else
@ -134,7 +134,7 @@ class Parser {
} else if (next(TokenType.DASH)) { } else if (next(TokenType.DASH)) {
// i.e. -599 // i.e. -599
if (next(TokenType.INT)) { if (next(TokenType.INT)) {
return new RangeHeaderItem(-1, int.parse(current.span.text)); return new RangeHeaderItem(-1, int.parse(current!.span!.text));
} else } else
throw _expected('integer'); throw _expected('integer');
} else } else

View file

@ -1,4 +1,5 @@
import 'dart:collection'; import 'dart:collection';
import 'exception.dart';
import 'parser.dart'; import 'parser.dart';
import 'range_header_item.dart'; import 'range_header_item.dart';
import 'range_header_impl.dart'; import 'range_header_impl.dart';
@ -9,12 +10,12 @@ abstract class RangeHeader {
UnmodifiableListView<RangeHeaderItem> get items; UnmodifiableListView<RangeHeaderItem> get items;
const factory RangeHeader(Iterable<RangeHeaderItem> items, const factory RangeHeader(Iterable<RangeHeaderItem> items,
{String rangeUnit}) = _ConstantRangeHeader; {String? rangeUnit}) = _ConstantRangeHeader;
/// Eliminates any overlapping [items], sorts them, and folds them all into the most efficient representation possible. /// Eliminates any overlapping [items], sorts them, and folds them all into the most efficient representation possible.
static UnmodifiableListView<RangeHeaderItem> foldItems( static UnmodifiableListView<RangeHeaderItem> foldItems(
Iterable<RangeHeaderItem> items) { Iterable<RangeHeaderItem> items) {
var out = new Set<RangeHeaderItem>(); var out = Set<RangeHeaderItem>();
for (var item in items) { for (var item in items) {
// Remove any overlapping items, consolidate them. // Remove any overlapping items, consolidate them.
@ -37,27 +38,30 @@ abstract class RangeHeader {
/// ///
/// If [fold] is `true`, the items will be folded into the most compact /// If [fold] is `true`, the items will be folded into the most compact
/// possible representation. /// possible representation.
///
factory RangeHeader.parse(String text, factory RangeHeader.parse(String text,
{Iterable<String> allowedRangeUnits, bool fold: true}) { {Iterable<String>? allowedRangeUnits, bool fold: true}) {
var tokens = scan(text, allowedRangeUnits?.toList() ?? ['bytes']); var tokens = scan(text, allowedRangeUnits?.toList() ?? ['bytes']);
var parser = new Parser(tokens); var parser = new Parser(tokens);
var header = parser.parseRangeHeader(); var header = parser.parseRangeHeader();
if (header == null) return null; if (header == null) {
throw InvalidRangeHeaderException('Header is null');
}
var items = foldItems(header.items); var items = foldItems(header.items);
return RangeHeaderImpl(header.rangeUnit, items); return RangeHeaderImpl(header.rangeUnit, items);
} }
/// Returns this header's range unit. Most commonly, this is `bytes`. /// Returns this header's range unit. Most commonly, this is `bytes`.
String get rangeUnit; String? get rangeUnit;
} }
class _ConstantRangeHeader implements RangeHeader { class _ConstantRangeHeader implements RangeHeader {
final Iterable<RangeHeaderItem> items_; final Iterable<RangeHeaderItem> items_;
final String rangeUnit; final String? rangeUnit;
const _ConstantRangeHeader(this.items_, {this.rangeUnit: 'bytes'}); const _ConstantRangeHeader(this.items_, {this.rangeUnit: 'bytes'});
@override @override
UnmodifiableListView<RangeHeaderItem> get items => UnmodifiableListView<RangeHeaderItem> get items =>
new UnmodifiableListView(items_); UnmodifiableListView(items_);
} }

View file

@ -4,11 +4,11 @@ import 'range_header_item.dart';
/// Represents the contents of a parsed `Range` header. /// Represents the contents of a parsed `Range` header.
class RangeHeaderImpl implements RangeHeader { class RangeHeaderImpl implements RangeHeader {
UnmodifiableListView<RangeHeaderItem> _cached; UnmodifiableListView<RangeHeaderItem>? _cached;
final List<RangeHeaderItem> _items = []; final List<RangeHeaderItem> _items = [];
RangeHeaderImpl(this.rangeUnit, [List<RangeHeaderItem> items = const []]) { RangeHeaderImpl(this.rangeUnit, [List<RangeHeaderItem> items = const []]) {
this._items.addAll(items ?? []); this._items.addAll(items);
} }
@override @override
@ -16,5 +16,5 @@ class RangeHeaderImpl implements RangeHeader {
_cached ??= new UnmodifiableListView<RangeHeaderItem>(_items); _cached ??= new UnmodifiableListView<RangeHeaderItem>(_items);
@override @override
final String rangeUnit; final String? rangeUnit;
} }

View file

@ -71,7 +71,7 @@ class RangeHeaderItem implements Comparable<RangeHeaderItem> {
/// Please adhere to the standard!!! /// Please adhere to the standard!!!
/// http://httpwg.org/specs/rfc7233.html /// http://httpwg.org/specs/rfc7233.html
String toContentRange([int totalSize]) { String toContentRange([int? totalSize]) {
// var maxIndex = totalSize != null ? (totalSize - 1).toString() : '*'; // var maxIndex = totalSize != null ? (totalSize - 1).toString() : '*';
var s = start > -1 ? start : 0; var s = start > -1 ? start : 0;

View file

@ -1,20 +1,20 @@
name: range_header name: range_header
version: 2.0.2+2 version: 3.0.0
description: Range header parser for Dart. Beyond parsing, a stream transformer is included. description: Range header parser for Dart. Beyond parsing, a stream transformer is included.
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/thosakwe/range_header homepage: https://github.com/thosakwe/range_header
environment: environment:
sdk: ">=2.0.0-dev <3.0.0" sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
async: ^2.0.0 async: ^2.6.0
charcode: ^1.0.0 charcode: ^1.2.0
quiver_hashcode: ^2.0.0 quiver_hashcode: ^3.0.0+1
source_span: ^1.0.0 source_span: ^1.8.1
string_scanner: ^1.0.0 string_scanner: ^1.1.0
dev_dependencies: dev_dependencies:
angel_framework: #angel_framework:
angel_static: ^2.0.0 #angel_static: ^2.0.0
file: file: ^6.1.0
http_parser: ^3.0.0 http_parser: ^4.0.0
logging: ^0.11.0 logging: ^1.0.1
test: ^1.0.0 test: ^1.17.3

View file

@ -1,27 +1,32 @@
import 'package:range_header/range_header.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../lib/range_header.dart';
import '../lib/src/range_header.dart';
final Matcher throwsRangeParseException = final Matcher throwsRangeParseException =
throwsA(const TypeMatcher<RangeHeaderParseException>()); throwsA(const TypeMatcher<RangeHeaderParseException>());
final Matcher throwsInvalidRangeHeaderException =
throwsA(const TypeMatcher<InvalidRangeHeaderException>());
main() { main() {
group('one item', () { group('one item', () {
test('start and end', () { test('start and end', () {
var r = new RangeHeader.parse('bytes 1-200'); var r = RangeHeader.parse('bytes 1-200');
expect(r.items, hasLength(1)); expect(r.items, hasLength(1));
expect(r.items.first.start, 1); expect(r.items.first.start, 1);
expect(r.items.first.end, 200); expect(r.items.first.end, 200);
}); });
test('start only', () { test('start only', () {
var r = new RangeHeader.parse('bytes 1-'); var r = RangeHeader.parse('bytes 1-');
expect(r.items, hasLength(1)); expect(r.items, hasLength(1));
expect(r.items.first.start, 1); expect(r.items.first.start, 1);
expect(r.items.first.end, -1); expect(r.items.first.end, -1);
}); });
test('end only', () { test('end only', () {
var r = new RangeHeader.parse('bytes -200'); var r = RangeHeader.parse('bytes -200');
print(r.items); print(r.items);
expect(r.items, hasLength(1)); expect(r.items, hasLength(1));
expect(r.items.first.start, -1); expect(r.items.first.start, -1);
@ -31,7 +36,7 @@ main() {
group('multiple items', () { group('multiple items', () {
test('three items', () { test('three items', () {
var r = new RangeHeader.parse('bytes 1-20, 21-40, 41-60'); var r = RangeHeader.parse('bytes 1-20, 21-40, 41-60');
print(r.items); print(r.items);
expect(r.items, hasLength(3)); expect(r.items, hasLength(3));
expect(r.items[0].start, 1); expect(r.items[0].start, 1);
@ -43,7 +48,7 @@ main() {
}); });
test('one item without end', () { test('one item without end', () {
var r = new RangeHeader.parse('bytes 1-20, 21-'); var r = RangeHeader.parse('bytes 1-20, 21-');
print(r.items); print(r.items);
expect(r.items, hasLength(2)); expect(r.items, hasLength(2));
expect(r.items[0].start, 1); expect(r.items[0].start, 1);
@ -55,42 +60,38 @@ main() {
group('failures', () { group('failures', () {
test('no start with no end', () { test('no start with no end', () {
expect(new RangeHeader.parse('-'), isNull); expect(() => RangeHeader.parse('-'), throwsInvalidRangeHeaderException);
}); });
}); });
group('exceptions', () { group('exceptions', () {
test('invalid character', () { test('invalid character', () {
expect(() => new RangeHeader.parse('!!!'), throwsRangeParseException); expect(() => RangeHeader.parse('!!!'), throwsRangeParseException);
}); });
test('no ranges', () { test('no ranges', () {
expect(() => new RangeHeader.parse('bytes'), throwsRangeParseException); expect(() => RangeHeader.parse('bytes'), throwsRangeParseException);
}); });
test('no dash after int', () { test('no dash after int', () {
expect(() => new RangeHeader.parse('bytes 3'), throwsRangeParseException); expect(() => RangeHeader.parse('bytes 3'), throwsRangeParseException);
expect( expect(() => RangeHeader.parse('bytes 3,'), throwsRangeParseException);
() => new RangeHeader.parse('bytes 3,'), throwsRangeParseException); expect(() => RangeHeader.parse('bytes 3 24'), throwsRangeParseException);
expect(
() => new RangeHeader.parse('bytes 3 24'), throwsRangeParseException);
}); });
test('no int after dash', () { test('no int after dash', () {
expect( expect(() => RangeHeader.parse('bytes -,'), throwsRangeParseException);
() => new RangeHeader.parse('bytes -,'), throwsRangeParseException);
}); });
}); });
group('complete coverage', () { group('complete coverage', () {
test('exception toString()', () { test('exception toString()', () {
var m = new RangeHeaderParseException('hey'); var m = RangeHeaderParseException('hey');
expect(m.toString(), contains('hey')); expect(m.toString(), contains('hey'));
}); });
}); });
test('content-range', () { test('content-range', () {
expect( expect(RangeHeader.parse('bytes 1-2').items[0].toContentRange(3), '1-2/3');
new RangeHeader.parse('bytes 1-2').items[0].toContentRange(3), '1-2/3');
}); });
} }