diff --git a/.github/workflows/build.yml b/.github/workflows/build_haskell.yml similarity index 97% rename from .github/workflows/build.yml rename to .github/workflows/build_haskell.yml index 723b1d4a3..6abb59342 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build_haskell.yml @@ -7,6 +7,9 @@ on: - v5 tags: - "v*" + paths: + - haskell/** + - .github/workflows/build_haskell.yml pull_request: jobs: diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4de1..36ab7d1fe 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -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 diff --git a/packages/analysis_options.yaml b/packages/analysis_options.yaml new file mode 100644 index 000000000..69bffee5b --- /dev/null +++ b/packages/analysis_options.yaml @@ -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 diff --git a/packages/simplexmq/.gitignore b/packages/simplexmq/.gitignore new file mode 100644 index 000000000..65c34dc86 --- /dev/null +++ b/packages/simplexmq/.gitignore @@ -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 diff --git a/packages/simplexmq/CHANGELOG.md b/packages/simplexmq/CHANGELOG.md new file mode 100644 index 000000000..effe43c82 --- /dev/null +++ b/packages/simplexmq/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/simplexmq/README.md b/packages/simplexmq/README.md new file mode 100644 index 000000000..8b55e735b --- /dev/null +++ b/packages/simplexmq/README.md @@ -0,0 +1,39 @@ + + +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. diff --git a/packages/simplexmq/example/simplexmq_example.dart b/packages/simplexmq/example/simplexmq_example.dart new file mode 100644 index 000000000..45adcec72 --- /dev/null +++ b/packages/simplexmq/example/simplexmq_example.dart @@ -0,0 +1,6 @@ +// import 'package:simplexmq/simplexmq.dart'; + +// void main() { +// var awesome = Awesome(); +// print('awesome: ${awesome.isAwesome}'); +// } diff --git a/packages/simplexmq/lib/simplexmq.dart b/packages/simplexmq/lib/simplexmq.dart new file mode 100644 index 000000000..5d1551352 --- /dev/null +++ b/packages/simplexmq/lib/simplexmq.dart @@ -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. diff --git a/packages/simplexmq/lib/src/buffer.dart b/packages/simplexmq/lib/src/buffer.dart new file mode 100644 index 000000000..c8865af8c --- /dev/null +++ b/packages/simplexmq/lib/src/buffer.dart @@ -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(List 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 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 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 __base64lookup() { + final a = List.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; +} diff --git a/packages/simplexmq/lib/src/parser.dart b/packages/simplexmq/lib/src/parser.dart new file mode 100644 index 000000000..a3538f663 --- /dev/null +++ b/packages/simplexmq/lib/src/parser.dart @@ -0,0 +1,136 @@ +import "dart:typed_data"; +import "buffer.dart"; + +typedef BinaryTags = Map; + +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? 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(BinaryTags 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); diff --git a/packages/simplexmq/lib/src/protocol.dart b/packages/simplexmq/lib/src/protocol.dart new file mode 100644 index 000000000..7b44b1cf1 --- /dev/null +++ b/packages/simplexmq/lib/src/protocol.dart @@ -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 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 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 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 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 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; +} diff --git a/packages/simplexmq/pubspec.yaml b/packages/simplexmq/pubspec.yaml new file mode 100644 index 000000000..b82fce64c --- /dev/null +++ b/packages/simplexmq/pubspec.yaml @@ -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 diff --git a/packages/simplexmq/test/buffer_test.dart b/packages/simplexmq/test/buffer_test.dart new file mode 100644 index 000000000..c4e425d0d --- /dev/null +++ b/packages/simplexmq/test/buffer_test.dart @@ -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); + }); + }); +} diff --git a/packages/simplexmq/test/protocol_test.dart b/packages/simplexmq/test/protocol_test.dart new file mode 100644 index 000000000..3a3f36f0d --- /dev/null +++ b/packages/simplexmq/test/protocol_test.dart @@ -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")); + }); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 750761f76..500dc6342 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index ffed08453..a1eae2cc8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/test/widget_test.dart b/test/widget_test.dart index f06b78b7f..053b6c3b9 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -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); }); }