Added range_header, user_agent and body_parser

This commit is contained in:
thomashii 2021-09-12 09:23:12 +08:00
parent 6fb188b265
commit 7a2bb8f5da
51 changed files with 2104 additions and 7 deletions

View file

@ -6,9 +6,13 @@ This repository contains the common utility packages required for developing dar
## Available Packages
* Html Builder
* Body Parser
* Code Buffer
* Combinator
* Html Builder
* Merge Map
* Pub Sub
* Range Header
* Symbol Table
* User Agent

View file

@ -0,0 +1,4 @@
language: dart
dart:
- dev
- stable

View file

@ -0,0 +1,12 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
Thomas is the current maintainer of the code base. He has refactored and migrated the
code base to support NNBD.
* __[Tobe O](thosakwe@gmail.com)__
Tobe has written much of the original code prior to NNBD migration. He has moved on and
is no longer involved with the project.

View file

@ -0,0 +1,32 @@
# Change Log
## 3.0.0
* Upgraded from `pendantic` to `lints` linter
* Published as `belatuk_body_parser` package
* Fixed linter warnings
## 2.1.1
* Fixed calling deprecated methods in unit test
## 2.1.0
* Replaced `http_server` with `belatuk_http_server`
## 2.0.1
* Fixed source code formating warning
* Updated README
## 2.0.0
* Migrated to support Dart SDK 2.12.x NNBD
## 1.1.1
* Dart 2 updates; should fix Angel in Travis.
## 1.1.0
* Add `parseBodyFromStream`

