From d541e9df9dcf52ceed7563a59ffd40102285aade Mon Sep 17 00:00:00 2001 From: klockky Date: Thu, 14 May 2026 13:12:41 +0300 Subject: [PATCH] =?UTF-8?q?fix(transport):=20Zstd-=D1=80=D0=B0=D1=81=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D0=BE=D0=B2=D0=BA=D0=B0,=20=D0=B3=D0=BE=D0=BD?= =?UTF-8?q?=D0=BA=D0=B0=20PacketReceiver,=20fix=20=D0=B8=D0=BC=D1=91=D0=BD?= =?UTF-8?q?=20=D0=B8=20=D1=81=D1=82=D0=B8=D0=BA=D0=B5=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/api.dart | 10 ++++- lib/backend/modules/chats.dart | 1 - lib/backend/modules/contacts.dart | 54 ++++++++++++++++++++--- lib/core/protocol/packet.dart | 55 +++++++++++++++++------- lib/core/transport/receiver.dart | 21 ++++----- lib/frontend/widgets/message_bubble.dart | 8 ++-- lib/main.dart | 5 +++ lib/models/attachment.dart | 6 +-- pubspec.lock | 16 +++++++ pubspec.yaml | 1 + 10 files changed, 137 insertions(+), 40 deletions(-) diff --git a/lib/backend/api.dart b/lib/backend/api.dart index 495b56a..1fdd71f 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -266,7 +266,15 @@ class Api { } Future _onDataReceived(Uint8List data) async { - await for (final packet in _receiver.feed(data)) { + final rawPackets = _receiver.feed(data); + for (final raw in rawPackets) { + final Packet packet; + try { + packet = await unpackPacket(raw); + } catch (e) { + logger.e('PacketReceiver: ошибка распаковки: $e'); + continue; + } if (packet.isError && packet.payload is Map && (packet.payload['message'] == 'FAIL_LOGIN_TOKEN' || diff --git a/lib/backend/modules/chats.dart b/lib/backend/modules/chats.dart index f47aa70..2308163 100644 --- a/lib/backend/modules/chats.dart +++ b/lib/backend/modules/chats.dart @@ -160,7 +160,6 @@ class ChatsModule { static Future> getChats(int accountId) async { try { final rows = await AppDatabase.loadChats(accountId); - return rows.map(CachedChat.fromDbRow).toList(); } catch (e) { logger.e("Ошибка при получении чатов: $e"); diff --git a/lib/backend/modules/contacts.dart b/lib/backend/modules/contacts.dart index 71e4926..b4f103b 100644 --- a/lib/backend/modules/contacts.dart +++ b/lib/backend/modules/contacts.dart @@ -1,4 +1,5 @@ import '../../core/storage/app_database.dart'; +import 'messages.dart'; class CachedContact { final int id; @@ -44,22 +45,65 @@ class ContactsModule { final contacts = data['contacts']; if (contacts is! List || contacts.isEmpty) return; - final rows = contacts - .whereType() - .map((c) => _parseContact(c.cast(), accountId)) - .whereType>() - .toList(); + final rows = >[]; + for (final raw in contacts.whereType()) { + final contact = raw.cast(); + final row = _parseContact(contact, accountId); + if (row != null) rows.add(row); + _primeContactCache(contact); + } if (rows.isNotEmpty) { await AppDatabase.saveContacts(rows); } } + static void _primeContactCache(Map contact) { + final id = contact['id']; + if (id is! int) return; + + final names = contact['names']; + if (names is List && names.isNotEmpty) { + final nameRaw = names.firstWhere( + (n) => n is Map && n['type'] == 'ONEME', + orElse: () => names.firstWhere((n) => n is Map, orElse: () => null), + ); + if (nameRaw is Map) { + final firstName = (nameRaw['firstName'] as String?) ?? ''; + final lastName = nameRaw['lastName'] as String?; + final fullName = (lastName != null && lastName.isNotEmpty) + ? '$firstName $lastName' + : firstName; + if (fullName.isNotEmpty) ContactCache.put(id, fullName); + } + } + + final baseUrl = contact['baseUrl'] as String?; + if (baseUrl != null && baseUrl.isNotEmpty) { + ContactCache.putAvatar(id, baseUrl); + } + } + static Future> getContacts(int accountId) async { final rows = await AppDatabase.loadContacts(accountId); return rows.map(CachedContact.fromDbRow).toList(); } + /// Прогревает in-memory ContactCache из локальных контактов. + /// Нужно вызывать на cold start: иначе кэш пуст до следующего логина. + static Future primeCacheFromDb(int accountId) async { + final contacts = await getContacts(accountId); + for (final c in contacts) { + final fullName = (c.lastName != null && c.lastName!.isNotEmpty) + ? '${c.firstName} ${c.lastName}' + : c.firstName; + if (fullName.isNotEmpty) ContactCache.put(c.id, fullName); + if (c.baseUrl != null && c.baseUrl!.isNotEmpty) { + ContactCache.putAvatar(c.id, c.baseUrl); + } + } + } + static Map? _parseContact( Map contact, int accountId, diff --git a/lib/core/protocol/packet.dart b/lib/core/protocol/packet.dart index 5637328..e8b3e27 100644 --- a/lib/core/protocol/packet.dart +++ b/lib/core/protocol/packet.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'dart:isolate'; import 'package:dart_lz4/dart_lz4.dart'; +import 'package:es_compression/zstd.dart'; import 'package:msgpack_dart/msgpack_dart.dart' as msgpack; /// ver(1) + cmd(1) + seq(2) + opcode(2) + packedLen(4) = 10 @@ -129,21 +130,7 @@ Future unpackPacket(Uint8List packet) async { if (payloadBytes.isNotEmpty) { if (compFlag != 0) { - try { - payloadBytes = lz4Decompress( - payloadBytes, - decompressedSize: _maxDecompressedSize, - ); - } catch (_) { - try { - payloadBytes = _lz4BlockDecompress( - payloadBytes, - _maxDecompressedSize, - ); - } catch (e) { - throw Exception("LZ4 decompression error: $e"); - } - } + payloadBytes = _decompressPayload(payloadBytes); } try { @@ -165,6 +152,44 @@ Future unpackPacket(Uint8List packet) async { }); } +/// Определяет формат сжатия по 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 { + final out = zstd.decode(src); + return out is Uint8List ? out : Uint8List.fromList(out); + } 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) { diff --git a/lib/core/transport/receiver.dart b/lib/core/transport/receiver.dart index a14476e..e0a45ee 100644 --- a/lib/core/transport/receiver.dart +++ b/lib/core/transport/receiver.dart @@ -4,15 +4,16 @@ import '../protocol/packet.dart'; import '../utils/logger.dart'; /// Буфер входящих данных. -/// Копит сырые байты из сокета, собирает из них целые пакеты. +/// Копит сырые байты из сокета, нарезает их на байтовые срезы целых пакетов. class PacketReceiver { Uint8List _buffer = Uint8List(0); static const int _maxBufferSize = 2 * 1024 * 1024; // 2 мегабуйта - /// Добавляет байты в буфер, возвращает поток собранных пакетов. - /// Неполные данные остаются в буфере до следующего вызова. - Stream feed(Uint8List data) async* { + /// Добавляет байты в буфер и возвращает все собранные пакеты как сырые срезы. + /// Полностью синхронный — нарезка не блокируется на распаковке, поэтому + /// конкурентные вызовы из stream-листенера не могут пересечься на `_buffer`. + List feed(Uint8List data) { final newBuffer = Uint8List(_buffer.length + data.length); newBuffer.setAll(0, _buffer); newBuffer.setAll(_buffer.length, data); @@ -23,9 +24,10 @@ class PacketReceiver { 'PacketReceiver: переполнение буфера (${_buffer.length} B), сброс', ); reset(); - return; + return const []; } + final packets = []; while (_buffer.length >= headerSize) { final bd = ByteData.view( _buffer.buffer, @@ -38,15 +40,10 @@ class PacketReceiver { if (_buffer.length < totalLength) break; - final packetBytes = Uint8List.sublistView(_buffer, 0, totalLength); + packets.add(Uint8List.sublistView(_buffer, 0, totalLength)); _buffer = _buffer.sublist(totalLength); - - try { - yield await unpackPacket(packetBytes); - } catch (e) { - logger.e('PacketReceiver: ошибка распаковки: $e'); - } } + return packets; } void reset() { diff --git a/lib/frontend/widgets/message_bubble.dart b/lib/frontend/widgets/message_bubble.dart index 1217554..db9b7cf 100644 --- a/lib/frontend/widgets/message_bubble.dart +++ b/lib/frontend/widgets/message_bubble.dart @@ -1270,15 +1270,17 @@ class MessageBubble extends StatelessWidget { } Widget _buildStickerAttachment(BuildContext ctx, MessageAttachment sticker) { - final preview = (sticker as dynamic).previewData as String? ?? ''; + final url = sticker.baseUrl ?? ''; + final preview = sticker.previewData ?? ''; + final imageUrl = url.isNotEmpty ? url : preview; return ClipRRect( borderRadius: BorderRadius.circular(photoBorderRadius), child: Stack( children: [ - if (preview.isNotEmpty) + if (imageUrl.isNotEmpty) Image.network( - preview, + imageUrl, width: 150, height: 150, fit: BoxFit.contain, diff --git a/lib/main.dart b/lib/main.dart index 5c62627..1654efd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:komet/l10n/app_localizations.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'backend/api.dart'; import 'backend/modules/account.dart'; +import 'backend/modules/contacts.dart'; import 'backend/modules/messages.dart'; import 'core/storage/app_database.dart'; import 'core/storage/token_storage.dart'; @@ -36,6 +37,10 @@ Future _loadInitialLocale() async { void main() async { WidgetsFlutterBinding.ensureInitialized(); await AppDatabase.init(); + final activeAccountId = await TokenStorage.getActiveAccountId(); + if (activeAccountId != null) { + await ContactsModule.primeCacheFromDb(activeAccountId); + } await api.connect(); final initialLocale = await _loadInitialLocale(); diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index 825e348..4d72a09 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -304,9 +304,9 @@ class StickerAttachment extends MessageAttachment { return StickerAttachment( previewData: previewStr, - baseUrl: map['baseUrl'] as String?, - stickerId: map['stickerId'] as String?, - stickerPackId: map['stickerPackId'] as String?, + baseUrl: (map['url'] ?? map['baseUrl'])?.toString(), + stickerId: map['stickerId']?.toString(), + stickerPackId: (map['stickerPackId'] ?? map['setId'])?.toString(), width: map['width'] as int?, height: map['height'] as int?, ); diff --git a/pubspec.lock b/pubspec.lock index 1f7d572..e834ec0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -105,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.8" + es_compression: + dependency: "direct main" + description: + name: es_compression + sha256: c1ff7af54802631cf5c3942cb67bb99daadcc087f573ca99a9de91002d1a7ece + url: "https://pub.dev" + source: hosted + version: "2.0.15" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f681c96..017572f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 dart_lz4: ^1.0.0 + es_compression: ^2.0.15 msgpack_dart: ^1.0.1 logger: ^2.6.2 device_info_plus: 12.3.0