Updated range_header

This commit is contained in:
thomashii 2021-05-18 19:58:51 +08:00
parent 27d739864c
commit bb6fa4bec1
13 changed files with 99 additions and 77 deletions

View file

@ -1,3 +1,6 @@
# 3.0.1
* Resolve static analysis warnings
# 3.0.0 # 3.0.0
* Migrated to work with Dart SDK 2.12.x NNBD * Migrated to work with Dart SDK 2.12.x NNBD

View file

@ -1,5 +1,5 @@
# angel3_range_header # angel3_range_header
[![version](https://img.shields.io/badge/pub-v3.0.0-brightgreen)](https://pub.dartlang.org/packages/angel3_range_header) [![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dartlang.org/packages/angel3_range_header)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety) [![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion) [![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)

View file

@ -1,3 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer: analyzer:
strong-mode: strong-mode:
implicit-casts: false implicit-casts: false

View file

@ -3,7 +3,7 @@ import 'package:angel3_range_header/angel3_range_header.dart';
var file = File('some_video.mp4'); var file = File('some_video.mp4');
handleRequest(HttpRequest request) async { void handleRequest(HttpRequest request) async {
// Parse the header // Parse the header
var header = var header =
RangeHeader.parse(request.headers.value(HttpHeaders.rangeHeader)!); RangeHeader.parse(request.headers.value(HttpHeaders.rangeHeader)!);

View file

@ -2,7 +2,7 @@
//import 'package:angel_framework/http.dart'; //import 'package:angel_framework/http.dart';
//import 'package:angel_static/angel_static.dart'; //import 'package:angel_static/angel_static.dart';
main() async { void main() async {
/* /*
var app = new Angel(); var app = new Angel();
var http = new AngelHttp(app); var http = new AngelHttp(app);

View file

@ -17,15 +17,15 @@ class RangeHeaderTransformer
RangeHeaderTransformer(this.header, this.mimeType, this.totalLength, RangeHeaderTransformer(this.header, this.mimeType, this.totalLength,
{String? boundary}) {String? boundary})
: this.boundary = boundary ?? _randomString() { : boundary = boundary ?? _randomString() {
if (header.items.isEmpty) { if (header.items.isEmpty) {
throw new ArgumentError('`header` cannot be null or empty.'); throw ArgumentError('`header` cannot be null or empty.');
} }
} }
/// Computes the content length that will be written to a response, given a stream of the given [totalFileSize]. /// Computes the content length that will be written to a response, given a stream of the given [totalFileSize].
int computeContentLength(int totalFileSize) { int computeContentLength(int totalFileSize) {
int len = 0; var len = 0;
for (var item in header.items) { for (var item in header.items) {
if (item.start == -1) { if (item.start == -1) {
@ -59,15 +59,15 @@ class RangeHeaderTransformer
@override @override
Stream<List<int>> bind(Stream<List<int>> stream) { Stream<List<int>> bind(Stream<List<int>> stream) {
var ctrl = new StreamController<List<int>>(); var ctrl = StreamController<List<int>>();
new Future(() async { Future(() async {
var index = 0; var index = 0;
var enqueued = new Queue<List<int>>(); var enqueued = Queue<List<int>>();
var q = new StreamQueue(stream); var q = StreamQueue(stream);
Future<List<int>> absorb(int length) async { Future<List<int>> absorb(int length) async {
var out = new BytesBuilder(); var out = BytesBuilder();
while (out.length < length) { while (out.length < length) {
var remaining = length - out.length; var remaining = length - out.length;
@ -101,7 +101,7 @@ class RangeHeaderTransformer
// If we get this far, and the stream is EMPTY, the user requested // If we get this far, and the stream is EMPTY, the user requested
// too many bytes. // too many bytes.
if (out.length < length && enqueued.isEmpty && !(await q.hasNext)) { if (out.length < length && enqueued.isEmpty && !(await q.hasNext)) {
throw new StateError( throw StateError(
'The range denoted is bigger than the size of the input stream.'); 'The range denoted is bigger than the size of the input stream.');
} }
} }
@ -110,7 +110,7 @@ class RangeHeaderTransformer
} }
for (var item in header.items) { for (var item in header.items) {
var chunk = new BytesBuilder(); var chunk = BytesBuilder();
// Skip until we reach the start index. // Skip until we reach the start index.
while (index < item.start) { while (index < item.start) {
@ -120,8 +120,12 @@ class RangeHeaderTransformer
// Next, absorb until we reach the end. // Next, absorb until we reach the end.
if (item.end == -1) { if (item.end == -1) {
while (enqueued.isNotEmpty) chunk.add(enqueued.removeFirst()); while (enqueued.isNotEmpty) {
while (await q.hasNext) chunk.add(await q.next); chunk.add(enqueued.removeFirst());
}
while (await q.hasNext) {
chunk.add(await q.next);
}
} else { } else {
var remaining = item.end - index; var remaining = item.end - index;
chunk.add(await absorb(remaining)); chunk.add(await absorb(remaining));
@ -141,23 +145,27 @@ class RangeHeaderTransformer
ctrl.add(utf8.encode('--$boundary--\r\n')); ctrl.add(utf8.encode('--$boundary--\r\n'));
ctrl.close(); await ctrl.close();
}).catchError(ctrl.addError); }).catchError((e) {
ctrl.addError(e as Object);
return null;
});
return ctrl.stream; return ctrl.stream;
} }
} }
var _rnd = new Random(); var _rnd = Random();
String _randomString( String _randomString(
{int length: 32, {int length = 32,
String validChars: String validChars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'}) { 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'}) {
var len = _rnd.nextInt((length - 10)) + 10; var len = _rnd.nextInt((length - 10)) + 10;
var buf = new StringBuffer(); var buf = StringBuffer();
while (buf.length < len) while (buf.length < len) {
buf.writeCharCode(validChars.codeUnitAt(_rnd.nextInt(validChars.length))); buf.writeCharCode(validChars.codeUnitAt(_rnd.nextInt(validChars.length)));
}
return buf.toString(); return buf.toString();
} }

View file

@ -1,4 +1,5 @@
class RangeHeaderParseException extends FormatException { class RangeHeaderParseException extends FormatException {
@override
final String message; final String message;
RangeHeaderParseException(this.message); RangeHeaderParseException(this.message);

View file

@ -6,8 +6,8 @@ import 'range_header.dart';
import 'range_header_impl.dart'; import 'range_header_impl.dart';
import 'range_header_item.dart'; import 'range_header_item.dart';
final RegExp _rgxInt = new RegExp(r'[0-9]+'); final RegExp _rgxInt = RegExp(r'[0-9]+');
final RegExp _rgxWs = new RegExp(r'[ \n\r\t]'); final RegExp _rgxWs = RegExp(r'[ \n\r\t]');
enum TokenType { RANGE_UNIT, COMMA, INT, DASH, EQUALS } enum TokenType { RANGE_UNIT, COMMA, INT, DASH, EQUALS }
@ -19,27 +19,27 @@ class Token {
} }
List<Token> scan(String text, List<String> allowedRangeUnits) { List<Token> scan(String text, List<String> allowedRangeUnits) {
List<Token> tokens = []; var tokens = <Token>[];
var scanner = new SpanScanner(text); var scanner = SpanScanner(text);
while (!scanner.isDone) { while (!scanner.isDone) {
// Skip whitespace // Skip whitespace
scanner.scan(_rgxWs); scanner.scan(_rgxWs);
if (scanner.scanChar($comma)) if (scanner.scanChar($comma)) {
tokens.add(new Token(TokenType.COMMA, scanner.lastSpan)); tokens.add(Token(TokenType.COMMA, scanner.lastSpan));
else if (scanner.scanChar($dash)) } else if (scanner.scanChar($dash)) {
tokens.add(new Token(TokenType.DASH, scanner.lastSpan)); tokens.add(Token(TokenType.DASH, scanner.lastSpan));
else if (scanner.scan(_rgxInt)) } else if (scanner.scan(_rgxInt)) {
tokens.add(new Token(TokenType.INT, scanner.lastSpan)); tokens.add(Token(TokenType.INT, scanner.lastSpan));
else if (scanner.scanChar($equal)) } else if (scanner.scanChar($equal)) {
tokens.add(new Token(TokenType.EQUALS, scanner.lastSpan)); tokens.add(Token(TokenType.EQUALS, scanner.lastSpan));
else { } else {
bool matched = false; var matched = false;
for (var unit in allowedRangeUnits) { for (var unit in allowedRangeUnits) {
if (scanner.scan(unit)) { if (scanner.scan(unit)) {
tokens.add(new Token(TokenType.RANGE_UNIT, scanner.lastSpan)); tokens.add(Token(TokenType.RANGE_UNIT, scanner.lastSpan));
matched = true; matched = true;
break; break;
} }
@ -47,8 +47,8 @@ List<Token> scan(String text, List<String> allowedRangeUnits) {
if (!matched) { if (!matched) {
var ch = scanner.readChar(); var ch = scanner.readChar();
throw new RangeHeaderParseException( throw RangeHeaderParseException(
'Unexpected character: "${new String.fromCharCode(ch)}"'); 'Unexpected character: "${String.fromCharCode(ch)}"');
} }
} }
} }
@ -68,21 +68,22 @@ class Parser {
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; var offset = current?.span?.start.offset;
if (offset == null) return new RangeHeaderParseException('Expected $type.'); if (offset == null) return 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 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 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) {
if (done) return false; if (done) return false;
@ -91,34 +92,38 @@ class Parser {
_index++; _index++;
_current = tok; _current = tok;
return true; return true;
} else } else {
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 = []; var items = <RangeHeaderItem>[];
RangeHeaderItem? item = parseHeaderItem(); var item = parseHeaderItem();
while (item != null) { while (item != null) {
items.add(item); items.add(item);
// Parse comma // Parse comma
if (next(TokenType.COMMA)) { if (next(TokenType.COMMA)) {
item = parseHeaderItem(); item = parseHeaderItem();
} else } else {
item = null; item = null;
} }
}
if (items.isEmpty) if (items.isEmpty) {
throw _expected('range'); throw _expected('range');
else } else {
return new RangeHeaderImpl(unit, items); return RangeHeaderImpl(unit, items);
} else }
} else {
return null; return null;
} }
}
RangeHeaderItem? parseHeaderItem() { RangeHeaderItem? parseHeaderItem() {
if (next(TokenType.INT)) { if (next(TokenType.INT)) {
@ -126,18 +131,22 @@ class Parser {
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 RangeHeaderItem(start, int.parse(current!.span!.text));
} else } else {
return new RangeHeaderItem(start); return RangeHeaderItem(start);
} else }
} else {
throw _expected('"-"'); throw _expected('"-"');
}
} 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 RangeHeaderItem(-1, int.parse(current!.span!.text));
} else } else {
throw _expected('integer'); throw _expected('integer');
} else }
} else {
return null; return null;
} }
} }
}

View file

@ -15,7 +15,7 @@ abstract class RangeHeader {
/// 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 = Set<RangeHeaderItem>(); var out = <RangeHeaderItem>{};
for (var item in items) { for (var item in items) {
// Remove any overlapping items, consolidate them. // Remove any overlapping items, consolidate them.
@ -28,7 +28,7 @@ abstract class RangeHeader {
out.add(item); out.add(item);
} }
return new UnmodifiableListView(out.toList()..sort()); return UnmodifiableListView(out.toList()..sort());
} }
/// Attempts to parse a [RangeHeader] from its [text] representation. /// Attempts to parse a [RangeHeader] from its [text] representation.
@ -40,9 +40,9 @@ abstract class RangeHeader {
/// 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 = Parser(tokens);
var header = parser.parseRangeHeader(); var header = parser.parseRangeHeader();
if (header == null) { if (header == null) {
throw InvalidRangeHeaderException('Header is null'); throw InvalidRangeHeaderException('Header is null');
@ -57,9 +57,10 @@ abstract class RangeHeader {
class _ConstantRangeHeader implements RangeHeader { class _ConstantRangeHeader implements RangeHeader {
final Iterable<RangeHeaderItem> items_; final Iterable<RangeHeaderItem> items_;
@override
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 =>

View file

@ -8,12 +8,12 @@ class RangeHeaderImpl implements RangeHeader {
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); _items.addAll(items);
} }
@override @override
UnmodifiableListView<RangeHeaderItem> get items => UnmodifiableListView<RangeHeaderItem> get items =>
_cached ??= new UnmodifiableListView<RangeHeaderItem>(_items); _cached ??= UnmodifiableListView<RangeHeaderItem>(_items);
@override @override
final String? rangeUnit; final String? rangeUnit;

View file

@ -14,8 +14,9 @@ class RangeHeaderItem implements Comparable<RangeHeaderItem> {
/// Joins two items together into the largest possible range. /// Joins two items together into the largest possible range.
RangeHeaderItem consolidate(RangeHeaderItem other) { RangeHeaderItem consolidate(RangeHeaderItem other) {
if (!(other.overlaps(this))) if (!(other.overlaps(this))) {
throw ArgumentError('The two ranges do not overlap.'); throw ArgumentError('The two ranges do not overlap.');
}
return RangeHeaderItem(min(start, other.start), max(end, other.end)); return RangeHeaderItem(min(start, other.start), max(end, other.end));
} }
@ -56,13 +57,14 @@ class RangeHeaderItem implements Comparable<RangeHeaderItem> {
@override @override
String toString() { String toString() {
if (start > -1 && end > -1) if (start > -1 && end > -1) {
return '$start-$end'; return '$start-$end';
else if (start > -1) } else if (start > -1) {
return '$start-'; return '$start-';
else } else {
return '-$end'; return '-$end';
} }
}
/// Creates a representation of this instance suitable for a `Content-Range` header. /// Creates a representation of this instance suitable for a `Content-Range` header.
/// ///

View file

@ -1,5 +1,5 @@
name: angel3_range_header name: angel3_range_header
version: 3.0.0 version: 3.0.1
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.
homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/range_header homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/range_header
environment: environment:
@ -11,9 +11,8 @@ dependencies:
source_span: ^1.8.1 source_span: ^1.8.1
string_scanner: ^1.1.0 string_scanner: ^1.1.0
dev_dependencies: dev_dependencies:
#angel_framework:
#angel_static: ^2.0.0
file: ^6.1.0 file: ^6.1.0
http_parser: ^4.0.0 http_parser: ^4.0.0
logging: ^1.0.1 logging: ^1.0.1
test: ^1.17.4 test: ^1.17.4
pedantic: ^1.11.0

View file

@ -1,8 +1,6 @@
import 'package:angel3_range_header/angel3_range_header.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../lib/angel3_range_header.dart';
import '../lib/src/range_header.dart';
final Matcher throwsRangeParseException = final Matcher throwsRangeParseException =
throwsA(const TypeMatcher<RangeHeaderParseException>()); throwsA(const TypeMatcher<RangeHeaderParseException>());