Files
Qlyra/lib/core/protocol/packet.dart
T

263 lines
8.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:typed_data';
import 'dart:isolate';
import 'package:dart_lz4/dart_lz4.dart';
import 'package:libcompress/libcompress.dart';
import 'package:msgpack_dart/msgpack_dart.dart' as msgpack;
/// ver(1) + cmd(1) + seq(2) + opcode(2) + packedLen(4) = 10
const int headerSize = 10;
const int _maxDecompressedSize = 1048576; // 1 MB
/// Типы команд в протоколе
abstract class CmdType {
static const int request = 0; // запрос клиента / пуш от сервера (направление определяет смысл)
static const int push = 0; // пуш от сервера (имеет смысл только для incoming)
static const int ok = 1; // ответ: ок
static const int notFound = 2; // ответ: не найдено
static const int error = 3; // ответ: ошибка
}
/// Распакованный бинарный пакет
///
/// Формат заголовка (10 байт):
/// ```
/// [0] ver — версия протокола (uint8) (по умолчанию 10)
/// [1] cmd — тип команды (uint8) (при отправке от клиента равно 0)
/// [2..3] seq — порядковый номер (uint16 BE)
/// [4..5] opcode — код операции (uint16 BE)
/// [6..9] packedLen — флаг сжатия [6] + длина payload [7..9] (uint32 BE)
/// [10..] payload — данные в MsgPack, опционально сжатые LZ4
/// ```
class Packet {
int api;
int cmd;
int seq;
int opcode;
dynamic payload;
Packet({
this.api = 10,
this.cmd = 0,
this.seq = 0,
this.opcode = 0,
this.payload,
});
bool get isOk => cmd == CmdType.ok;
bool get isError => cmd == CmdType.error;
bool get isPush => cmd == CmdType.push;
@override
String toString() =>
'Packet(ver=$api cmd=$cmd seq=$seq opcode=$opcode payload=$payload)';
}
class PacketError implements Exception {
final String message;
final String? errorKey;
const PacketError(this.message, {this.errorKey});
@override
String toString() => message;
}
class SessionExpiredException extends PacketError {
const SessionExpiredException(super.message);
}
String messageFromErrorPayload(dynamic payload) {
if (payload is Map) {
final msg = payload['message'];
if (msg == 'FAIL_WRONG_PASSWORD' || msg == 'FAIL_LOGIN_TOKEN') {
return 'Ваш токен был отклонён сервером, хм... Попробуйте войти ещё раз.';
}
for (final key in ['localizedMessage', 'message', 'title']) {
final v = payload[key];
if (v is String && v.trim().isNotEmpty) return v.trim();
}
return 'Неизвестная ошибка';
}
if (payload == null) return 'Неизвестная ошибка';
final s = payload.toString();
return s.isNotEmpty ? s : 'Неизвестная ошибка';
}
/// Payload меньше этого размера отправляется без сжатия (как в оригинале).
const int _compressionThreshold = 32;
/// Упаковка пакета для отправки на сервер.
///
/// Payload сериализуется в MsgPack и при размере >= [_compressionThreshold]
/// сжимается LZ4-block. Старший байт поля packedLen — флаг сжатия:
/// `0` — без сжатия, иначе `(rawLen ~/ compLen) + 1` (множитель размера, по
/// которому получатель выделяет буфер под распаковку).
Uint8List packPacket(int opcode, Map<dynamic, dynamic> payload, {int seq = 0}) {
final Uint8List raw = msgpack.serialize(payload);
final List<int> body;
final int flag;
if (raw.length < _compressionThreshold) {
body = raw;
flag = 0;
} else {
body = lz4Compress(raw);
flag = (raw.length ~/ body.length) + 1;
}
final out = Uint8List(headerSize + body.length);
final header = ByteData.view(out.buffer, out.offsetInBytes, headerSize);
header.setUint8(0, 10);
header.setUint8(1, CmdType.request);
header.setUint16(2, seq, Endian.big);
header.setUint16(4, opcode, Endian.big);
header.setUint32(
6,
((flag & 0xFF) << 24) | (body.length & 0xFFFFFF),
Endian.big,
);
out.setRange(headerSize, out.length, body);
return out;
}
/// Распаковка пакета от сервера
Future<Packet> unpackPacket(Uint8List packet) async {
return Isolate.run(() {
// Для удобства расшифровки пакета переводим в ByteData
ByteData packetData = ByteData.view(
packet.buffer,
packet.offsetInBytes,
packet.lengthInBytes,
);
// API версия и cmd представляют из себя 8 битные числа
final apiVer = packetData.getUint8(0) & 0xFF;
final cmd = packetData.getUint8(1) & 0xFF;
// Sequence и OPCode представляют из себя 16 битные числа
final seq = packetData.getUint16(2) & 0xFFFF;
final opcode = packetData.getUint16(4) & 0xFFFF;
// После базовых переменных идет длина пакета, является 32 битным числом
final packedLen = packetData.getUint32(6);
// Compression flag показывает, сжат ли payload
final compFlag = packedLen >> 24;
// Длина payload'а
final payloadLength = packedLen & 0xFFFFFF;
// Байты payload'а, могут быть сжаты LZ4
var payloadBytes = packet.buffer.asUint8List(10, payloadLength);
dynamic payload;
if (payloadBytes.isNotEmpty) {
if (compFlag != 0) {
payloadBytes = _decompressPayload(payloadBytes);
}
try {
payload = msgpack.deserialize(payloadBytes);
} catch (e) {
if (payloadBytes.isNotEmpty) {
throw Exception("MsgPack deserialization error: $e");
}
}
}
return Packet(
api: apiVer,
cmd: cmd,
seq: seq,
opcode: opcode,
payload: payload,
);
});
}
/// Определяет формат сжатия по magic-number и распаковывает payload.
/// Сервер может присылать LZ4 block ИЛИ Zstandard в зависимости от ответа.
Uint8List _decompressPayload(Uint8List src) {
// Zstandard: magic 28 B5 2F FD (little-endian)
if (src.length >= 4 &&
src[0] == 0x28 &&
src[1] == 0xB5 &&
src[2] == 0x2F &&
src[3] == 0xFD) {
try {
return ZstdCodec().decompress(src);
} catch (e) {
throw Exception('Zstd decompression error: $e');
}
}
// LZ4 frame: magic 04 22 4D 18
if (src.length >= 4 &&
src[0] == 0x04 &&
src[1] == 0x22 &&
src[2] == 0x4D &&
src[3] == 0x18) {
try {
return lz4Decompress(src, decompressedSize: _maxDecompressedSize);
} catch (e) {
throw Exception('LZ4 frame decompression error: $e');
}
}
// По умолчанию — LZ4 block (без magic)
try {
return _lz4BlockDecompress(src, _maxDecompressedSize);
} catch (e) {
throw Exception('LZ4 block decompression error: $e');
}
}
/// LZ4 block декомпрессия (без frame-заголовка).
/// Сервер шлёт именно block-формат, dart_lz4 его не поддерживает.
Uint8List _lz4BlockDecompress(Uint8List src, int maxSize) {
final dst = BytesBuilder(copy: false);
int pos = 0;
while (pos < src.length) {
final token = src[pos++];
var litLen = token >> 4;
if (litLen == 15) {
while (pos < src.length) {
final b = src[pos++];
litLen += b;
if (b != 255) break;
}
}
if (litLen > 0) {
dst.add(src.sublist(pos, pos + litLen));
pos += litLen;
}
if (pos >= src.length) break;
if (pos + 1 >= src.length) throw StateError('LZ4: unexpected end of input');
final offset = src[pos] | (src[pos + 1] << 8);
pos += 2;
if (offset == 0) throw StateError('LZ4: offset = 0');
var matchLen = (token & 0x0F) + 4;
if ((token & 0x0F) == 0x0F) {
while (pos < src.length) {
final b = src[pos++];
matchLen += b;
if (b != 255) break;
}
}
final out = dst.toBytes();
final start = out.length - offset;
dst.add(List<int>.generate(matchLen, (i) => out[start + (i % offset)]));
if (dst.length > maxSize) throw StateError('LZ4: превышен лимит');
}
return dst.toBytes();
}