mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-12 11:52:07 +10:00
fix multibyte path in private message and repeter
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user