View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2021, dukefirehawk.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,75 @@
# Belatuk Body Parser
[![version](https://img.shields.io/badge/pub-v3.0.0-brightgreen)](https://pub.dev/packages/belatuk_body_parser)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/body_parser/LICENSE)
**Replacement of `package:body_parser` with breaking changes to support NNBD.**
Parse request bodies and query strings in Dart, as well multipart/form-data uploads. No external dependencies required.
This is the request body parser powering the [Angel3 framework](https://pub.dev/packages/angel3_framework). If you are looking for a server-side solution with dependency injection, WebSockets, and more, then I highly recommend it as your first choice. Bam!
## Contents
- [Belatuk Body Parser](#belatuk-body-parser)
- [Contents](#contents)
- [About](#about)
- [Installation](#installation)
- [Usage](#usage)
- [Custom Body Parsing](#custom-body-parsing)
### About
I needed something like Express.js's `body-parser` module, so I made it here. It fully supports JSON requests. x-www-form-urlencoded fully supported, as well as query strings. You can also include arrays in your query, in the same way you would for a PHP application. Full file upload support will also be present by the production 1.0.0 release.
A benefit of this is that primitive types are automatically deserialized correctly. As in, if you have a `hello=1.5` request, then `body['hello']` will equal `1.5` and not `'1.5'`. A very semantic difference, yes, but it relieves stress in my head.
### Installation
To install Body Parser for your Dart project, simply add body_parser to your pub dependencies.
dependencies:
belatuk_body_parser: ^3.0.0
### Usage
Body Parser exposes a simple class called `BodyParseResult`. You can easily parse the query string and request body for a request by calling `Future<BodyParseResult> parseBody`.
```dart
import 'dart:convert';
import 'package:belatuk_body_parser/belatuk_body_parser.dart';
main() async {
// ...
await for (HttpRequest request in server) {
request.response.write(JSON.encode(await parseBody(request).body));
await request.response.close();
}
}
```
You can also use `buildMapFromUri(Map, String)` to populate a map from a URL encoded string.
This can easily be used with a library like [Angel3 JSON God](https://pub.dev/packages/angel3_json_god) to build structured JSON/REST APIs. Add validation and you've got an instant backend.
```dart
MyClass create(HttpRequest request) async {
return god.deserialize(await parseBody(request).body, MyClass);
}
```
### Custom Body Parsing
In cases where you need to parse unrecognized content types, `body_parser` won't be of any help to you on its own. However, you can use the `originalBuffer` property of a `BodyParseResult` to see the original request buffer. To get this functionality, pass `storeOriginalBuffer` as `true` when calling `parseBody`.
For example, if you wanted to [parse GraphQL queries within your server](https://github.com/dukefirehawk/graphql_dart)...
```dart
app.get('/graphql', (req, res) async {
if (req.headers.contentType.mimeType == 'application/graphql') {
var graphQlString = String.fromCharCodes(req.originalBuffer);
// ...
}
});
```

View file

@ -0,0 +1 @@
include: package:lints/recommended.yaml

View file

@ -0,0 +1,61 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:http_parser/http_parser.dart';
import 'package:belatuk_body_parser/belatuk_body_parser.dart';
void main() async {
var address = '127.0.0.1';
var port = 3000;
var futures = <Future>[];
for (var i = 1; i < Platform.numberOfProcessors; i++) {
futures.add(Isolate.spawn(start, [address, port, i]));
}
await Future.wait(futures).then((_) {
print('All instances started.');
print(
'Test with "wrk -t12 -c400 -d30s -s ./example/post.lua http://localhost:3000" or similar');
start([address, port, 0]);
});
}
void start(List args) {
var address = InternetAddress(args[0] as String);
var port = 8080;
if (args[1] is int) {
args[1];
}
var id = 0;
if (args[2] is int) {
args[2];
}
HttpServer.bind(address, port, shared: true).then((server) {
server.listen((request) async {
// ignore: deprecated_member_use
var body = await defaultParseBody(request);
request.response
..headers.contentType = ContentType('application', 'json')
..write(json.encode(body.body));
await request.response.close();
});
print(
'Server #$id listening at http://${server.address.address}:${server.port}');
});
}
Future<BodyParseResult> defaultParseBody(HttpRequest request,
{bool storeOriginalBuffer = false}) {
return parseBodyFromStream(
request,
request.headers.contentType != null
? MediaType.parse(request.headers.contentType.toString())
: null,
request.uri,
storeOriginalBuffer: storeOriginalBuffer);
}

View file

@ -0,0 +1,6 @@
-- example HTTP POST script which demonstrates setting the
-- HTTP method, body, and adding a header
wrk.method = "POST"
wrk.body = "foo=bar&baz=quux"
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"

View file

@ -0,0 +1,6 @@
/// A library for parsing HTTP request bodies and queries.
library angel3_body_parser;
export 'src/body_parse_result.dart';
export 'src/file_upload_info.dart';
export 'src/parse_body.dart';

View file

@ -0,0 +1,28 @@
import 'file_upload_info.dart';
/// A representation of data from an incoming request.
abstract class BodyParseResult {
/// The parsed body.
Map<String?, dynamic> get body;
/// The parsed query string.
Map<String?, dynamic> get query;
/// All files uploaded within this request.
List<FileUploadInfo> get files;
/// The original body bytes sent with this request.
///
/// You must set [storeOriginalBuffer] to `true` to see this.
List<int>? get originalBuffer;
/// If an error was encountered while parsing the body, it will appear here.
///
/// Otherwise, this is `null`.
dynamic get error;
/// If an error was encountered while parsing the body, the call stack will appear here.
///
/// Otherwise, this is `null`.
StackTrace? get stack;
}

View file

@ -0,0 +1,7 @@
import 'file_upload_info.dart';
List<FileUploadInfo> getFileDataFromChunk(
String chunk, String boundary, String fileUploadName, Map body) {
var result = <FileUploadInfo>[];
return result;
}

View file

@ -0,0 +1,17 @@
/// Represents a file uploaded to the server.
class FileUploadInfo {
/// The MIME type of the uploaded file.
String? mimeType;
/// The name of the file field from the request.
String? name;
/// The filename of the file.
String? filename;
/// The bytes that make up this file.
List<int> data;
FileUploadInfo(
{this.mimeType, this.name, this.filename, this.data = const []});
}

View file

@ -0,0 +1,22 @@
import 'dart:convert';
dynamic getValue(String value) {
try {
var numValue = num.parse(value);
if (!numValue.isNaN) {
return numValue;
} else {
return value;
}
} on FormatException {
if (value.startsWith('[') && value.endsWith(']')) {
return json.decode(value);
} else if (value.startsWith('{') && value.endsWith('}')) {
return json.decode(value);
} else if (value.trim().toLowerCase() == 'null') {
return null;
} else {
return value;
}
}
}

View file

@ -0,0 +1,44 @@
import 'get_value.dart';
/// Parses a URI-encoded string into real data! **Wow!**
///
/// Whichever map you provide will be automatically populated from the urlencoded body string you provide.
void buildMapFromUri(Map map, String body) {
var parseArrayRgx = RegExp(r'^(.+)\[\]$');
for (var keyValuePair in body.split('&')) {
if (keyValuePair.contains('=')) {
var equals = keyValuePair.indexOf('=');
var key = Uri.decodeQueryComponent(keyValuePair.substring(0, equals));
var value = Uri.decodeQueryComponent(keyValuePair.substring(equals + 1));
if (parseArrayRgx.hasMatch(key)) {
Match queryMatch = parseArrayRgx.firstMatch(key)!;
key = queryMatch.group(1)!;
if (map[key] is! List) {
map[key] = [];
}
map[key].add(getValue(value));
} else if (key.contains('.')) {
// i.e. map.foo.bar => [map, foo, bar]
var keys = key.split('.');
var targetMap = map[keys[0]] != null ? map[keys[0]] as Map? : {};
map[keys[0]] = targetMap;
for (var i = 1; i < keys.length; i++) {
if (i < keys.length - 1) {
targetMap![keys[i]] = targetMap[keys[i]] ?? {};
targetMap = targetMap[keys[i]] as Map?;
} else {
targetMap![keys[i]] = getValue(value);
}
}
} else {
map[key] = getValue(value);
}
} else {
map[Uri.decodeQueryComponent(keyValuePair)] = true;
}
}
}

View file

@ -0,0 +1,147 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:http_parser/http_parser.dart';
import 'package:belatuk_http_server/belatuk_http_server.dart';
import 'package:mime/mime.dart';
import 'body_parse_result.dart';
import 'file_upload_info.dart';
import 'map_from_uri.dart';
/// Forwards to [parseBodyFromStream].
@deprecated
Future<BodyParseResult> parseBody(HttpRequest request,
{bool storeOriginalBuffer = false}) {
return parseBodyFromStream(
request,
request.headers.contentType != null
? MediaType.parse(request.headers.contentType.toString())
: null,
request.uri,
storeOriginalBuffer: storeOriginalBuffer);
}
/// Grabs data from an incoming request.
///
/// Supports URL-encoded and JSON, as well as multipart/* forms.
/// On a file upload request, only fields with the name **'file'** are processed
/// as files. Anything else is put in the body. You can change the upload file name
/// via the *fileUploadName* parameter. :)
///
/// Use [storeOriginalBuffer] to add the original request bytes to the result.
Future<BodyParseResult> parseBodyFromStream(
Stream<Uint8List> data, MediaType? contentType, Uri requestUri,
{bool storeOriginalBuffer = false}) async {
var result = _BodyParseResultImpl();
Future<Uint8List> getBytes() {
return data
.fold<BytesBuilder>(BytesBuilder(copy: false), (a, b) => a..add(b))
.then((b) => b.takeBytes());
}
Future<String> getBody() {
if (storeOriginalBuffer) {
return getBytes().then((bytes) {
result.originalBuffer = bytes;
return utf8.decode(bytes);
});
} else {
return utf8.decoder.bind(data).join();
}
}
try {
if (contentType != null) {
if (contentType.type == 'multipart' &&
contentType.parameters.containsKey('boundary')) {
Stream<Uint8List> stream;
if (storeOriginalBuffer) {
var bytes = result.originalBuffer = await getBytes();
var ctrl = StreamController<Uint8List>()..add(bytes);
await ctrl.close();
stream = ctrl.stream;
} else {
stream = data;
}
var parts =
MimeMultipartTransformer(contentType.parameters['boundary']!)
.bind(stream)
.map((part) =>
HttpMultipartFormData.parse(part, defaultEncoding: utf8));
await for (HttpMultipartFormData part in parts) {
if (part.isBinary ||
part.contentDisposition.parameters.containsKey('filename')) {
var builder = await part.fold(
BytesBuilder(copy: false),
(BytesBuilder b, d) =>
b..add(d is! String ? (d as List<int>?)! : d.codeUnits));
var upload = FileUploadInfo(
mimeType: part.contentType!.mimeType,
name: part.contentDisposition.parameters['name'],
filename:
part.contentDisposition.parameters['filename'] ?? 'file',
data: builder.takeBytes());
result.files.add(upload);
} else if (part.isText) {
var text = await part.join();
buildMapFromUri(result.body,
'${part.contentDisposition.parameters["name"]}=$text');
}
}
} else if (contentType.mimeType == 'application/json') {
result.body.addAll(
_foldToStringDynamic(json.decode(await getBody()) as Map?)!);
} else if (contentType.mimeType == 'application/x-www-form-urlencoded') {
var body = await getBody();
buildMapFromUri(result.body, body);
} else if (storeOriginalBuffer == true) {
result.originalBuffer = await getBytes();
}
} else {
if (requestUri.hasQuery) {
buildMapFromUri(result.query, requestUri.query);
}
if (storeOriginalBuffer == true) {
result.originalBuffer = await getBytes();
}
}
} catch (e, st) {
result.error = e;
result.stack = st;
}
return result;
}
class _BodyParseResultImpl implements BodyParseResult {
@override
Map<String?, dynamic> body = {};
@override
List<FileUploadInfo> files = [];
@override
List<int>? originalBuffer;
@override
Map<String?, dynamic> query = {};
@override
var error;
@override
StackTrace? stack;
}
Map<String, dynamic>? _foldToStringDynamic(Map? map) {
return map?.keys.fold<Map<String, dynamic>>(
<String, dynamic>{}, (out, k) => out..[k.toString()] = map[k]);
}

View file

@ -0,0 +1,14 @@
name: belatuk_body_parser
version: 3.0.0
description: Parse request bodies and query strings in Dart. Supports JSON, URL-encoded, and multi-part bodies.
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/body_parser
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
http_parser: ^4.0.0
belatuk_http_server: ^2.0.0
mime: ^1.0.0
dev_dependencies:
http: ^0.13.0
test: ^1.17.8
lints: ^1.0.0

View file

@ -0,0 +1,156 @@
import 'dart:io';
import 'dart:convert';
import 'package:belatuk_body_parser/belatuk_body_parser.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:test/test.dart';
import 'server_test.dart';
Future<BodyParseResult> _parseBody(HttpRequest request) {
return parseBodyFromStream(
request,
request.headers.contentType != null
? MediaType.parse(request.headers.contentType.toString())
: null,
request.uri,
storeOriginalBuffer: false);
}
void main() {
HttpServer? server;
String? url;
http.Client? client;
setUp(() async {
server = await HttpServer.bind('127.0.0.1', 0);
server!.listen((HttpRequest request) async {
//Server will simply return a JSON representation of the parsed body
// ignore: deprecated_member_use
request.response.write(jsonEncodeBody(await _parseBody(request)));
await request.response.close();
});
url = 'http://localhost:${server!.port}';
print('Test server listening on $url');
client = http.Client();
});
tearDown(() async {
await server!.close(force: true);
client!.close();
server = null;
url = null;
client = null;
});
test('No upload', () async {
var boundary = 'myBoundary';
var headers = <String, String>{
'content-type': 'multipart/form-data; boundary=$boundary'
};
var postData = '''
--$boundary
Content-Disposition: form-data; name="hello"
world
--$boundary--
'''
.replaceAll('\n', '\r\n');
print(
'Form Data: \n${postData.replaceAll("\r", "\\r").replaceAll("\n", "\\n")}');
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
print('Response: ${response.body}');
var jsons = json.decode(response.body);
var files = jsons['files'].map((map) {
return map == null
? null
: map.keys.fold<Map<String, dynamic>>(
<String, dynamic>{}, (out, k) => out..[k.toString()] = map[k]);
});
expect(files.length, equals(0));
expect(jsons['body']['hello'], equals('world'));
});
test('Single upload', () async {
var boundary = 'myBoundary';
var headers = <String, String>{
'content-type': ContentType('multipart', 'form-data',
parameters: {'boundary': boundary}).toString()
};
var postData = '''
--$boundary
Content-Disposition: form-data; name="hello"
world
--$boundary
Content-Disposition: form-data; name="file"; filename="app.dart"
Content-Type: application/dart
Hello world
--$boundary--
'''
.replaceAll('\n', '\r\n');
print(
'Form Data: \n${postData.replaceAll("\r", "\\r").replaceAll("\n", "\\n")}');
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
print('Response: ${response.body}');
var jsons = json.decode(response.body);
var files = jsons['files'];
expect(files.length, equals(1));
expect(files[0]['name'], equals('file'));
expect(files[0]['mimeType'], equals('application/dart'));
expect(files[0]['data'].length, equals(11));
expect(files[0]['filename'], equals('app.dart'));
expect(jsons['body']['hello'], equals('world'));
});
test('Multiple upload', () async {
var boundary = 'myBoundary';
var headers = <String, String>{
'content-type': 'multipart/form-data; boundary=$boundary'
};
var postData = '''
--$boundary
Content-Disposition: form-data; name="json"
god
--$boundary
Content-Disposition: form-data; name="num"
14.50000
--$boundary
Content-Disposition: form-data; name="file"; filename="app.dart"
Content-Type: text/plain
Hello world
--$boundary
Content-Disposition: form-data; name="entry-point"; filename="main.js"
Content-Type: text/javascript
function main() {
console.log("Hello, world!");
}
--$boundary--
'''
.replaceAll('\n', '\r\n');
print(
'Form Data: \n${postData.replaceAll("\r", "\\r").replaceAll("\n", "\\n")}');
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
print('Response: ${response.body}');
var jsons = json.decode(response.body);
var files = jsons['files'];
expect(files.length, equals(2));
expect(files[0]['name'], equals('file'));
expect(files[0]['mimeType'], equals('text/plain'));
expect(files[0]['data'].length, equals(11));
expect(files[1]['name'], equals('entry-point'));
expect(files[1]['mimeType'], equals('text/javascript'));
expect(jsons['body']['json'], equals('god'));
expect(jsons['body']['num'], equals(14.5));
});
}

View file

@ -0,0 +1,174 @@
import 'dart:convert';
import 'dart:io' show HttpRequest, HttpServer;
import 'package:belatuk_body_parser/belatuk_body_parser.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:test/test.dart';
const TOKEN =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIxMjcuMC4wLjEiLCJleHAiOi0xLCJpYXQiOiIyMDE2LTEyLTIyVDEyOjQ5OjUwLjM2MTQ0NiIsImlzcyI6ImFuZ2VsX2F1dGgiLCJzdWIiOiIxMDY2OTQ4Mzk2MDIwMjg5ODM2NTYifQ==.PYw7yUb-cFWD7N0sSLztP7eeRvO44nu1J2OgDNyT060=';
String jsonEncodeBody(BodyParseResult result) {
return json.encode({
'query': result.query,
'body': result.body,
'error': result.error?.toString(),
'files': result.files.map((f) {
return {
'name': f.name,
'mimeType': f.mimeType,
'filename': f.filename,
'data': f.data,
};
}).toList(),
'originalBuffer': result.originalBuffer,
'stack': null, //result.stack.toString(),
});
}
Future<BodyParseResult> _parseBody(HttpRequest request) {
return parseBodyFromStream(
request,
request.headers.contentType != null
? MediaType.parse(request.headers.contentType.toString())
: null,
request.uri,
storeOriginalBuffer: true);
}
void main() {
HttpServer? server;
String? url;
http.Client? client;
setUp(() async {
server = await HttpServer.bind('127.0.0.1', 0);
server!.listen((HttpRequest request) async {
//Server will simply return a JSON representation of the parsed body
request.response.write(
// ignore: deprecated_member_use
jsonEncodeBody(await _parseBody(request)));
await request.response.close();
});
url = 'http://localhost:${server!.port}';
print('Test server listening on $url');
client = http.Client();
});
tearDown(() async {
await server!.close(force: true);
client!.close();
server = null;
url = null;
client = null;
});
group('query string', () {
test('GET Simple', () async {
print('GET $url/?hello=world');
var response = await client!.get(Uri.parse('$url/?hello=world'));
print('Response: ${response.body}');
var result = json.decode(response.body);
expect(result['body'], equals({}));
expect(result['query'], equals({'hello': 'world'}));
expect(result['files'], equals([]));
//expect(result['originalBuffer'], isNull);
});
test('GET Complex', () async {
var postData =
'hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=${3 - 1}&map.foo.bar=baz';
print('Body: $postData');
var response = await client!.get(Uri.parse('$url/?$postData'));
print('Response: ${response.body}');
var query = json.decode(response.body)['query'];
expect(query['hello'], equals('world'));
expect(query['nums'][2], equals(2));
expect(query['map'] is Map, equals(true));
expect(query['map']['foo'], equals({'bar': 'baz'}));
});
test('JWT', () async {
var postData = 'token=$TOKEN';
print('Body: $postData');
var response = await client!.get(Uri.parse('$url/?$postData'));
print('Response: ${response.body}');
var query = json.decode(response.body)['query'];
expect(query['token'], equals(TOKEN));
});
});
group('urlencoded', () {
var headers = <String, String>{
'content-type': 'application/x-www-form-urlencoded'
};
test('POST Simple', () async {
print('Body: hello=world');
var response = await client!
.post(Uri.parse(url!), headers: headers, body: 'hello=world');
print('Response: ${response.body}');
var result = json.decode(response.body);
expect(result['query'], equals({}));
expect(result['body'], equals({'hello': 'world'}));
expect(result['files'], equals([]));
expect(result['originalBuffer'], isList);
expect(result['originalBuffer'], isNotEmpty);
});
test('Post Complex', () async {
var postData =
'hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=${3 - 1}&map.foo.bar=baz';
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
print('Response: ${response.body}');
var body = json.decode(response.body)['body'];
expect(body['hello'], equals('world'));
expect(body['nums'][2], equals(2));
expect(body['map'] is Map, equals(true));
expect(body['map']['foo'], equals({'bar': 'baz'}));
});
test('JWT', () async {
var postData = 'token=$TOKEN';
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
var body = json.decode(response.body)['body'];
expect(body['token'], equals(TOKEN));
});
});
group('json', () {
var headers = <String, String>{'content-type': 'application/json'};
test('Post Simple', () async {
var postData = json.encode({'hello': 'world'});
print('Body: $postData');
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
print('Response: ${response.body}');
var result = json.decode(response.body);
expect(result['body'], equals({'hello': 'world'}));
expect(result['query'], equals({}));
expect(result['files'], equals([]));
expect(result['originalBuffer'], allOf(isList, isNotEmpty));
});
test('Post Complex', () async {
var postData = json.encode({
'hello': 'world',
'nums': [1, 2.0, 3 - 1],
'map': {
'foo': {'bar': 'baz'}
}
});
print('Body: $postData');
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
print('Response: ${response.body}');
var body = json.decode(response.body)['body'];
expect(body['hello'], equals('world'));
expect(body['nums'][2], equals(2));
expect(body['map'] is Map, equals(true));
expect(body['map']['foo'], equals({'bar': 'baz'}));
});
});
}

View file

@ -1,6 +1,6 @@
# Belatuk Code Buffer
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dartlang.org/packages/belatuk_code_buffer)
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dev/packages/belatuk_code_buffer)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/code_buffer/LICENSE)

View file

@ -1,6 +1,6 @@
# Belatuk Combinator
[![version](https://img.shields.io/badge/pub-v3.0.0-brightgreen)](https://pub.dartlang.org/packages/belatuk_combinator)
[![version](https://img.shields.io/badge/pub-v3.0.0-brightgreen)](https://pub.dev/packages/belatuk_combinator)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/combinator/LICENSE)

View file

@ -1,6 +1,6 @@
# Betaluk Html Builder
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dartlang.org/packages/belatuk_html_builder)
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dev/packages/belatuk_html_builder)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/html_builder/LICENSE)

View file

@ -1,6 +1,6 @@
# Belatuk Merge Map
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dartlang.org/packages/belatuk_merge_map)
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dev/packages/belatuk_merge_map)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/merge_map/LICENSE)

View file

@ -1,6 +1,6 @@
# Belatuk Pub Sub
[![version](https://img.shields.io/badge/pub-v4.0.2-brightgreen)](https://pub.dartlang.org/packages/belatuk_pub_sub)
[![version](https://img.shields.io/badge/pub-v4.0.2-brightgreen)](https://pub.dev/packages/belatuk_pub_sub)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/pub_sub/LICENSE)

View file

@ -0,0 +1,12 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
Thomas is the current maintainer of the code base. He has refactored and migrated the
code base to support NNBD.
* __[Tobe O](thosakwe@gmail.com)__
Tobe has written much of the original code prior to NNBD migration. He has moved on and
is no longer involved with the project.

View file

@ -0,0 +1,34 @@
# Change Log
## 4.0.0
* Upgraded from `pendantic` to `lints` linter
* Published as `belatuk_range_header` package
## 3.0.2
* Updated README
## 3.0.1
* Resolve static analysis warnings
## 3.0.0
* Migrated to work with Dart SDK 2.12.x NNBD
## 2.0.2
* Fix bug in `toContentRange` that printed invalid indices.
* Fold header items by default.
## 2.0.1
* Adjust `RangeHeaderTransformer` to properly print the content range of each item,
when multiple are present.
## 2.0.0
* Dart 2 update.
* Add `RangeHeaderTransformer`.
* Overall restructuring/refactoring.

View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2021, dukefirehawk.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,40 @@
# Belatuk Range Header
[![version](https://img.shields.io/badge/pub-v3.0.0-brightgreen)](https://pub.dev/packages/belatuk_range_header)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/range_header/LICENSE)
**Replacement of `package:range_header` with breaking changes to support NNBD.**
Range header parser for belatuk. Can be used by any dart backend.
## Installation
In your `pubspec.yaml`:
```yaml
dependencies:
belatuk_range_header: ^4.0.0
```
## Usage
```dart
handleRequest(HttpRequest request) async {
// Parse the header
var header = RangeHeader.parse(request.headers.value(HttpHeaders.rangeHeader));
// Optimize/canonicalize it
var items = RangeHeader.foldItems(header.items);
header = RangeHeader(items);
// Get info
header.items;
header.rangeUnit;
print(header.items[0].toContentRange(fileSize));
// Serve the file
var transformer = RangeHeaderTransformer(header);
await file.openRead().transform(transformer).pipe(request.response);
}
```

View file

@ -0,0 +1 @@
include: package:lints/recommended.yaml

View file

@ -0,0 +1,28 @@
import 'dart:io';
import 'package:angel3_range_header/angel3_range_header.dart';
var file = File('some_video.mp4');
void handleRequest(HttpRequest request) async {
// Parse the header
var header =
RangeHeader.parse(request.headers.value(HttpHeaders.rangeHeader)!);
// Optimize/canonicalize it
var items = RangeHeader.foldItems(header.items);
header = RangeHeader(items);
// Get info
header.items;
header.rangeUnit;
header.items.forEach((item) => item.toContentRange(400));
// Serve the file
var transformer =
RangeHeaderTransformer(header, 'video/mp4', await file.length());
await file
.openRead()
.cast<List<int>>()
.transform(transformer)
.pipe(request.response);
}

View file

@ -0,0 +1,103 @@
//import 'package:angel_framework/angel_framework.dart';
//import 'package:angel_framework/http.dart';
//import 'package:angel_static/angel_static.dart';
void main() async {
/*
var app = new Angel();
var http = new AngelHttp(app);
var fs = const LocalFileSystem();
var vDir = new _RangingVirtualDirectory(app, fs.currentDirectory);
app.logger = new Logger('range_header')
..onRecord.listen((rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
});
app.mimeTypeResolver
..addExtension('dart', 'text/dart')
..addExtension('lock', 'text/plain')
..addExtension('md', 'text/plain')
..addExtension('packages', 'text/plain')
..addExtension('yaml', 'text/plain')
..addExtension('yml', 'text/plain');
app.fallback(vDir.handleRequest);
app.fallback((req, res) => throw new AngelHttpException.notFound());
await http.startServer('127.0.0.1', 3000);
print('Listening at ${http.uri}');
*/
}
/*
class _RangingVirtualDirectory extends VirtualDirectory {
_RangingVirtualDirectory(Angel app, Directory source)
: super(app, source.fileSystem,
source: source, allowDirectoryListing: true);
@override
Future<bool> serveFile(
File file, FileStat stat, RequestContext req, ResponseContext res) async {
res.headers[HttpHeaders.acceptRangesHeader] = 'bytes';
if (req.headers.value(HttpHeaders.rangeHeader)?.startsWith('bytes') ==
true) {
var header =
new RangeHeader.parse(req.headers.value(HttpHeaders.rangeHeader));
header = new RangeHeader(RangeHeader.foldItems(header.items));
if (header.items.length == 1) {
var item = header.items[0];
Stream<Uint8List> stream;
int len = 0, total = await file.length();
if (item.start == -1) {
if (item.end == -1) {
len = total;
stream = file.openRead();
} else {
len = item.end + 1;
stream = file.openRead(0, item.end + 1);
}
} else {
if (item.end == -1) {
len = total - item.start;
stream = file.openRead(item.start);
} else {
len = item.end - item.start + 1;
stream = file.openRead(item.start, item.end + 1);
}
}
res.contentType = new MediaType.parse(
app.mimeTypeResolver.lookup(file.path) ??
'application/octet-stream');
res.statusCode = HttpStatus.partialContent;
res.headers[HttpHeaders.contentLengthHeader] = len.toString();
res.headers[HttpHeaders.contentRangeHeader] =
'bytes ' + item.toContentRange(total);
await stream.cast<List<int>>().pipe(res);
return false;
} else {
var totalFileSize = await file.length();
var transformer = new RangeHeaderTransformer(
header,
app.mimeTypeResolver.lookup(file.path) ??
'application/octet-stream',
await file.length());
res.statusCode = HttpStatus.partialContent;
res.headers[HttpHeaders.contentLengthHeader] =
transformer.computeContentLength(totalFileSize).toString();
res.contentType = new MediaType(
'multipart', 'byteranges', {'boundary': transformer.boundary});
await file
.openRead()
.cast<List<int>>()
.transform(transformer)
.pipe(res);
return false;
}
} else {
return await super.serveFile(file, stat, req, res);
}
}
}
*/

View file

@ -0,0 +1,4 @@
export 'src/converter.dart';
export 'src/exception.dart';
export 'src/range_header.dart';
export 'src/range_header_item.dart';

View file

@ -0,0 +1,171 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io' show BytesBuilder;
import 'dart:math';
import 'package:async/async.dart';
import 'package:charcode/ascii.dart';
import 'range_header.dart';
/// A [StreamTransformer] that uses a parsed [RangeHeader] and transforms an input stream
/// into one compatible with the `multipart/byte-ranges` specification.
class RangeHeaderTransformer
extends StreamTransformerBase<List<int>, List<int>> {
final RangeHeader header;
final String boundary, mimeType;
final int totalLength;
RangeHeaderTransformer(this.header, this.mimeType, this.totalLength,
{String? boundary})
: boundary = boundary ?? _randomString() {
if (header.items.isEmpty) {
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].
int computeContentLength(int totalFileSize) {
var len = 0;
for (var item in header.items) {
if (item.start == -1) {
if (item.end == -1) {
len += totalFileSize;
} else {
//len += item.end + 1;
len += item.end + 1;
}
} else if (item.end == -1) {
len += totalFileSize - item.start;
//len += totalFileSize - item.start - 1;
} else {
len += item.end - item.start;
}
// Take into consideration the fact that delimiters are written.
len += utf8.encode('--$boundary\r\n').length;
len += utf8.encode('Content-Type: $mimeType\r\n').length;
len += utf8
.encode(
'Content-Range: ${header.rangeUnit} ${item.toContentRange(totalLength)}/$totalLength\r\n\r\n')
.length;
len += 2; // CRLF
}
len += utf8.encode('--$boundary--\r\n').length;
return len;
}
@override
Stream<List<int>> bind(Stream<List<int>> stream) {
var ctrl = StreamController<List<int>>();
Future(() async {
var index = 0;
var enqueued = Queue<List<int>>();
var q = StreamQueue(stream);
Future<List<int>> absorb(int length) async {
var out = BytesBuilder();
while (out.length < length) {
var remaining = length - out.length;
while (out.length < length && enqueued.isNotEmpty) {
remaining = length - out.length;
var blob = enqueued.removeFirst();
if (blob.length > remaining) {
enqueued.addFirst(blob.skip(remaining).toList());
blob = blob.take(remaining).toList();
}
out.add(blob);
index += blob.length;
}
if (out.length < length && await q.hasNext) {
var blob = await q.next;
remaining = length - out.length;
if (blob.length > remaining) {
enqueued.addFirst(blob.skip(remaining).toList());
blob = blob.take(remaining).toList();
}
out.add(blob);
index += blob.length;
}
// If we get this far, and the stream is EMPTY, the user requested
// too many bytes.
if (out.length < length && enqueued.isEmpty && !(await q.hasNext)) {
throw StateError(
'The range denoted is bigger than the size of the input stream.');
}
}
return out.takeBytes();
}
for (var item in header.items) {
var chunk = BytesBuilder();
// Skip until we reach the start index.
while (index < item.start) {
var remaining = item.start - index;
await absorb(remaining);
}
// Next, absorb until we reach the end.
if (item.end == -1) {
while (enqueued.isNotEmpty) {
chunk.add(enqueued.removeFirst());
}
while (await q.hasNext) {
chunk.add(await q.next);
}
} else {
var remaining = item.end - index;
chunk.add(await absorb(remaining));
}
// Next, write the boundary and data.
ctrl.add(utf8.encode('--$boundary\r\n'));
ctrl.add(utf8.encode('Content-Type: $mimeType\r\n'));
ctrl.add(utf8.encode(
'Content-Range: ${header.rangeUnit} ${item.toContentRange(totalLength)}/$totalLength\r\n\r\n'));
ctrl.add(chunk.takeBytes());
ctrl.add(const [$cr, $lf]);
// If this range was unbounded, don't bother looping any further.
if (item.end == -1) break;
}
ctrl.add(utf8.encode('--$boundary--\r\n'));
await ctrl.close();
}).catchError((e) {
ctrl.addError(e as Object);
return null;
});
return ctrl.stream;
}
}
var _rnd = Random();
String _randomString(
{int length = 32,
String validChars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'}) {
var len = _rnd.nextInt((length - 10)) + 10;
var buf = StringBuffer();
while (buf.length < len) {
buf.writeCharCode(validChars.codeUnitAt(_rnd.nextInt(validChars.length)));
}
return buf.toString();
}

View file

@ -0,0 +1,18 @@
class RangeHeaderParseException extends FormatException {
@override
final String message;
RangeHeaderParseException(this.message);
@override
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

@ -0,0 +1,152 @@
import 'package:charcode/charcode.dart';
import 'package:source_span/source_span.dart';
import 'package:string_scanner/string_scanner.dart';
import 'exception.dart';
import 'range_header.dart';
import 'range_header_impl.dart';
import 'range_header_item.dart';
final RegExp _rgxInt = RegExp(r'[0-9]+');
final RegExp _rgxWs = RegExp(r'[ \n\r\t]');
enum TokenType { RANGE_UNIT, COMMA, INT, DASH, EQUALS }
class Token {
final TokenType type;
final SourceSpan? span;
Token(this.type, this.span);
}
List<Token> scan(String text, List<String> allowedRangeUnits) {
var tokens = <Token>[];
var scanner = SpanScanner(text);
while (!scanner.isDone) {
// Skip whitespace
scanner.scan(_rgxWs);
if (scanner.scanChar($comma)) {
tokens.add(Token(TokenType.COMMA, scanner.lastSpan));
} else if (scanner.scanChar($dash)) {
tokens.add(Token(TokenType.DASH, scanner.lastSpan));
} else if (scanner.scan(_rgxInt)) {
tokens.add(Token(TokenType.INT, scanner.lastSpan));
} else if (scanner.scanChar($equal)) {
tokens.add(Token(TokenType.EQUALS, scanner.lastSpan));
} else {
var matched = false;
for (var unit in allowedRangeUnits) {
if (scanner.scan(unit)) {
tokens.add(Token(TokenType.RANGE_UNIT, scanner.lastSpan));
matched = true;
break;
}
}
if (!matched) {
var ch = scanner.readChar();
throw RangeHeaderParseException(
'Unexpected character: "${String.fromCharCode(ch)}"');
}
}
}
return tokens;
}
class Parser {
Token? _current;
int _index = -1;
final List<Token> tokens;
Parser(this.tokens);
Token? get current => _current;
bool get done => _index >= tokens.length - 1;
RangeHeaderParseException _expected(String type) {
var offset = current?.span?.start.offset;
if (offset == null) return RangeHeaderParseException('Expected $type.');
Token? peek;
if (_index < tokens.length - 1) peek = tokens[_index + 1];
if (peek != null && peek.span != null) {
return RangeHeaderParseException(
'Expected $type at offset $offset, found "${peek.span!.text}" instead. \nSource:\n${peek.span?.highlight() ?? peek.type}');
} else {
return RangeHeaderParseException(
'Expected $type at offset $offset, but the header string ended without one.\nSource:\n${current!.span?.highlight() ?? current!.type}');
}
}
bool next(TokenType type) {
if (done) return false;
var tok = tokens[_index + 1];
if (tok.type == type) {
_index++;
_current = tok;
return true;
} else {
return false;
}
}
RangeHeader? parseRangeHeader() {
if (next(TokenType.RANGE_UNIT)) {
var unit = current!.span!.text;
next(TokenType.EQUALS); // Consume =, if any.
var items = <RangeHeaderItem>[];
var item = parseHeaderItem();
while (item != null) {
items.add(item);
// Parse comma
if (next(TokenType.COMMA)) {
item = parseHeaderItem();
} else {
item = null;
}
}
if (items.isEmpty) {
throw _expected('range');
} else {
return RangeHeaderImpl(unit, items);
}
} else {
return null;
}
}
RangeHeaderItem? parseHeaderItem() {
if (next(TokenType.INT)) {
// i.e 500-544, or 600-
var start = int.parse(current!.span!.text);
if (next(TokenType.DASH)) {
if (next(TokenType.INT)) {
return RangeHeaderItem(start, int.parse(current!.span!.text));
} else {
return RangeHeaderItem(start);
}
} else {
throw _expected('"-"');
}
} else if (next(TokenType.DASH)) {
// i.e. -599
if (next(TokenType.INT)) {
return RangeHeaderItem(-1, int.parse(current!.span!.text));
} else {
throw _expected('integer');
}
} else {
return null;
}
}
}

View file

@ -0,0 +1,68 @@
import 'dart:collection';
import 'exception.dart';
import 'parser.dart';
import 'range_header_item.dart';
import 'range_header_impl.dart';
/// Represents the contents of a parsed `Range` header.
abstract class RangeHeader {
/// Returns an immutable list of the ranges that were parsed.
UnmodifiableListView<RangeHeaderItem> get items;
const factory RangeHeader(Iterable<RangeHeaderItem> items,
{String? rangeUnit}) = _ConstantRangeHeader;
/// Eliminates any overlapping [items], sorts them, and folds them all into the most efficient representation possible.
static UnmodifiableListView<RangeHeaderItem> foldItems(
Iterable<RangeHeaderItem> items) {
var out = <RangeHeaderItem>{};
for (var item in items) {
// Remove any overlapping items, consolidate them.
while (out.any((x) => x.overlaps(item))) {
var f = out.firstWhere((x) => x.overlaps(item));
out.remove(f);
item = item.consolidate(f);
}
out.add(item);
}
return UnmodifiableListView(out.toList()..sort());
}
/// Attempts to parse a [RangeHeader] from its [text] representation.
///
/// You can optionally pass a custom list of [allowedRangeUnits].
/// The default is `['bytes']`.
///
/// If [fold] is `true`, the items will be folded into the most compact
/// possible representation.
///
factory RangeHeader.parse(String text,
{Iterable<String>? allowedRangeUnits, bool fold = true}) {
var tokens = scan(text, allowedRangeUnits?.toList() ?? ['bytes']);
var parser = Parser(tokens);
var header = parser.parseRangeHeader();
if (header == null) {
throw InvalidRangeHeaderException('Header is null');
}
var items = foldItems(header.items);
return RangeHeaderImpl(header.rangeUnit, items);
}
/// Returns this header's range unit. Most commonly, this is `bytes`.
String? get rangeUnit;
}
class _ConstantRangeHeader implements RangeHeader {
final Iterable<RangeHeaderItem> items_;
@override
final String? rangeUnit;
const _ConstantRangeHeader(this.items_, {this.rangeUnit = 'bytes'});
@override
UnmodifiableListView<RangeHeaderItem> get items =>
UnmodifiableListView(items_);
}

View file

@ -0,0 +1,20 @@
import 'dart:collection';
import 'range_header.dart';
import 'range_header_item.dart';
/// Represents the contents of a parsed `Range` header.
class RangeHeaderImpl implements RangeHeader {
UnmodifiableListView<RangeHeaderItem>? _cached;
final List<RangeHeaderItem> _items = [];
RangeHeaderImpl(this.rangeUnit, [List<RangeHeaderItem> items = const []]) {
_items.addAll(items);
}
@override
UnmodifiableListView<RangeHeaderItem> get items =>
_cached ??= UnmodifiableListView<RangeHeaderItem>(_items);
@override
final String? rangeUnit;
}

View file

@ -0,0 +1,93 @@
import 'dart:math';
import 'package:quiver/core.dart';
/// Represents an individual range, with an optional start index and optional end index.
class RangeHeaderItem implements Comparable<RangeHeaderItem> {
/// The index at which this chunk begins. May be `-1`.
final int start;
/// The index at which this chunk ends. May be `-1`.
final int end;
const RangeHeaderItem([this.start = -1, this.end = -1]);
/// Joins two items together into the largest possible range.
RangeHeaderItem consolidate(RangeHeaderItem other) {
if (!(other.overlaps(this))) {
throw ArgumentError('The two ranges do not overlap.');
}
return RangeHeaderItem(min(start, other.start), max(end, other.end));
}
@override
int get hashCode => hash2(start, end);
@override
bool operator ==(other) =>
other is RangeHeaderItem && other.start == start && other.end == end;
bool overlaps(RangeHeaderItem other) {
if (other.start <= start) {
return other.end < start;
} else if (other.start > start) {
return other.start <= end;
}
return false;
}
@override
int compareTo(RangeHeaderItem other) {
if (other.start > start) {
return -1;
} else if (other.start == start) {
if (other.end == end) {
return 0;
} else if (other.end < end) {
return 1;
} else {
return -1;
}
} else if (other.start < start) {
return 1;
} else {
return -1;
}
}
@override
String toString() {
if (start > -1 && end > -1) {
return '$start-$end';
} else if (start > -1) {
return '$start-';
} else {
return '-$end';
}
}
/// Creates a representation of this instance suitable for a `Content-Range` header.
///
/// This can only be used if the user request only one range. If not, send a
/// `multipart/byteranges` response.
///
/// Please adhere to the standard!!!
/// http://httpwg.org/specs/rfc7233.html
String toContentRange([int? totalSize]) {
// var maxIndex = totalSize != null ? (totalSize - 1).toString() : '*';
var s = start > -1 ? start : 0;
if (end == -1) {
if (totalSize == null) {
throw UnsupportedError(
'If the end of this range is unknown, `totalSize` must not be null.');
} else {
// if (end == totalSize - 1) {
return '$s-${totalSize - 1}/$totalSize';
}
}
return '$s-$end/$totalSize';
}
}

View file

@ -0,0 +1,18 @@
name: belatuk_range_header
version: 4.0.0
description: Range header parser for Dart. Beyond parsing, a stream transformer is included.
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/range_header
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
async: ^2.6.0
charcode: ^1.2.0
quiver: ^3.0.1
source_span: ^1.8.1
string_scanner: ^1.1.0
dev_dependencies:
file: ^6.1.0
http_parser: ^4.0.0
logging: ^1.0.1
test: ^1.17.8
lints: ^1.0.0

View file

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

View file

@ -1,6 +1,6 @@
# Belatuk Symbol Table
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dartlang.org/packages/belatuk_symbol_table)
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dev/packages/belatuk_symbol_table)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/symbol_table/LICENSE)

View file

@ -0,0 +1 @@
language: dart

View file

@ -0,0 +1,12 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
Thomas is the current maintainer of the code base. He has refactored and migrated the
code base to support NNBD.
* __[Tobe O](thosakwe@gmail.com)__
Tobe has written much of the original code prior to NNBD migration. He has moved on and
is no longer involved with the project.

View file

@ -0,0 +1,22 @@
# Change Log
## 3.0.2
* Upgraded from `pendantic` to `lints` linter
* Fixed linter warnings
## 3.0.1
* Updated to use non nullable results
## 3.0.0
* Migrated to support Dart SDK 2.12.x NNBD
## 2.0.0
* Dart 2 updates.
## 0.0.1
* Initial version, created by Stagehand

View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2021, dukefirehawk.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,26 @@
# User Agent Analyzer
[![version](https://img.shields.io/badge/pub-v3.0.2-brightgreen)](https://pub.dev/packages/user_agent_analyzer)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/user_agent/LICENSE)
**Replacement of `package:user_agent` with breaking changes to support NNBD.**
A library to identify the type of devices and web browsers based on `User-Agent` string.
Runs anywhere.
```dart
void main() async {
app.get('/', (req, res) async {
var ua = UserAgent(req.headers.value('user-agent'));
if (ua.isChrome) {
res.redirect('/upgrade-your-browser');
return;
} else {
// ...
}
});
}
```

View file

@ -0,0 +1 @@
include: package:lints/recommended.yaml

View file

@ -0,0 +1,7 @@
import 'package:user_agent_analyzer/user_agent_analyzer.dart';
void main() {
var ua = UserAgent(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36');
print(ua.isChrome);
}

View file

@ -0,0 +1,235 @@
library user_agent_analyzer;
/// Utils for device detection.
class UserAgent {
bool _isChrome = false;
bool _isOpera = false;
bool _isIE = false;
bool _isFirefox = false;
bool _isWebKit = false;
String? _cachedCssPrefix;
String? _cachedPropertyPrefix;
final String value, _lowerValue;
static const List<String> knownMobileUserAgentPrefixes = [
'w3c ',
'w3c-',
'acs-',
'alav',
'alca',
'amoi',
'audi',
'avan',
'benq',
'bird',
'blac',
'blaz',
'brew',
'cell',
'cldc',
'cmd-',
'dang',
'doco',
'eric',
'hipt',
'htc_',
'inno',
'ipaq',
'ipod',
'jigs',
'kddi',
'keji',
'leno',
'lg-c',
'lg-d',
'lg-g',
'lge-',
'lg/u',
'maui',
'maxo',
'midp',
'mits',
'mmef',
'mobi',
'mot-',
'moto',
'mwbp',
'nec-',
'newt',
'noki',
'palm',
'pana',
'pant',
'phil',
'play',
'port',
'prox',
'qwap',
'sage',
'sams',
'sany',
'sch-',
'sec-',
'send',
'seri',
'sgh-',
'shar',
'sie-',
'siem',
'smal',
'smar',
'sony',
'sph-',
'symb',
't-mo',
'teli',
'tim-',
'tosh',
'tsm-',
'upg1',
'upsi',
'vk-v',
'voda',
'wap-',
'wapa',
'wapi',
'wapp',
'wapr',
'webc',
'winw',
'winw',
'xda ',
'xda-'
];
static const List<String> knownMobileUserAgentKeywords = [
'blackberry',
'webos',
'ipod',
'lge vx',
'midp',
'maemo',
'mmp',
'mobile',
'netfront',
'hiptop',
'nintendo DS',
'novarra',
'openweb',
'opera mobi',
'opera mini',
'palm',
'psp',
'phone',
'smartphone',
'symbian',
'up.browser',
'up.link',
'wap',
'windows ce'
];
static const List<String> knownTabletUserAgentKeywords = [
'ipad',
'playbook',
'hp-tablet',
'kindle'
];
UserAgent(this.value) : _lowerValue = value.toLowerCase();
/// Determines if the user agent string contains the desired string. Case-insensitive.
bool contains(String needle) => _lowerValue.contains(needle.toLowerCase());
bool get isDesktop => isMacOS || (!isMobile && !isTablet);
bool get isTablet => knownTabletUserAgentKeywords.any(contains);
bool get isMobile => knownMobileUserAgentKeywords.any(contains);
bool get isMacOS => contains('Macintosh') || contains('Mac OS X');
bool get isSafari => contains('Safari');
bool get isAndroid => contains('android');
bool get isAndroidPhone => contains('android') && contains('mobile');
bool get isAndroidTablet => contains('android') && !contains('mobile');
bool get isWindows => contains('windows');
bool get isWindowsPhone => isWindows && contains('phone');
bool get isWindowsTablet => isWindows && contains('touch');
bool get isBlackberry =>
contains('blackberry') || contains('bb10') || contains('rim');
bool get isBlackberryPhone => isBlackberry && !contains('tablet');
bool get isBlackberryTablet => isBlackberry && contains('tablet');
/// Determines if the current device is running Chrome.
bool get isChrome {
_isChrome = value.contains('Chrome', 0);
return _isChrome;
}
/// Determines if the current device is running Opera.
bool get isOpera {
_isOpera = value.contains('Opera', 0);
return _isOpera;
}
/// Determines if the current device is running Internet Explorer.
bool get isIE {
_isIE = !isOpera && value.contains('Trident/', 0);
return _isIE;
}
/// Determines if the current device is running Firefox.
bool get isFirefox {
_isFirefox = value.contains('Firefox', 0);
return _isFirefox;
}
/// Determines if the current device is running WebKit.
bool get isWebKit {
_isWebKit = !isOpera && value.contains('WebKit', 0);
return _isWebKit;
}
/// Gets the CSS property prefix for the current platform.
String get cssPrefix {
var prefix = _cachedCssPrefix;
if (prefix != null) return prefix;
if (isFirefox) {
prefix = '-moz-';
} else if (isIE) {
prefix = '-ms-';
} else if (isOpera) {
prefix = '-o-';
} else {
prefix = '-webkit-';
}
return _cachedCssPrefix = prefix;
}
/// Prefix as used for JS property names.
String get propertyPrefix {
var prefix = _cachedPropertyPrefix;
if (prefix != null) return prefix;
if (isFirefox) {
prefix = 'moz';
} else if (isIE) {
prefix = 'ms';
} else if (isOpera) {
prefix = 'o';
} else {
prefix = 'webkit';
}
return _cachedPropertyPrefix = prefix;
}
}

View file

@ -0,0 +1,9 @@
name: user_agent_analyzer
version: 3.0.2
description: A library to identify the type of devices and web browsers based on User-Agent string.
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/user_agent
environment:
sdk: '>=2.12.0 <3.0.0'
dev_dependencies:
test: ^1.17.8
lints: ^1.0.0

View file

@ -0,0 +1,30 @@
// Copyright (c) 2017, thosakwe. All rights reserved. Use of this source code
// is governed by a BSD-style license that can be found in the LICENSE file.
import 'package:user_agent_analyzer/user_agent_analyzer.dart';
import 'package:test/test.dart';
void main() {
test('chrome', () {
var ua = UserAgent(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36');
expect([ua.isChrome, ua.isWebKit, ua.isSafari, ua.isDesktop, ua.isMacOS],
everyElement(isTrue));
expect([ua.isFirefox, ua.isIE, ua.isOpera, ua.isMobile, ua.isTablet],
everyElement(isFalse));
expect([
ua.isAndroid,
ua.isAndroidPhone,
ua.isAndroidTablet,
ua.isBlackberry,
ua.isBlackberryPhone,
ua.isBlackberryTablet,
ua.isWindows,
ua.isWindowsPhone,
ua.isWindowsTablet
], everyElement(isFalse));
expect(ua.cssPrefix, equals('-webkit-'));
expect(ua.propertyPrefix, equals('webkit'));
});
}