SMP protocol commands encoding/decoding (#111)

* SMP protocol commands encoding/decoding

* change "var" to type

* Parser `word` method now returns null if the word is empty

* refactor Parser `word` method

* move Parser `end` getter

* add linter rules, move linter options to root

* remove omit_local_variable_types linter rule

* ci: only build haskell on changes
This commit is contained in:
Evgeny Poberezkin 2021-10-03 19:10:11 +01:00 committed by GitHub
parent fec99d526a
commit a39cd2990f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1087 additions and 12 deletions

View File

@ -7,6 +7,9 @@ on:
- v5
tags:
- "v*"
paths:
- haskell/**
- .github/workflows/build_haskell.yml
pull_request:
jobs:

View File

@ -23,7 +23,8 @@ linter:
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
prefer_double_quotes: true
constant_identifier_names: false
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@ -0,0 +1,44 @@
include: package:lints/recommended.yaml
linter:
rules:
prefer_double_quotes: true
constant_identifier_names: false
always_declare_return_types: true
avoid_dynamic_calls: true
avoid_empty_else: true
avoid_relative_lib_imports: true
avoid_shadowing_type_parameters: true
avoid_slow_async_io: true
avoid_types_as_parameter_names: true
await_only_futures: true
camel_case_extensions: true
camel_case_types: true
cancel_subscriptions: true
curly_braces_in_flow_control_structures: true
directives_ordering: true
empty_catches: true
hash_and_equals: true
iterable_contains_unrelated_type: true
list_remove_unrelated_type: true
no_adjacent_strings_in_list: true
no_duplicate_case_values: true
package_api_docs: true
package_prefixed_library_names: true
prefer_generic_function_type_aliases: true
prefer_is_empty: true
prefer_is_not_empty: true
prefer_iterable_whereType: true
prefer_typing_uninitialized_variables: true
sort_child_properties_last: true
test_types_in_equals: true
throw_in_finally: true
unawaited_futures: true
unnecessary_import: true
unnecessary_null_aware_assignments: true
unnecessary_statements: true
unnecessary_type_check: true
unrelated_type_equality_checks: true
unsafe_html: true
use_full_hex_values_for_flutter_colors: true
valid_regexps: true

10
packages/simplexmq/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# Files and directories created by pub.
.dart_tool/
.packages
# Conventional directory for build outputs.
build/
# Omit committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock

View File

@ -0,0 +1,3 @@
## 1.0.0
- Initial version.

View File

@ -0,0 +1,39 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

View File

@ -0,0 +1,6 @@
// import 'package:simplexmq/simplexmq.dart';
// void main() {
// var awesome = Awesome();
// print('awesome: ${awesome.isAwesome}');
// }

View File

@ -0,0 +1,8 @@
/// Support for doing something awesome.
///
/// More dartdocs go here.
library simplexmq;
export "src/protocol.dart";
// TODO: Export any libraries intended for clients of this package.

View File

@ -0,0 +1,132 @@
import "dart:typed_data";
Uint8List encodeAscii(String s) => Uint8List.fromList(s.codeUnits);
String decodeAscii(Uint8List b) => String.fromCharCodes(b);
Uint8List concat(Uint8List b1, Uint8List b2) {
final a = Uint8List(b1.length + b2.length);
a.setAll(0, b1);
a.setAll(b1.length, b2);
return a;
}
T fold<T, E>(List<E> xs, T Function(T, E) combine, T initial) {
T res = initial;
for (final x in xs) {
res = combine(res, x);
}
return res;
}
Uint8List concatN(List<Uint8List> bs) {
final aLen = fold(bs, (int size, Uint8List b) => size + b.length, 0);
final a = Uint8List(aLen);
fold(bs, (int offset, Uint8List b) {
a.setAll(offset, b);
return offset + b.length;
}, 0);
return a;
}
final charSpace = " ".codeUnitAt(0);
final charEqual = "=".codeUnitAt(0);
final empty = Uint8List(0);
Uint8List unwords(Uint8List b1, Uint8List b2) {
final a = Uint8List(b1.length + b2.length + 1);
a.setAll(0, b1);
a[b1.length] = charSpace;
a.setAll(b1.length + 1, b2);
return a;
}
Uint8List unwordsN(List<Uint8List> bs) {
int i = bs.length;
int size = bs.length - 1;
while (i > 0) {
size += bs[--i].length;
}
final a = Uint8List(size);
int offset = 0;
for (i = 0; i < bs.length - 1; i++) {
final b = bs[i];
a.setAll(offset, b);
offset += b.length;
a[offset++] = charSpace;
}
a.setAll(offset, bs[i]);
return a;
}
final _base64chars = Uint8List.fromList(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
.codeUnits);
List<int?> __base64lookup() {
final a = List<int?>.filled(256, null);
for (int i = 0; i < _base64chars.length; i++) {
a[_base64chars[i]] = i;
}
return a;
}
final _base64lookup = __base64lookup();
Uint8List encode64(Uint8List a) {
final len = a.length;
final b64len = (len / 3).ceil() * 4;
final b64 = Uint8List(b64len);
int j = 0;
for (int i = 0; i < len; i += 3) {
final e1 = i + 1 < len ? a[i + 1] : 0;
final e2 = i + 2 < len ? a[i + 2] : 0;
b64[j++] = _base64chars[a[i] >> 2];
b64[j++] = _base64chars[((a[i] & 3) << 4) | (e1 >> 4)];
b64[j++] = _base64chars[((e1 & 15) << 2) | (e2 >> 6)];
b64[j++] = _base64chars[e2 & 63];
}
if (len % 3 != 0) b64[b64len - 1] = charEqual;
if (len % 3 == 1) b64[b64len - 2] = charEqual;
return b64;
}
Uint8List? decode64(Uint8List b64) {
int len = b64.length;
if (len % 4 != 0) return null;
int bLen = (len * 3) >> 2;
if (b64[len - 1] == charEqual) {
len--;
bLen--;
if (b64[len - 1] == charEqual) {
len--;
bLen--;
}
}
final bytes = Uint8List(bLen);
int i = 0;
int pos = 0;
while (i < len) {
final enc1 = _base64lookup[b64[i++]];
final enc2 = i < len ? _base64lookup[b64[i++]] : 0;
final enc3 = i < len ? _base64lookup[b64[i++]] : 0;
final enc4 = i < len ? _base64lookup[b64[i++]] : 0;
if (enc1 == null || enc2 == null || enc3 == null || enc4 == null) {
return null;
}
bytes[pos++] = (enc1 << 2) | (enc2 >> 4);
int p = pos++;
if (p < bLen) bytes[p] = ((enc2 & 15) << 4) | (enc3 >> 2);
p = pos++;
if (p < bLen) bytes[p] = ((enc3 & 3) << 6) | (enc4 & 63);
}
return bytes;
}

View File

@ -0,0 +1,136 @@
import "dart:typed_data";
import "buffer.dart";
typedef BinaryTags<T> = Map<T, Uint8List>;
int cc(String c) => c.codeUnitAt(0);
final char0 = cc("0");
final char9 = cc("9");
final charLowerA = cc("a");
final charLowerZ = cc("z");
final charUpperA = cc("A");
final charUpperZ = cc("Z");
final charPlus = cc("+");
final charSlash = cc("/");
class Parser {
final Uint8List _s;
int _pos = 0;
bool _fail = false;
Parser(this._s);
bool get fail => _fail;
bool get end => _pos >= _s.length;
// only calls `parse` if the parser did not previously fail
T? _run<T>(T? Function() parse) {
if (_fail || _pos >= _s.length) {
_fail = true;
return null;
}
final res = parse();
if (res == null) _fail = true;
return res;
}
// takes a required number of bytes
Uint8List? take(int len) => _run(() {
final end = _pos + len;
if (end > _s.length) return null;
final res = _s.sublist(_pos, end);
_pos = end;
return res;
});
// takes chars (> 0) while condition is true; function isAlphaNum or isDigit can be used
Uint8List? takeWhile1(bool Function(int) f) => _run(() {
final pos = _pos;
while (f(_s[_pos])) {
_pos++;
}
return _pos > pos ? _s.sublist(pos, _pos) : null;
});
// takes the non-empty word until the first space or until the end of the string
Uint8List? word() => _run(() {
int pos = _s.indexOf(charSpace, _pos);
if (pos == -1) pos = _s.length;
if (pos <= _pos) return null;
final res = _s.sublist(_pos, pos);
_pos = pos;
return res;
});
bool? str(Uint8List s) => _run(() {
for (int i = 0, j = _pos; i < s.length; i++, j++) {
if (s[i] != _s[j]) return null;
}
_pos += s.length;
return true;
});
// takes space
bool? space() => _run(() {
if (_s[_pos] == charSpace) {
_pos++;
return true;
}
});
int? decimal() => _run(() {
final s = takeWhile1(isDigit);
if (s == null) return null;
int n = 0;
for (int i = 0; i < s.length; i++) {
n *= 10;
n += s[i] - char0;
}
return n;
});
DateTime? datetime() => _run(() {
final s = word();
if (s != null) return DateTime.tryParse(decodeAscii(s));
});
// takes base-64 encoded string and returns decoded binary
Uint8List? base64() => _run(() {
bool tryCharEqual() {
final ok = _pos < _s.length && _s[_pos] == charEqual;
if (ok) _pos++;
return ok;
}
final pos = _pos;
int c;
do {
c = _s[_pos];
} while ((isAlphaNum(c) || c == charPlus || c == charSlash) &&
++_pos < _s.length);
if (tryCharEqual()) tryCharEqual();
return _pos > pos ? decode64(_s.sublist(pos, _pos)) : null;
});
// takes one of the binary tags and returns its key
T? someStr<T>(BinaryTags<T> ts) => _run(() {
outer:
for (final t in ts.entries) {
final s = t.value;
for (int i = 0, j = _pos; i < s.length; i++, j++) {
if (s[i] != _s[j]) continue outer;
}
_pos += s.length;
return t.key;
}
return null;
});
}
bool isDigit(int c) => c >= char0 && c <= char9;
bool isAlphaNum(int c) =>
(c >= char0 && c <= char9) ||
(c >= charLowerA && c <= charLowerZ) ||
(c >= charUpperA && c <= charUpperZ);

View File

@ -0,0 +1,268 @@
import "dart:typed_data";
import "buffer.dart";
import "parser.dart";
abstract class SMPCommand {
Uint8List serialize();
}
abstract class ClientCommand extends SMPCommand {}
abstract class BrokerCommand extends SMPCommand {}
final rsaPrefix = encodeAscii("rsa:");
Uint8List serializePubKey(Uint8List rcvPubKey) =>
concat(rsaPrefix, encode64(rcvPubKey));
final Uint8List cNEW = encodeAscii("NEW");
final Uint8List cSUB = encodeAscii("SUB");
final Uint8List cKEY = encodeAscii("KEY");
final Uint8List cACK = encodeAscii("ACK");
final Uint8List cOFF = encodeAscii("OFF");
final Uint8List cDEL = encodeAscii("DEL");
final Uint8List cSEND = encodeAscii("SEND");
final Uint8List cPING = encodeAscii("PING");
final Uint8List cIDS = encodeAscii("IDS");
final Uint8List cMSG = encodeAscii("MSG");
final Uint8List cEND = encodeAscii("END");
final Uint8List cOK = encodeAscii("OK");
final Uint8List cERR = encodeAscii("ERR");
final Uint8List cPONG = encodeAscii("PONG");
enum SMPCmdTag {
NEW,
SUB,
KEY,
ACK,
OFF,
DEL,
SEND,
PING,
IDS,
MSG,
END,
OK,
ERR,
PONG,
}
final BinaryTags<SMPCmdTag> smpCmdTags = {
SMPCmdTag.NEW: cNEW,
SMPCmdTag.SUB: cSUB,
SMPCmdTag.KEY: cKEY,
SMPCmdTag.ACK: cACK,
SMPCmdTag.OFF: cOFF,
SMPCmdTag.DEL: cDEL,
SMPCmdTag.SEND: cSEND,
SMPCmdTag.PING: cPING,
SMPCmdTag.IDS: cIDS,
SMPCmdTag.MSG: cMSG,
SMPCmdTag.END: cEND,
SMPCmdTag.OK: cOK,
SMPCmdTag.ERR: cERR,
SMPCmdTag.PONG: cPONG,
};
class NEW extends ClientCommand {
final Uint8List rcvPubKey;
NEW(this.rcvPubKey);
@override
Uint8List serialize() => unwords(cNEW, serializePubKey(rcvPubKey));
}
class SUB extends ClientCommand {
@override
Uint8List serialize() => cSUB;
}
class KEY extends ClientCommand {
final Uint8List sndPubKey;
KEY(this.sndPubKey);
@override
Uint8List serialize() => unwords(cKEY, serializePubKey(sndPubKey));
}
class ACK extends ClientCommand {
@override
Uint8List serialize() => cACK;
}
class OFF extends ClientCommand {
@override
Uint8List serialize() => cOFF;
}
class DEL extends ClientCommand {
@override
Uint8List serialize() => cDEL;
}
List<Uint8List> serializeMsg(Uint8List msg) =>
[encodeAscii(msg.length.toString()), msg, empty];
class SEND extends ClientCommand {
final Uint8List msgBody;
SEND(this.msgBody);
@override
Uint8List serialize() => unwordsN([cSEND, ...serializeMsg(msgBody)]);
}
class PING extends ClientCommand {
@override
Uint8List serialize() => cPING;
}
class IDS extends BrokerCommand {
final Uint8List rcvId;
final Uint8List sndId;
IDS(this.rcvId, this.sndId) : super();
@override
Uint8List serialize() => unwordsN([cIDS, encode64(rcvId), encode64(sndId)]);
}
class MSG extends BrokerCommand {
final Uint8List msgId;
final DateTime ts;
final Uint8List msgBody;
MSG(this.msgId, this.ts, this.msgBody);
@override
Uint8List serialize() => unwordsN([
cMSG,
encode64(msgId),
encodeAscii(ts.toIso8601String()),
...serializeMsg(msgBody)
]);
}
class END extends BrokerCommand {
@override
Uint8List serialize() => cEND;
}
class OK extends BrokerCommand {
@override
Uint8List serialize() => cOK;
}
enum ErrorType { BLOCK, CMD, AUTH, QUOTA, NO_MSG, INTERNAL }
final BinaryTags<ErrorType> errorTags = {
ErrorType.BLOCK: encodeAscii("BLOCK"),
ErrorType.CMD: encodeAscii("CMD"),
ErrorType.AUTH: encodeAscii("AUTH"),
ErrorType.QUOTA: encodeAscii("QUOTA"),
ErrorType.NO_MSG: encodeAscii("NO_MSG"),
ErrorType.INTERNAL: encodeAscii("INTERNAL"),
};
enum CmdErrorType { PROHIBITED, KEY_SIZE, SYNTAX, NO_AUTH, HAS_AUTH, NO_QUEUE }
final BinaryTags<CmdErrorType> cmdErrorTags = {
CmdErrorType.PROHIBITED: encodeAscii("PROHIBITED"),
CmdErrorType.KEY_SIZE: encodeAscii("KEY_SIZE"),
CmdErrorType.SYNTAX: encodeAscii("SYNTAX"),
CmdErrorType.NO_AUTH: encodeAscii("NO_AUTH"),
CmdErrorType.HAS_AUTH: encodeAscii("HAS_AUTH"),
CmdErrorType.NO_QUEUE: encodeAscii("NO_QUEUE"),
};
class ERR extends BrokerCommand {
final ErrorType err;
final CmdErrorType? cmdErr;
ERR(this.err)
: cmdErr = err == ErrorType.CMD
? throw ArgumentError("CMD error should be created with ERR.CMD")
: null;
ERR.cmd(this.cmdErr) : err = ErrorType.CMD;
@override
Uint8List serialize() {
final _err = errorTags[err]!;
return cmdErr == null
? unwords(cERR, _err)
: unwordsN([cERR, _err, cmdErrorTags[cmdErr!]!]);
}
}
class PONG extends BrokerCommand {
@override
Uint8List serialize() => cPONG;
}
final Map<SMPCmdTag, SMPCommand? Function(Parser p)> smpCmdParsers = {
SMPCmdTag.NEW: (p) {
p.space();
final key = pubKeyP(p);
if (key != null) return NEW(key);
},
SMPCmdTag.SUB: (_) => SUB(),
SMPCmdTag.KEY: (p) {
p.space();
final key = pubKeyP(p);
if (key != null) return KEY(key);
},
SMPCmdTag.ACK: (_) => ACK(),
SMPCmdTag.OFF: (_) => OFF(),
SMPCmdTag.DEL: (_) => DEL(),
SMPCmdTag.SEND: (p) {
p.space();
final msg = messageP(p);
if (msg != null) return SEND(msg);
},
SMPCmdTag.PING: (_) => PING(),
SMPCmdTag.IDS: (p) {
p.space();
final rId = p.base64();
p.space();
final sId = p.base64();
if (rId != null && sId != null) return IDS(rId, sId);
},
SMPCmdTag.MSG: (p) {
p.space();
final msgId = p.base64();
p.space();
final ts = p.datetime();
p.space();
final msg = messageP(p);
if (msgId != null && ts != null && msg != null) return MSG(msgId, ts, msg);
},
SMPCmdTag.END: (_) => END(),
SMPCmdTag.OK: (_) => OK(),
SMPCmdTag.ERR: (p) {
p.space();
final err = p.someStr(errorTags);
if (err == ErrorType.CMD) {
p.space();
final cmdErr = p.someStr(cmdErrorTags);
if (cmdErr != null) return ERR.cmd(cmdErr);
} else if (err != null) {
return ERR(err);
}
},
SMPCmdTag.PONG: (_) => PONG(),
};
SMPCommand? smpCommandP(Parser p) {
final cmd = p.someStr(smpCmdTags);
return p.fail ? null : smpCmdParsers[cmd]!(p);
}
SMPCommand? parseSMPCommand(Uint8List s) {
final p = Parser(s);
final cmd = smpCommandP(p);
if (cmd != null && p.end) return cmd;
}
Uint8List? pubKeyP(Parser p) {
p.str(rsaPrefix);
return p.base64();
}
Uint8List? messageP(Parser p) {
final len = p.decimal();
p.space();
Uint8List? msg;
if (len != null) msg = p.take(len);
p.space();
return p.fail ? null : msg;
}

View File

@ -0,0 +1,15 @@
name: simplexmq
description: A starting point for Dart libraries or applications.
version: 0.0.1
# homepage: https://www.example.com
environment:
sdk: '>=2.14.0 <3.0.0'
# dependencies:
# path: ^1.8.0
dev_dependencies:
lints: ^1.0.0
test: ^1.16.0

View File

@ -0,0 +1,66 @@
import "dart:typed_data";
import "package:simplexmq/src/buffer.dart";
import "package:test/test.dart";
final hello123 = Uint8List.fromList([104, 101, 108, 108, 111, 49, 50, 51]);
class Base64Test {
final String description;
final String binary;
final String base64;
Base64Test(this.binary, this.base64) : description = binary;
Base64Test.withDescription(this.description, this.binary, this.base64);
}
void main() {
group("ascii encoding/decoding", () {
test("encodeAscii", () {
expect(encodeAscii("hello123"), hello123);
});
test("decodeAscii", () {
expect(decodeAscii(hello123), "hello123");
});
});
group("base-64 encoding/decoding", () {
String allBinaryChars() {
final a = Uint8List(256);
for (var i = 0; i < 256; i++) {
a[i] = i;
}
return decodeAscii(a);
}
final base64tests = [
Base64Test("\x12\x34\x56\x78", "EjRWeA=="),
Base64Test("hello123", "aGVsbG8xMjM="),
Base64Test("Hello world", "SGVsbG8gd29ybGQ="),
Base64Test("Hello worlds!", "SGVsbG8gd29ybGRzIQ=="),
Base64Test("May", "TWF5"),
Base64Test("Ma", "TWE="),
Base64Test("M", "TQ=="),
Base64Test.withDescription(
"all binary chars",
allBinaryChars(),
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==",
),
];
test("encode64", () {
for (final t in base64tests) {
expect(encode64(encodeAscii(t.binary)), encodeAscii(t.base64));
}
});
test("decode64", () {
for (final t in base64tests) {
expect(decode64(encodeAscii(t.base64)), encodeAscii(t.binary));
}
expect(decode64(encodeAscii("TWE")), null);
expect(decode64(encodeAscii("TWE==")), null);
expect(decode64(encodeAscii("TW!=")), null);
});
});
}

View File

@ -0,0 +1,105 @@
import "dart:typed_data";
import "package:simplexmq/src/buffer.dart";
import "package:simplexmq/src/protocol.dart";
import "package:test/test.dart";
void main() {
group("Parsing & serializing SMP commands", () {
group("valid commands", () {
Null Function() parseSerialize(SMPCommand cmd) => () {
final s = cmd.serialize();
expect(parseSMPCommand(s)?.serialize(), s);
expect(parseSMPCommand(concat(s, Uint8List.fromList([charSpace]))),
null);
};
test("NEW", parseSerialize(NEW(encodeAscii("rsa:1234"))));
test("SUB", parseSerialize(SUB()));
test("KEY", parseSerialize(KEY(encodeAscii("rsa:1234"))));
test("ACK", parseSerialize(ACK()));
test("OFF", parseSerialize(OFF()));
test("DEL", parseSerialize(DEL()));
test("SEND", parseSerialize(SEND(encodeAscii("hello"))));
test("PING", parseSerialize(PING()));
test("IDS", parseSerialize(IDS(encodeAscii("abc"), encodeAscii("def"))));
test(
"MSG",
parseSerialize(MSG(encodeAscii("fgh"), DateTime.now().toUtc(),
encodeAscii("hello"))));
test("END", parseSerialize(END()));
test("OK", parseSerialize(OK()));
test("ERR", parseSerialize(ERR(ErrorType.AUTH)));
test("ERR CMD", parseSerialize(ERR.cmd(CmdErrorType.SYNTAX)));
test("PONG", parseSerialize(PONG()));
});
group("invalid commands", () {
void Function() parseFailure(String s) =>
() => expect(parseSMPCommand(encodeAscii(s)), null);
void Function() parseSuccess(String s) =>
() => expect(parseSMPCommand(encodeAscii(s)) is SMPCommand, true);
group("NEW", () {
test("ok", parseSuccess("NEW rsa:abcd"));
test("no key", parseFailure("NEW"));
test("invalid base64", parseFailure("NEW rsa:abc"));
test("double space", parseFailure("NEW rsa:abcd"));
});
group("KEY", () {
test("ok", parseSuccess("KEY rsa:abcd"));
test("no key", parseFailure("KEY"));
test("invalid base64", parseFailure("KEY rsa:abc"));
test("double space", parseFailure("KEY rsa:abcd"));
});
group("SEND", () {
test("ok", parseSuccess("SEND 5 hello "));
test("no size", parseFailure("SEND hello "));
test("incorrect size", parseFailure("SEND 6 hello "));
test("no trailing space", parseFailure("SEND 5 hello"));
test("double space 1", parseFailure("SEND 5 hello "));
test("double space 2", parseFailure("SEND 5 hello "));
});
group("IDS", () {
test("ok", parseSuccess("IDS abcd efgh"));
test("no IDs", parseFailure("IDS"));
test("only one ID", parseFailure("IDS abcd"));
test("invalid base64 1", parseFailure("IDS abc efgh"));
test("invalid base64 2", parseFailure("IDS abcd efg"));
test("double space 1", parseFailure("IDS abcd efgh"));
test("double space 2", parseFailure("IDS abcd efgh"));
});
group("MSG", () {
final String ts = "2021-10-03T10:50:59.895Z";
test("ok", parseSuccess("MSG abcd $ts 5 hello "));
test("invalid base64", parseFailure("MSG abc $ts 5 hello "));
test("invalid timestamp 1",
parseFailure("MSG abc 2021-10-03T10:50:59.895 5 hello "));
test("invalid timestamp 2",
parseFailure("MSG abc 2021-14-03T10:50:59.895Z 5 hello "));
test("no size", parseFailure("MSG abcd $ts hello "));
test("incorrect size", parseFailure("MSG abcd $ts 6 hello "));
test("no trailing space", parseFailure("MSG abcd $ts 5 hello"));
test("double space 1", parseFailure("MSG abcd $ts 5 hello "));
test("double space 2", parseFailure("MSG abcd $ts 5 hello "));
test("double space 3", parseFailure("MSG abcd $ts 5 hello "));
test("double space 4", parseFailure("MSG abcd $ts 5 hello "));
});
group("ERR", () {
test("ok 1", parseSuccess("ERR AUTH"));
test("ok 2", parseSuccess("ERR CMD SYNTAX"));
test("unknown error", parseFailure("ERR HELLO"));
test("unknown CMD error", parseFailure("ERR CMD HELLO"));
test("bad sub-error", parseFailure("ERR AUTH SYNTAX"));
test("double space 1", parseFailure("ERR AUTH"));
test("double space 2", parseFailure("ERR CMD SYNTAX"));
test("double space 3", parseFailure("ERR CMD SYNTAX"));
});
});
});
}

View File

@ -1,6 +1,27 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "27.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.0"
async:
dependency: transitive
description:
@ -29,6 +50,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.4"
clock:
dependency: transitive
description:
@ -43,6 +71,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
coverage:
dependency: transitive
description:
name: coverage
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
cupertino_icons:
dependency: "direct main"
description:
@ -57,6 +106,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.2"
flutter:
dependency: "direct main"
description: flutter
@ -74,6 +130,48 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
lints:
dependency: transitive
description:
@ -81,6 +179,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
matcher:
dependency: transitive
description:
@ -95,6 +200,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
path:
dependency: transitive
description:
@ -102,11 +228,74 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.1"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.0"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
shelf_static:
dependency: transitive
description:
name: shelf_static
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
source_maps:
dependency: transitive
description:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.10"
source_span:
dependency: transitive
description:
@ -142,6 +331,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
test:
dependency: "direct dev"
description:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.17.10"
test_api:
dependency: transitive
description:
@ -149,6 +345,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
typed_data:
dependency: transitive
description:
@ -163,5 +366,40 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
vm_service:
dependency: transitive
description:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "7.3.0"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
sdks:
dart: ">=2.12.0 <3.0.0"
dart: ">=2.14.0 <3.0.0"

View File

@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.12.0 <3.0.0"
sdk: ">=2.14.0 <3.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@ -45,6 +45,7 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
test: ^1.17.10
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View File

@ -5,26 +5,26 @@
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import 'package:simplex_chat/main.dart';
import "package:simplex_chat/main.dart";
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
testWidgets("Counter increments smoke test", (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
expect(find.text("0"), findsOneWidget);
expect(find.text("1"), findsNothing);
// Tap the '+' icon and trigger a frame.
// Tap the "+" icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
expect(find.text("0"), findsNothing);
expect(find.text("1"), findsOneWidget);
});
}