Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
484 lines
13 KiB
Dart
484 lines
13 KiB
Dart
import 'dart:convert';
|
||
import 'dart:typed_data';
|
||
|
||
class MaxWebFrame {
|
||
final int cmd;
|
||
final int seq;
|
||
final int opcode;
|
||
final Object? payload;
|
||
|
||
const MaxWebFrame({
|
||
required this.cmd,
|
||
required this.seq,
|
||
required this.opcode,
|
||
this.payload,
|
||
});
|
||
|
||
bool get isOk => cmd == MaxWebCmd.ok;
|
||
bool get isError => cmd != MaxWebCmd.ok && cmd != MaxWebCmd.request;
|
||
}
|
||
|
||
abstract class MaxWebCmd {
|
||
static const int request = 0;
|
||
static const int ok = 1;
|
||
static const int notFound = 2;
|
||
static const int error = 3;
|
||
}
|
||
|
||
abstract class MaxWebFraming {
|
||
static const int protocolVersion = 10;
|
||
static const int headerSize = 10;
|
||
static const int maxPayloadSize = 0x00ffffff;
|
||
static const int maxDecompressedPayloadSize = 64 * 1024 * 1024;
|
||
|
||
static Uint8List encode({
|
||
required int cmd,
|
||
required int seq,
|
||
required int opcode,
|
||
Object? payload,
|
||
}) {
|
||
final body = payload == null ? Uint8List(0) : MaxMsgpack.encode(payload);
|
||
if (body.length > maxPayloadSize) {
|
||
throw ArgumentError.value(
|
||
body.length,
|
||
'payload',
|
||
'превышает допустимый размер кадра',
|
||
);
|
||
}
|
||
final frame = Uint8List(headerSize + body.length);
|
||
final view = ByteData.view(frame.buffer);
|
||
|
||
view.setUint8(0, protocolVersion);
|
||
view.setUint8(1, cmd);
|
||
view.setInt16(2, seq);
|
||
view.setInt16(4, opcode);
|
||
view.setUint8(6, 0);
|
||
view.setUint8(7, (body.length >> 16) & 0xFF);
|
||
view.setUint8(8, (body.length >> 8) & 0xFF);
|
||
view.setUint8(9, body.length & 0xFF);
|
||
|
||
frame.setRange(headerSize, frame.length, body);
|
||
return frame;
|
||
}
|
||
|
||
static MaxWebFrame decode(Uint8List frame) {
|
||
if (frame.length < headerSize) {
|
||
throw const FormatException('MaxWebFraming: кадр короче заголовка');
|
||
}
|
||
final view = ByteData.view(frame.buffer, frame.offsetInBytes, frame.length);
|
||
|
||
final cmd = view.getUint8(1);
|
||
final seq = view.getInt16(2);
|
||
final opcode = view.getInt16(4);
|
||
final compressionRatio = view.getUint8(6);
|
||
final length =
|
||
(view.getUint8(7) << 16) | (view.getUint8(8) << 8) | view.getUint8(9);
|
||
final expectedLength = headerSize + length;
|
||
if (frame.length != expectedLength) {
|
||
throw FormatException(
|
||
'MaxWebFraming: длина кадра ${frame.length} не совпадает с $expectedLength',
|
||
);
|
||
}
|
||
|
||
if (length <= 0) {
|
||
return MaxWebFrame(cmd: cmd, seq: seq, opcode: opcode);
|
||
}
|
||
|
||
var body = Uint8List.sublistView(frame, headerSize, headerSize + length);
|
||
if (compressionRatio > 0) {
|
||
body = Lz4Block.decompress(body, maxDecompressedPayloadSize);
|
||
}
|
||
|
||
return MaxWebFrame(
|
||
cmd: cmd,
|
||
seq: seq,
|
||
opcode: opcode,
|
||
payload: MaxMsgpack.decode(body),
|
||
);
|
||
}
|
||
}
|
||
|
||
abstract class Lz4Block {
|
||
static Uint8List decompress(Uint8List source, int maxOutputSize) {
|
||
if (maxOutputSize < 0) {
|
||
throw ArgumentError.value(maxOutputSize, 'maxOutputSize');
|
||
}
|
||
final output = <int>[];
|
||
var input = 0;
|
||
|
||
while (input < source.length) {
|
||
final token = source[input++];
|
||
|
||
var literalLength = token >> 4;
|
||
if (literalLength == 15) {
|
||
literalLength += _readLengthExtension(
|
||
source,
|
||
() => input,
|
||
(v) => input = v,
|
||
);
|
||
}
|
||
|
||
if (literalLength > source.length - input) {
|
||
throw const FormatException('Lz4Block: обрыв литералов');
|
||
}
|
||
if (literalLength > maxOutputSize - output.length) {
|
||
throw const FormatException('Lz4Block: литералы не помещаются');
|
||
}
|
||
output.addAll(
|
||
Uint8List.sublistView(source, input, input + literalLength),
|
||
);
|
||
input += literalLength;
|
||
|
||
if (input >= source.length) break;
|
||
|
||
if (source.length - input < 2) {
|
||
throw const FormatException('Lz4Block: обрыв смещения совпадения');
|
||
}
|
||
|
||
final offset = source[input] | (source[input + 1] << 8);
|
||
input += 2;
|
||
if (offset == 0 || offset > output.length) {
|
||
throw const FormatException('Lz4Block: неверное смещение совпадения');
|
||
}
|
||
|
||
var matchLength = token & 0x0F;
|
||
if (matchLength == 15) {
|
||
matchLength += _readLengthExtension(
|
||
source,
|
||
() => input,
|
||
(v) => input = v,
|
||
);
|
||
}
|
||
matchLength += 4;
|
||
|
||
if (matchLength > maxOutputSize - output.length) {
|
||
throw const FormatException('Lz4Block: совпадение не помещается');
|
||
}
|
||
|
||
for (var i = 0; i < matchLength; i++) {
|
||
output.add(output[output.length - offset]);
|
||
}
|
||
}
|
||
|
||
return Uint8List.fromList(output);
|
||
}
|
||
|
||
static int _readLengthExtension(
|
||
Uint8List source,
|
||
int Function() get,
|
||
void Function(int) set,
|
||
) {
|
||
var cursor = get();
|
||
var extra = 0;
|
||
while (true) {
|
||
if (cursor >= source.length) {
|
||
throw const FormatException('Lz4Block: обрыв в расширении длины');
|
||
}
|
||
final byte = source[cursor++];
|
||
extra += byte;
|
||
if (byte != 255) break;
|
||
}
|
||
set(cursor);
|
||
return extra;
|
||
}
|
||
}
|
||
|
||
abstract class MaxMsgpack {
|
||
static Uint8List encode(Object? value) {
|
||
final sink = BytesBuilder(copy: false);
|
||
_write(sink, value);
|
||
return sink.takeBytes();
|
||
}
|
||
|
||
static Object? decode(Uint8List bytes) => _Reader(bytes).read();
|
||
|
||
static void _write(BytesBuilder sink, Object? value) {
|
||
if (value == null) {
|
||
sink.addByte(0xC0);
|
||
} else if (value is bool) {
|
||
sink.addByte(value ? 0xC3 : 0xC2);
|
||
} else if (value is int) {
|
||
_writeInt(sink, value);
|
||
} else if (value is double) {
|
||
final buffer = ByteData(9)
|
||
..setUint8(0, 0xCB)
|
||
..setFloat64(1, value);
|
||
sink.add(buffer.buffer.asUint8List());
|
||
} else if (value is String) {
|
||
_writeString(sink, value);
|
||
} else if (value is Uint8List) {
|
||
_writeBinary(sink, value);
|
||
} else if (value is List) {
|
||
_writePrefix(sink, value.length, 0x90, 0xDC, 0xDD);
|
||
for (final item in value) {
|
||
_write(sink, item);
|
||
}
|
||
} else if (value is Map) {
|
||
_writePrefix(sink, value.length, 0x80, 0xDE, 0xDF);
|
||
value.forEach((key, item) {
|
||
_write(sink, key);
|
||
_write(sink, item);
|
||
});
|
||
} else {
|
||
throw ArgumentError(
|
||
'MaxMsgpack: неподдерживаемый тип ${value.runtimeType}',
|
||
);
|
||
}
|
||
}
|
||
|
||
static void _writePrefix(
|
||
BytesBuilder sink,
|
||
int length,
|
||
int fixBase,
|
||
int wide16,
|
||
int wide32,
|
||
) {
|
||
if (length < 16) {
|
||
sink.addByte(fixBase | length);
|
||
} else if (length < 0x10000) {
|
||
sink.addByte(wide16);
|
||
sink.add([(length >> 8) & 0xFF, length & 0xFF]);
|
||
} else {
|
||
sink.addByte(wide32);
|
||
sink.add([
|
||
(length >> 24) & 0xFF,
|
||
(length >> 16) & 0xFF,
|
||
(length >> 8) & 0xFF,
|
||
length & 0xFF,
|
||
]);
|
||
}
|
||
}
|
||
|
||
static void _writeString(BytesBuilder sink, String value) {
|
||
final utf8Bytes = utf8.encode(value);
|
||
final length = utf8Bytes.length;
|
||
if (length < 32) {
|
||
sink.addByte(0xA0 | length);
|
||
} else if (length < 0x100) {
|
||
sink.add([0xD9, length]);
|
||
} else if (length < 0x10000) {
|
||
sink.add([0xDA, (length >> 8) & 0xFF, length & 0xFF]);
|
||
} else {
|
||
sink.add([
|
||
0xDB,
|
||
(length >> 24) & 0xFF,
|
||
(length >> 16) & 0xFF,
|
||
(length >> 8) & 0xFF,
|
||
length & 0xFF,
|
||
]);
|
||
}
|
||
sink.add(utf8Bytes);
|
||
}
|
||
|
||
static void _writeBinary(BytesBuilder sink, Uint8List value) {
|
||
final length = value.length;
|
||
if (length < 0x100) {
|
||
sink.add([0xC4, length]);
|
||
} else if (length < 0x10000) {
|
||
sink.add([0xC5, (length >> 8) & 0xFF, length & 0xFF]);
|
||
} else {
|
||
sink.add([
|
||
0xC6,
|
||
(length >> 24) & 0xFF,
|
||
(length >> 16) & 0xFF,
|
||
(length >> 8) & 0xFF,
|
||
length & 0xFF,
|
||
]);
|
||
}
|
||
sink.add(value);
|
||
}
|
||
|
||
static void _writeInt(BytesBuilder sink, int value) {
|
||
if (value >= 0) {
|
||
if (value < 0x80) {
|
||
sink.addByte(value);
|
||
} else if (value < 0x100) {
|
||
sink.add([0xCC, value]);
|
||
} else if (value < 0x10000) {
|
||
sink.add([0xCD, (value >> 8) & 0xFF, value & 0xFF]);
|
||
} else if (value < 0x100000000) {
|
||
sink.add([
|
||
0xCE,
|
||
(value >> 24) & 0xFF,
|
||
(value >> 16) & 0xFF,
|
||
(value >> 8) & 0xFF,
|
||
value & 0xFF,
|
||
]);
|
||
} else {
|
||
final buffer = ByteData(9)
|
||
..setUint8(0, 0xCF)
|
||
..setUint64(1, value);
|
||
sink.add(buffer.buffer.asUint8List());
|
||
}
|
||
} else if (value >= -32) {
|
||
sink.addByte(0xE0 | (value + 32));
|
||
} else if (value >= -128) {
|
||
final buffer = ByteData(2)
|
||
..setUint8(0, 0xD0)
|
||
..setInt8(1, value);
|
||
sink.add(buffer.buffer.asUint8List());
|
||
} else if (value >= -32768) {
|
||
final buffer = ByteData(3)
|
||
..setUint8(0, 0xD1)
|
||
..setInt16(1, value);
|
||
sink.add(buffer.buffer.asUint8List());
|
||
} else if (value >= -2147483648) {
|
||
final buffer = ByteData(5)
|
||
..setUint8(0, 0xD2)
|
||
..setInt32(1, value);
|
||
sink.add(buffer.buffer.asUint8List());
|
||
} else {
|
||
final buffer = ByteData(9)
|
||
..setUint8(0, 0xD3)
|
||
..setInt64(1, value);
|
||
sink.add(buffer.buffer.asUint8List());
|
||
}
|
||
}
|
||
}
|
||
|
||
class _Reader {
|
||
_Reader(this._bytes)
|
||
: _view = ByteData.view(_bytes.buffer, _bytes.offsetInBytes, _bytes.length);
|
||
|
||
final Uint8List _bytes;
|
||
final ByteData _view;
|
||
int _cursor = 0;
|
||
|
||
Object? read() {
|
||
final byte = _u8();
|
||
|
||
if (byte <= 0x7F) return byte;
|
||
if (byte >= 0xE0) return byte - 256;
|
||
if (byte >= 0x80 && byte <= 0x8F) return _map(byte & 0x0F);
|
||
if (byte >= 0x90 && byte <= 0x9F) return _list(byte & 0x0F);
|
||
if (byte >= 0xA0 && byte <= 0xBF) return _string(byte & 0x1F);
|
||
|
||
switch (byte) {
|
||
case 0xC0:
|
||
return null;
|
||
case 0xC2:
|
||
return false;
|
||
case 0xC3:
|
||
return true;
|
||
case 0xC4:
|
||
return _binary(_u8());
|
||
case 0xC5:
|
||
return _binary(_u16());
|
||
case 0xC6:
|
||
return _binary(_u32());
|
||
case 0xCA:
|
||
final value = _view.getFloat32(_cursor);
|
||
_cursor += 4;
|
||
return value;
|
||
case 0xCB:
|
||
final value = _view.getFloat64(_cursor);
|
||
_cursor += 8;
|
||
return value;
|
||
case 0xCC:
|
||
return _u8();
|
||
case 0xCD:
|
||
return _u16();
|
||
case 0xCE:
|
||
return _u32();
|
||
case 0xCF:
|
||
final value = _view.getUint64(_cursor);
|
||
_cursor += 8;
|
||
return value;
|
||
case 0xD0:
|
||
final value = _view.getInt8(_cursor);
|
||
_cursor += 1;
|
||
return value;
|
||
case 0xD1:
|
||
final value = _view.getInt16(_cursor);
|
||
_cursor += 2;
|
||
return value;
|
||
case 0xD2:
|
||
final value = _view.getInt32(_cursor);
|
||
_cursor += 4;
|
||
return value;
|
||
case 0xD3:
|
||
final value = _view.getInt64(_cursor);
|
||
_cursor += 8;
|
||
return value;
|
||
case 0xD9:
|
||
return _string(_u8());
|
||
case 0xDA:
|
||
return _string(_u16());
|
||
case 0xDB:
|
||
return _string(_u32());
|
||
case 0xDC:
|
||
return _list(_u16());
|
||
case 0xDD:
|
||
return _list(_u32());
|
||
case 0xDE:
|
||
return _map(_u16());
|
||
case 0xDF:
|
||
return _map(_u32());
|
||
}
|
||
|
||
if (byte >= 0xD4 && byte <= 0xD8) return _ext(1 << (byte - 0xD4));
|
||
if (byte == 0xC7) return _ext(_u8());
|
||
if (byte == 0xC8) return _ext(_u16());
|
||
if (byte == 0xC9) return _ext(_u32());
|
||
|
||
throw FormatException(
|
||
'MaxMsgpack: неизвестный маркер 0x${byte.toRadixString(16)}',
|
||
);
|
||
}
|
||
|
||
static const int _numberExtType = 1;
|
||
|
||
Object? _ext(int length) {
|
||
final type = _u8();
|
||
final data = Uint8List.fromList(
|
||
Uint8List.sublistView(_bytes, _cursor, _cursor + length),
|
||
);
|
||
_cursor += length;
|
||
if (type != _numberExtType) return null;
|
||
return _Reader(data).read();
|
||
}
|
||
|
||
int _u8() => _bytes[_cursor++];
|
||
|
||
int _u16() {
|
||
final value = _view.getUint16(_cursor);
|
||
_cursor += 2;
|
||
return value;
|
||
}
|
||
|
||
int _u32() {
|
||
final value = _view.getUint32(_cursor);
|
||
_cursor += 4;
|
||
return value;
|
||
}
|
||
|
||
String _string(int length) {
|
||
final value = utf8.decode(
|
||
Uint8List.sublistView(_bytes, _cursor, _cursor + length),
|
||
allowMalformed: true,
|
||
);
|
||
_cursor += length;
|
||
return value;
|
||
}
|
||
|
||
Uint8List _binary(int length) {
|
||
final value = Uint8List.fromList(
|
||
Uint8List.sublistView(_bytes, _cursor, _cursor + length),
|
||
);
|
||
_cursor += length;
|
||
return value;
|
||
}
|
||
|
||
List<Object?> _list(int length) =>
|
||
List<Object?>.generate(length, (_) => read(), growable: false);
|
||
|
||
Map<Object?, Object?> _map(int length) {
|
||
final map = <Object?, Object?>{};
|
||
for (var i = 0; i < length; i++) {
|
||
final key = read();
|
||
map[key] = read();
|
||
}
|
||
return map;
|
||
}
|
||
}
|