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 = []; 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 _list(int length) => List.generate(length, (_) => read(), growable: false); Map _map(int length) { final map = {}; for (var i = 0; i < length; i++) { final key = read(); map[key] = read(); } return map; } }