fix multibyte path in private message and repeter

This commit is contained in:
PacoX
2026-05-23 13:32:11 +02:00
parent 945a118853
commit 5486ae8ccb
5 changed files with 195 additions and 19 deletions
+15 -3
View File
@@ -2797,11 +2797,15 @@ class MeshCoreConnector extends ChangeNotifier {
try {
if (!isConnected) return;
final mode = _pathHashByteWidth - 1;
final encodedPathLen = (pathLen >= 0 && pathLen != 0xFF)
? (pathLen | (mode << 6))
: pathLen;
await sendFrame(
buildUpdateContactPathFrame(
contact.publicKey,
customPath,
pathLen,
encodedPathLen,
type: contact.type,
flags: contact.flags,
name: contact.name,
@@ -2858,11 +2862,15 @@ class MeshCoreConnector extends ChangeNotifier {
: (updatedFlags & ~contactFlagTeleEnv))
: updatedFlags;
final mode = _pathHashByteWidth - 1;
final encodedPathLen = (latestContact.pathLength >= 0 && latestContact.pathLength != 0xFF)
? (latestContact.pathLength | (mode << 6))
: latestContact.pathLength;
await sendFrame(
buildUpdateContactPathFrame(
latestContact.publicKey,
latestContact.path,
latestContact.pathLength,
encodedPathLen,
type: latestContact.type,
flags: updatedFlags,
name: latestContact.name,
@@ -3198,11 +3206,15 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> importDiscoveredContact(Contact contact) async {
if (!isConnected) return;
final mode = _pathHashByteWidth - 1;
final encodedPathLen = (contact.pathLength >= 0 && contact.pathLength != 0xFF)
? (contact.pathLength | (mode << 6))
: contact.pathLength;
await sendFrame(
buildUpdateContactPathFrame(
contact.publicKey,
contact.path,
contact.pathLength,
encodedPathLen,
type: contact.type,
flags: contact.flags,
name: contact.name,
+13 -3
View File
@@ -166,8 +166,18 @@ class Contact {
final type = reader.readByte();
final flags = reader.readByte();
final pathLen = reader.readByte();
final safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen)
int hopCount = 0;
int byteLen = 0;
if (pathLen == 0xFF) {
hopCount = -1;
} else {
final mode = (pathLen & 0xC0) >> 6;
hopCount = pathLen & 0x3F;
final width = mode + 1;
byteLen = hopCount * width;
}
final safePathLen = byteLen > 0
? (byteLen > maxPathSize ? maxPathSize : byteLen)
: 0;
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
final name = reader.readCStringGreedy(maxNameSize);
@@ -213,7 +223,7 @@ class Contact {
name: name.isEmpty ? 'Unknown' : name,
type: type,
flags: flags,
pathLength: (pathLen == 0xFF || pathLen > maxPathSize) ? -1 : pathLen,
pathLength: hopCount,
path: pathBytes,
latitude: lat,
longitude: lon,
+29 -4
View File
@@ -88,15 +88,40 @@ class ContactStore {
final lastSeenMs = json['lastSeen'] as int? ?? 0;
final lastMessageMs = json['lastMessageAt'] as int?;
final lastModifiedMs = json['lastModified'] as int?;
final rawPathLength = json['pathLength'] as int? ?? -1;
final rawPath = json['path'] != null
? Uint8List.fromList(base64Decode(json['path'] as String))
: Uint8List(0);
int decodedPathLength = rawPathLength;
Uint8List decodedPath = rawPath;
if (rawPathLength == 0xFF || rawPathLength < 0) {
decodedPathLength = -1;
decodedPath = Uint8List(0);
} else if (rawPathLength >= 64) {
final mode = (rawPathLength & 0xC0) >> 6;
final hopCount = rawPathLength & 0x3F;
final width = mode + 1;
final byteLen = hopCount * width;
decodedPathLength = hopCount;
if (byteLen <= rawPath.length) {
decodedPath = rawPath.sublist(0, byteLen);
} else {
decodedPath = Uint8List(0);
}
} else if (rawPathLength == 0) {
decodedPath = Uint8List(0);
}
return Contact(
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
name: json['name'] as String? ?? 'Unknown',
type: json['type'] as int? ?? 0,
flags: json['flags'] as int? ?? 0,
pathLength: json['pathLength'] as int? ?? -1,
path: json['path'] != null
? Uint8List.fromList(base64Decode(json['path'] as String))
: Uint8List(0),
pathLength: decodedPathLength,
path: decodedPath,
pathOverride: json['pathOverride'] as int?,
pathOverrideBytes: json['pathOverrideBytes'] != null
? Uint8List.fromList(
+31 -4
View File
@@ -113,6 +113,35 @@ class MessageStore {
final decodedText = isCli
? rawText
: (Smaz.tryDecodePrefixed(rawText) ?? rawText);
final rawPathLength = json['pathLength'] as int?;
final rawPathBytes = json['pathBytes'] != null
? Uint8List.fromList(base64Decode(json['pathBytes'] as String))
: Uint8List(0);
int? decodedPathLength = rawPathLength;
Uint8List decodedPathBytes = rawPathBytes;
if (rawPathLength != null) {
if (rawPathLength == 0xFF || rawPathLength < 0) {
decodedPathLength = -1;
decodedPathBytes = Uint8List(0);
} else if (rawPathLength >= 64) {
final mode = (rawPathLength & 0xC0) >> 6;
final hopCount = rawPathLength & 0x3F;
final width = mode + 1;
final byteLen = hopCount * width;
decodedPathLength = hopCount;
if (byteLen <= rawPathBytes.length) {
decodedPathBytes = rawPathBytes.sublist(0, byteLen);
} else {
decodedPathBytes = Uint8List(0);
}
} else if (rawPathLength == 0) {
decodedPathBytes = Uint8List(0);
}
}
return Message(
senderKey: Uint8List.fromList(base64Decode(json['senderKey'] as String)),
text: decodedText,
@@ -138,10 +167,8 @@ class MessageStore {
? DateTime.fromMillisecondsSinceEpoch(json['deliveredAt'] as int)
: null,
tripTimeMs: json['tripTimeMs'] as int?,
pathLength: json['pathLength'] as int?,
pathBytes: json['pathBytes'] != null
? Uint8List.fromList(base64Decode(json['pathBytes'] as String))
: Uint8List(0),
pathLength: decodedPathLength,
pathBytes: decodedPathBytes,
reactions:
(json['reactions'] as Map<String, dynamic>?)?.map(
(key, value) => MapEntry(key, value as int),
+107 -5
View File
@@ -2,11 +2,16 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/models/channel_message.dart';
import 'package:meshcore_open/models/contact.dart';
import 'package:meshcore_open/models/message.dart';
import 'package:meshcore_open/models/path_history.dart';
import 'package:meshcore_open/models/app_settings.dart';
import 'package:meshcore_open/storage/prefs_manager.dart';
import 'package:meshcore_open/storage/contact_store.dart';
import 'package:meshcore_open/storage/message_store.dart';
// Builds a valid contact frame with the given pathLen and optional overrides.
// Frame layout: [respCode(1)][pubKey(32)][type(1)][flags(1)][pathLen(1)][path(64)][name(32)][timestamp(4)][lat(4)][lon(4)]
@@ -147,11 +152,11 @@ void main() {
expect(contact!.pathLength, equals(1));
});
test('pathLen == 64 (maxPathSize) → pathLength == 64', () {
final frame = _buildContactFrame(pathLen: maxPathSize);
test('pathLen == 64 (mode 1, 0 hops) → pathLength == 0', () {
final frame = _buildContactFrame(pathLen: 64);
final contact = Contact.fromFrame(frame);
expect(contact, isNotNull);
expect(contact!.pathLength, equals(maxPathSize));
expect(contact!.pathLength, equals(0));
});
test('pathLen == 0xFF → pathLength == -1 (flood)', () {
@@ -161,11 +166,18 @@ void main() {
expect(contact!.pathLength, equals(-1));
});
test('pathLen == 65 (over maxPathSize) → pathLength == -1 (flood)', () {
test('pathLen == 65 (mode 1, 1 hop) → pathLength == 1', () {
final frame = _buildContactFrame(pathLen: 65);
final contact = Contact.fromFrame(frame);
expect(contact, isNotNull);
expect(contact!.pathLength, equals(-1));
expect(contact!.pathLength, equals(1));
});
test('pathLen == 129 (mode 2, 1 hop) → pathLength == 1', () {
final frame = _buildContactFrame(pathLen: 129);
final contact = Contact.fromFrame(frame);
expect(contact, isNotNull);
expect(contact!.pathLength, equals(1));
});
});
@@ -452,4 +464,94 @@ void main() {
expect(updated.maxRouteWeight, equals(settings.maxRouteWeight));
});
});
group('Storage migration — multi-byte paths compatibility', () {
setUp(() async {
SharedPreferences.setMockInitialValues({});
await PrefsManager.initialize();
});
test('ContactStore decodes and migrates legacy mode-encoded paths', () async {
final store = ContactStore()..publicKeyHex = '1234567890';
// Let's create a contact with legacy 64 path length (mode 1, 0 hops)
final rawPath = Uint8List(64); // 64 bytes of zeroes
final contactJson = [
{
'publicKey': base64Encode(Uint8List(32)..[0] = 0xAA),
'name': 'LegacyNode',
'type': 2,
'flags': 0,
'pathLength': 64, // encoded pathLength (mode 1, 0 hops)
'path': base64Encode(rawPath),
'lastSeen': DateTime.now().millisecondsSinceEpoch,
'lastMessageAt': DateTime.now().millisecondsSinceEpoch,
'isActive': true,
}
];
final prefs = PrefsManager.instance;
await prefs.setString(store.keyFor, jsonEncode(contactJson));
final contacts = await store.loadContacts();
expect(contacts, hasLength(1));
expect(contacts.first.pathLength, equals(0));
expect(contacts.first.path, isEmpty);
});
test('ContactStore decodes and migrates legacy mode-1 paths with hops', () async {
final store = ContactStore()..publicKeyHex = '1234567890';
// Contact with legacy 65 path length (mode 1, 1 hop)
final rawPath = Uint8List(64)..[0] = 0xBB..[1] = 0xCC;
final contactJson = [
{
'publicKey': base64Encode(Uint8List(32)..[0] = 0xAA),
'name': 'LegacyNode2',
'type': 2,
'flags': 0,
'pathLength': 65, // encoded pathLength (mode 1, 1 hop)
'path': base64Encode(rawPath),
'lastSeen': DateTime.now().millisecondsSinceEpoch,
'lastMessageAt': DateTime.now().millisecondsSinceEpoch,
'isActive': true,
}
];
final prefs = PrefsManager.instance;
await prefs.setString(store.keyFor, jsonEncode(contactJson));
final contacts = await store.loadContacts();
expect(contacts, hasLength(1));
expect(contacts.first.pathLength, equals(1));
expect(contacts.first.path, equals(Uint8List.fromList([0xBB, 0xCC])));
});
test('MessageStore decodes and migrates legacy mode-encoded message paths', () async {
final store = MessageStore()..publicKeyHex = '1234567890';
final contactKeyHex = pubKeyToHex(Uint8List(32)..[0] = 0xAA);
final rawPath = Uint8List(64);
final messageJson = [
{
'senderKey': base64Encode(Uint8List(32)..[0] = 0xAA),
'text': 'Hello',
'timestamp': DateTime.now().millisecondsSinceEpoch,
'isOutgoing': false,
'status': 2, // delivered
'messageId': 'msg_1',
'pathLength': 64, // encoded pathLength (mode 1, 0 hops)
'pathBytes': base64Encode(rawPath),
}
];
final prefs = PrefsManager.instance;
await prefs.setString('${store.keyFor}$contactKeyHex', jsonEncode(messageJson));
final messages = await store.loadMessages(contactKeyHex);
expect(messages, hasLength(1));
expect(messages.first.pathLength, equals(0));
expect(messages.first.pathBytes, isEmpty);
});
});
}