Added range_header, user_agent and body_parser
This commit is contained in:
parent
6fb188b265
commit
7a2bb8f5da
51 changed files with 2104 additions and 7 deletions
|
@ -6,9 +6,13 @@ This repository contains the common utility packages required for developing dar
|
||||||
|
|
||||||
## Available Packages
|
## Available Packages
|
||||||
|
|
||||||
* Html Builder
|
* Body Parser
|
||||||
* Code Buffer
|
* Code Buffer
|
||||||
* Combinator
|
* Combinator
|
||||||
|
* Html Builder
|
||||||
* Merge Map
|
* Merge Map
|
||||||
|
* Pub Sub
|
||||||
|
* Range Header
|
||||||
* Symbol Table
|
* Symbol Table
|
||||||
|
* User Agent
|
||||||
|
|
4
packages/body_parser/.travis.yml
Normal file
4
packages/body_parser/.travis.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
language: dart
|
||||||
|
dart:
|
||||||
|
- dev
|
||||||
|
- stable
|
12
packages/body_parser/AUTHORS.md
Normal file
12
packages/body_parser/AUTHORS.md
Normal 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.
|
32
packages/body_parser/CHANGELOG.md
Normal file
32
packages/body_parser/CHANGELOG.md
Normal 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`
|
29
packages/body_parser/LICENSE
Normal file
29
packages/body_parser/LICENSE
Normal 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.
|
75
packages/body_parser/README.md
Normal file
75
packages/body_parser/README.md
Normal 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);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
1
packages/body_parser/analysis_options.yaml
Normal file
1
packages/body_parser/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
include: package:lints/recommended.yaml
|
61
packages/body_parser/example/main.dart
Normal file
61
packages/body_parser/example/main.dart
Normal 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);
|
||||||
|
}
|
6
packages/body_parser/example/post.lua
Normal file
6
packages/body_parser/example/post.lua
Normal 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"
|
6
packages/body_parser/lib/belatuk_body_parser.dart
Normal file
6
packages/body_parser/lib/belatuk_body_parser.dart
Normal 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';
|
28
packages/body_parser/lib/src/body_parse_result.dart
Normal file
28
packages/body_parser/lib/src/body_parse_result.dart
Normal 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;
|
||||||
|
}
|
7
packages/body_parser/lib/src/chunk.dart
Normal file
7
packages/body_parser/lib/src/chunk.dart
Normal 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;
|
||||||
|
}
|
17
packages/body_parser/lib/src/file_upload_info.dart
Normal file
17
packages/body_parser/lib/src/file_upload_info.dart
Normal 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 []});
|
||||||
|
}
|
22
packages/body_parser/lib/src/get_value.dart
Normal file
22
packages/body_parser/lib/src/get_value.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
packages/body_parser/lib/src/map_from_uri.dart
Normal file
44
packages/body_parser/lib/src/map_from_uri.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
147
packages/body_parser/lib/src/parse_body.dart
Normal file
147
packages/body_parser/lib/src/parse_body.dart
Normal 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]);
|
||||||
|
}
|
14
packages/body_parser/pubspec.yaml
Normal file
14
packages/body_parser/pubspec.yaml
Normal 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
|
156
packages/body_parser/test/form_data_test.dart
Normal file
156
packages/body_parser/test/form_data_test.dart
Normal 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));
|
||||||
|
});
|
||||||
|
}
|
174
packages/body_parser/test/server_test.dart
Normal file
174
packages/body_parser/test/server_test.dart
Normal 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'}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
# Belatuk Code Buffer
|
# 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)
|
[![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)
|
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/code_buffer/LICENSE)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Belatuk Combinator
|
# 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)
|
[![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)
|
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/combinator/LICENSE)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Betaluk Html Builder
|
# 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)
|
[![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)
|
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/html_builder/LICENSE)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Belatuk Merge Map
|
# 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)
|
[![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)
|
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/merge_map/LICENSE)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Belatuk Pub Sub
|
# 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)
|
[![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)
|
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/pub_sub/LICENSE)
|
||||||
|
|
||||||
|
|
12
packages/range_header/AUTHORS.md
Normal file
12
packages/range_header/AUTHORS.md
Normal 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.
|
34
packages/range_header/CHANGELOG.md
Normal file
34
packages/range_header/CHANGELOG.md
Normal 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.
|
29
packages/range_header/LICENSE
Normal file
29
packages/range_header/LICENSE
Normal 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.
|
40
packages/range_header/README.md
Normal file
40
packages/range_header/README.md
Normal 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);
|
||||||
|
}
|
||||||
|
```
|
1
packages/range_header/analysis_options.yaml
Normal file
1
packages/range_header/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
include: package:lints/recommended.yaml
|
28
packages/range_header/example/main.dart
Normal file
28
packages/range_header/example/main.dart
Normal 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);
|
||||||
|
}
|
103
packages/range_header/example/server.dart
Normal file
103
packages/range_header/example/server.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
4
packages/range_header/lib/angel3_range_header.dart
Normal file
4
packages/range_header/lib/angel3_range_header.dart
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export 'src/converter.dart';
|
||||||
|
export 'src/exception.dart';
|
||||||
|
export 'src/range_header.dart';
|
||||||
|
export 'src/range_header_item.dart';
|
171
packages/range_header/lib/src/converter.dart
Normal file
171
packages/range_header/lib/src/converter.dart
Normal 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();
|
||||||
|
}
|
18
packages/range_header/lib/src/exception.dart
Normal file
18
packages/range_header/lib/src/exception.dart
Normal 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';
|
||||||
|
}
|
152
packages/range_header/lib/src/parser.dart
Normal file
152
packages/range_header/lib/src/parser.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
packages/range_header/lib/src/range_header.dart
Normal file
68
packages/range_header/lib/src/range_header.dart
Normal 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_);
|
||||||
|
}
|
20
packages/range_header/lib/src/range_header_impl.dart
Normal file
20
packages/range_header/lib/src/range_header_impl.dart
Normal 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;
|
||||||
|
}
|
93
packages/range_header/lib/src/range_header_item.dart
Normal file
93
packages/range_header/lib/src/range_header_item.dart
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
18
packages/range_header/pubspec.yaml
Normal file
18
packages/range_header/pubspec.yaml
Normal 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
|
95
packages/range_header/test/all_test.dart
Normal file
95
packages/range_header/test/all_test.dart
Normal 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');
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
# Belatuk Symbol Table
|
# 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)
|
[![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)
|
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/symbol_table/LICENSE)
|
||||||
|
|
||||||
|
|
1
packages/user_agent/.travis.yml
Normal file
1
packages/user_agent/.travis.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
language: dart
|
12
packages/user_agent/AUTHORS.md
Normal file
12
packages/user_agent/AUTHORS.md
Normal 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.
|
22
packages/user_agent/CHANGELOG.md
Normal file
22
packages/user_agent/CHANGELOG.md
Normal 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
|
29
packages/user_agent/LICENSE
Normal file
29
packages/user_agent/LICENSE
Normal 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.
|
26
packages/user_agent/README.md
Normal file
26
packages/user_agent/README.md
Normal 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 {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
1
packages/user_agent/analysis_options.yaml
Normal file
1
packages/user_agent/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
include: package:lints/recommended.yaml
|
7
packages/user_agent/example/example.dart
Normal file
7
packages/user_agent/example/example.dart
Normal 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);
|
||||||
|
}
|
235
packages/user_agent/lib/user_agent_analyzer.dart
Normal file
235
packages/user_agent/lib/user_agent_analyzer.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
9
packages/user_agent/pubspec.yaml
Normal file
9
packages/user_agent/pubspec.yaml
Normal 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
|
30
packages/user_agent/test/user_agent_test.dart
Normal file
30
packages/user_agent/test/user_agent_test.dart
Normal 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'));
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue