Enhance message parsing and error handling in MeshCoreConnector (#260)

* Enhance readString method to include Latin-1 fallback for decoding errors

* Refactor _parseContactMessage to improve error handling and message parsing logic

* Update lib/connector/meshcore_connector.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Winston Lowe
2026-03-04 22:56:39 -08:00
committed by GitHub
parent bd5db9a9d5
commit 7d8e049745
2 changed files with 88 additions and 59 deletions
+80 -57
View File
@@ -2459,70 +2459,93 @@ class MeshCoreConnector extends ChangeNotifier {
}
Message? _parseContactMessage(Uint8List frame) {
if (frame.isEmpty) return null;
final code = frame[0];
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
if (frame.isEmpty) {
appLogger.warn('Received empty frame, ignoring');
return null;
}
final reader = BufferReader(frame);
// Companion radio layout:
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
final prefixOffset = code == respCodeContactMsgRecvV3 ? 4 : 1;
const prefixLen = 6;
final pathLenOffset = prefixOffset + prefixLen;
final txtTypeOffset = pathLenOffset + 1;
final timestampOffset = txtTypeOffset + 1;
final baseTextOffset = timestampOffset + 4;
try {
final code = reader.readByte();
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
appLogger.warn(
'Unexpected message code: $code, expected contact message receive codes',
);
return null;
}
if (frame.length <= baseTextOffset) return null;
final fourBytePubMSG = frame.sublist(baseTextOffset, baseTextOffset + 4);
final senderPrefix = frame.sublist(prefixOffset, prefixOffset + prefixLen);
final flags = frame[txtTypeOffset];
final shiftedType = flags >> 2;
final rawType = flags;
final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain;
final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData;
if (!isPlain && !isCli) {
return null;
}
// Companion radio layout:
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
// double snr = 0;
if (code == respCodeContactMsgRecvV3) {
// Older firmware layout with SNR as a signed byte after the code
// snr = reader.readInt8().toDouble() * 4; // SNR in dB, scaled by 4
reader.skipBytes(1); // Skip SNR byte
reader.skipBytes(2); // Skip reserved bytes
}
// Try base text offset; if empty and there is room for the optional 4-byte extra
// (used by signed/plain variants), try again skipping those bytes.
var text = readCString(
frame,
baseTextOffset,
frame.length - baseTextOffset,
);
if (text.isEmpty && frame.length > baseTextOffset + 4) {
text = readCString(
frame,
baseTextOffset + 4,
frame.length - (baseTextOffset + 4),
final senderPrefix = reader.readBytes(6);
final pathLength = reader.readByte();
final txtType = reader.readByte();
final timestampRaw = reader.readUInt32LE();
final timestamp = DateTime.fromMillisecondsSinceEpoch(
timestampRaw * 1000,
);
if (txtType == 2) {
reader.skipBytes(4); // Skip extra 4 bytes for signed/plain variants
}
final msgText = reader.readString();
final flags = txtType;
final shiftedType = flags >> 2;
final rawType = flags;
final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain;
final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData;
if (!isPlain && !isCli) {
appLogger.warn(
'Unknown message type received: txtType=$txtType, shifted=$shiftedType, raw=$rawType',
);
return null;
}
if (msgText.isEmpty) {
appLogger.warn('Received message with empty text, ignoring');
return null;
}
final decodedText = isCli
? msgText
: (Smaz.tryDecodePrefixed(msgText) ?? msgText);
final contact = _contacts.cast<Contact?>().firstWhere(
(c) => c != null && _matchesPrefix(c.publicKey, senderPrefix),
orElse: () => null,
);
if (contact == null) {
appLogger.warn(
'Received message from unknown contact with prefix: ${senderPrefix.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()).join('')}',
);
return null;
}
return Message(
senderKey: contact.publicKey,
text: decodedText,
timestamp: timestamp,
isOutgoing: false,
isCli: isCli,
status: MessageStatus.delivered,
pathLength: pathLength == 0xFF ? 0 : pathLength,
pathBytes: Uint8List(0),
fourByteRoomContactKey: msgText.length >= 4
? Uint8List.fromList(msgText.substring(0, 4).codeUnits)
: null,
);
} catch (e) {
appLogger.warn('Error parsing contact direct message: $e');
return null;
}
if (text.isEmpty) return null;
final decodedText = isCli ? text : (Smaz.tryDecodePrefixed(text) ?? text);
final timestampRaw = readUint32LE(frame, timestampOffset);
final pathLenByte = frame[pathLenOffset];
final contact = _contacts.cast<Contact?>().firstWhere(
(c) => c != null && _matchesPrefix(c.publicKey, senderPrefix),
orElse: () => null,
);
if (contact == null) return null;
return Message(
senderKey: contact.publicKey,
text: decodedText,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
isOutgoing: false,
isCli: isCli,
status: MessageStatus.delivered,
pathLength: pathLenByte == 0xFF ? 0 : pathLenByte,
pathBytes: Uint8List(0),
fourByteRoomContactKey: fourBytePubMSG,
);
}
bool _matchesPrefix(Uint8List fullKey, Uint8List prefix) {
+8 -2
View File
@@ -34,8 +34,14 @@ class BufferReader {
Uint8List readRemainingBytes() => readBytes(remaining);
String readString() =>
utf8.decode(readRemainingBytes(), allowMalformed: true);
String readString() {
final value = readRemainingBytes();
try {
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
} catch (e) {
return String.fromCharCodes(value); // Latin-1 fallback
}
}
String readCString(int maxLength) {
final value = <int>[];