refactor: Replace string reading methods with CString equivalents and improve error handling

This commit is contained in:
Winston Lowe
2026-03-22 10:50:11 -07:00
parent dbefb0b5f4
commit 767dc1164e
8 changed files with 82 additions and 135 deletions
+43 -49
View File
@@ -2127,9 +2127,7 @@ class MeshCoreConnector extends ChangeNotifier {
outboundText, outboundText,
selfKey, selfKey,
); );
final ackHashHex = ackHash final ackHashHex = ackHashToHex(ackHash);
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
final messageBytes = utf8.encode(outboundText).length; final messageBytes = utf8.encode(outboundText).length;
_pendingRepeaterAcks[ackHashHex]?.timeout?.cancel(); _pendingRepeaterAcks[ackHashHex]?.timeout?.cancel();
_pendingRepeaterAcks[ackHashHex] = _RepeaterAckContext( _pendingRepeaterAcks[ackHashHex] = _RepeaterAckContext(
@@ -2901,7 +2899,7 @@ class MeshCoreConnector extends ChangeNotifier {
_currentSf = reader.readByte(); _currentSf = reader.readByte();
_currentCr = reader.readByte(); _currentCr = reader.readByte();
_selfName = reader.readString(); _selfName = reader.readCString();
} catch (e) { } catch (e) {
_appDebugLogService?.error( _appDebugLogService?.error(
'Error parsing SELF_INFO frame: $e', 'Error parsing SELF_INFO frame: $e',
@@ -3554,7 +3552,7 @@ class MeshCoreConnector extends ChangeNotifier {
reader.skipBytes(4); // Skip extra 4 bytes for signed/plain variants reader.skipBytes(4); // Skip extra 4 bytes for signed/plain variants
} }
final msgText = reader.readString(); final msgText = reader.readCString();
final flags = txtType; final flags = txtType;
final shiftedType = flags >> 2; final shiftedType = flags >> 2;
@@ -3724,10 +3722,9 @@ class MeshCoreConnector extends ChangeNotifier {
final packet = _parseRawPacket(raw); final packet = _parseRawPacket(raw);
if (packet == null || packet.payloadType != _payloadTypeGroupText) return; if (packet == null || packet.payloadType != _payloadTypeGroupText) return;
final payload = packet.payload; final payload = BufferReader(packet.payload);
if (payload.length <= _cipherMacSize) return; final channelHash = payload.readByte();
final channelHash = payload[0]; final encrypted = Uint8List.fromList(payload.readRemainingBytes());
final encrypted = Uint8List.fromList(payload.sublist(1));
// Use cached channels as fallback if live channels not yet loaded // Use cached channels as fallback if live channels not yet loaded
final channelsToSearch = _channels.isNotEmpty final channelsToSearch = _channels.isNotEmpty
@@ -3741,15 +3738,14 @@ class MeshCoreConnector extends ChangeNotifier {
final decryptedBytes = _decryptPayload(channel.psk, encrypted); final decryptedBytes = _decryptPayload(channel.psk, encrypted);
if (decryptedBytes == null || decryptedBytes.length < 6) return; if (decryptedBytes == null || decryptedBytes.length < 6) return;
final decrypted = BufferReader(decryptedBytes); final decrypted = BufferReader(decryptedBytes);
// Skip header + SNR + reserved (2)
decrypted.skipBytes(4); final timestampRaw = decrypted.readUInt32LE();
final txtType = decrypted.readByte(); final txtType = decrypted.readByte();
if ((txtType >> 2) != 0) { if ((txtType >> 2) != 0) {
return; return;
} }
final timestampRaw = decrypted.readUInt32LE(); final text = decrypted.readCString();
final text = decrypted.readString();
final parsed = _splitSenderText(text); final parsed = _splitSenderText(text);
final decodedText = final decodedText =
Smaz.tryDecodePrefixed(parsed.text) ?? parsed.text; Smaz.tryDecodePrefixed(parsed.text) ?? parsed.text;
@@ -3811,13 +3807,13 @@ class MeshCoreConnector extends ChangeNotifier {
try { try {
final reader = BufferReader(frame); final reader = BufferReader(frame);
reader.skipBytes(2); // code + is_flood reader.skipBytes(2); //Skip code and is_flood
final ackHash = reader.readUInt32LE(); final ackHash = reader.readUInt32LE();
final timeoutMs = reader.readUInt32LE(); final timeoutMs = reader.readUInt32LE();
// Check if this is a CLI command ACK - if so, ignore it // Check if this is a CLI command ACK - if so, ignore it
if (_lastSentWasCliCommand) { if (_lastSentWasCliCommand) {
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0'); final ackHashHex = ackHashToHex(ackHash);
debugPrint('Ignoring CLI command ACK (sent): $ackHashHex'); debugPrint('Ignoring CLI command ACK (sent): $ackHashHex');
_lastSentWasCliCommand = false; _lastSentWasCliCommand = false;
return; return;
@@ -3949,7 +3945,7 @@ class MeshCoreConnector extends ChangeNotifier {
} }
bool _handleRepeaterCommandSent(int ackHash, int timeoutMs) { bool _handleRepeaterCommandSent(int ackHash, int timeoutMs) {
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0'); final ackHashHex = ackHashToHex(ackHash);
final entry = _pendingRepeaterAcks[ackHashHex]; final entry = _pendingRepeaterAcks[ackHashHex];
if (entry == null) return false; if (entry == null) return false;
@@ -3968,7 +3964,7 @@ class MeshCoreConnector extends ChangeNotifier {
} }
bool _handleRepeaterCommandAck(int ackHash, int tripTimeMs) { bool _handleRepeaterCommandAck(int ackHash, int tripTimeMs) {
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0'); final ackHashHex = ackHashToHex(ackHash);
final entry = _pendingRepeaterAcks.remove(ackHashHex); final entry = _pendingRepeaterAcks.remove(ackHashHex);
if (entry == null) return false; if (entry == null) return false;
entry.timeout?.cancel(); entry.timeout?.cancel();
@@ -4319,36 +4315,34 @@ class MeshCoreConnector extends ChangeNotifier {
} }
_RawPacket? _parseRawPacket(Uint8List raw) { _RawPacket? _parseRawPacket(Uint8List raw) {
if (raw.length < 3) return null; try {
var index = 0; final reader = BufferReader(raw);
final header = raw[index++]; final header = reader.readByte();
final routeType = header & _phRouteMask; final routeType = header & _phRouteMask;
final hasTransport = final hasTransport =
routeType == _routeTransportFlood || routeType == _routeTransportDirect; routeType == _routeTransportFlood ||
if (hasTransport) { routeType == _routeTransportDirect;
if (raw.length < index + 4) return null; if (hasTransport) {
index += 4; reader.skipBytes(2); // Skip reserved bytes in transport header
} }
if (raw.length <= index) return null; final pathLenRaw = reader.readByte();
final pathLenRaw = raw[index++]; final pathByteLen = _decodePathByteLen(pathLenRaw);
final pathByteLen = _decodePathByteLen(pathLenRaw); final pathBytes = reader.readBytes(pathByteLen);
if (raw.length < index + pathByteLen) return null; final payload = reader.readBytes(reader.remaining);
final pathBytes = Uint8List.fromList(
raw.sublist(index, index + pathByteLen),
);
index += pathByteLen;
if (raw.length <= index) return null;
final payload = Uint8List.fromList(raw.sublist(index));
return _RawPacket( return _RawPacket(
header: header, header: header,
routeType: routeType, routeType: routeType,
payloadType: (header >> _phTypeShift) & _phTypeMask, payloadType: (header >> _phTypeShift) & _phTypeMask,
payloadVer: (header >> _phVerShift) & _phVerMask, payloadVer: (header >> _phVerShift) & _phVerMask,
pathLenRaw: pathLenRaw, pathLenRaw: pathLenRaw,
pathBytes: pathBytes, pathBytes: pathBytes,
payload: payload, payload: payload,
); );
} catch (e) {
appLogger.warn('Error parsing raw packet: $e');
return null;
}
} }
int _computeChannelHash(Uint8List psk) { int _computeChannelHash(Uint8List psk) {
@@ -4767,7 +4761,7 @@ class MeshCoreConnector extends ChangeNotifier {
void _handleCustomVars(Uint8List frame) { void _handleCustomVars(Uint8List frame) {
final buf = BufferReader(frame.sublist(1)); final buf = BufferReader(frame.sublist(1));
try { try {
_currentCustomVars = _parseKeyValueString(buf.readString()); _currentCustomVars = _parseKeyValueString(buf.readCString());
} catch (e) { } catch (e) {
appLogger.warn('Malformed custom vars frame: $e', tag: 'Connector'); appLogger.warn('Malformed custom vars frame: $e', tag: 'Connector');
} }
@@ -4924,7 +4918,7 @@ class MeshCoreConnector extends ChangeNotifier {
longitude = packet.readInt32LE() / 1e6; longitude = packet.readInt32LE() / 1e6;
} }
if (hasName && packet.remaining > 0) { if (hasName && packet.remaining > 0) {
name = packet.readString(); name = packet.readCString();
} }
} catch (e) { } catch (e) {
appLogger.warn('Malformed advert frame: $e', tag: 'Connector'); appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
@@ -4986,7 +4980,7 @@ class MeshCoreConnector extends ChangeNotifier {
longitude = advert.readInt32LE() / 1e6; longitude = advert.readInt32LE() / 1e6;
} }
if (hasName && advert.remaining > 0) { if (hasName && advert.remaining > 0) {
name = advert.readString(); name = advert.readCString();
} }
} catch (e) { } catch (e) {
appLogger.warn('Malformed advert frame: $e', tag: 'Connector'); appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
+28 -43
View File
@@ -1,6 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/widgets.dart';
// Buffer Reader - sequential binary data reader with pointer tracking // Buffer Reader - sequential binary data reader with pointer tracking
class BufferReader { class BufferReader {
int _pointer = 0; int _pointer = 0;
@@ -37,16 +39,6 @@ class BufferReader {
Uint8List readRemainingBytes() => readBytes(remaining); Uint8List readRemainingBytes() => readBytes(remaining);
String readString() {
_lastPointer = _pointer;
final value = readRemainingBytes();
try {
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
} catch (e) {
return String.fromCharCodes(value); // Latin-1 fallback
}
}
String readCStringGreedy(int maxLength) { String readCStringGreedy(int maxLength) {
_lastPointer = _pointer; _lastPointer = _pointer;
final value = <int>[]; final value = <int>[];
@@ -62,11 +54,12 @@ class BufferReader {
} }
} }
String readCString(int maxLength) { String readCString({int maxLength = -1}) {
final backupPointer = _pointer; final backupPointer = _pointer;
final value = <int>[]; final value = <int>[];
int counter = 0; int counter = 0;
while (counter < maxLength) { final maxLen = maxLength >= 0 ? maxLength : remaining;
while (counter < maxLen) {
final byte = readByte(); final byte = readByte();
if (byte == 0) break; if (byte == 0) break;
value.add(byte); value.add(byte);
@@ -409,48 +402,40 @@ ParsedContactText? parseContactMessageText(Uint8List frame) {
// Signed messages have a 4-byte signature after the timestamp, before the text // Signed messages have a 4-byte signature after the timestamp, before the text
message.skipBytes(4); message.skipBytes(4);
} }
final text = message.readString(); final text = message.readCString();
if (text.isEmpty) return null; if (text.isEmpty) return null;
return ParsedContactText(senderPrefix: senderPrefix, text: text); return ParsedContactText(senderPrefix: senderPrefix, text: text);
} catch (e) { } catch (e) {
debugPrint('Error parsing contact message text: $e');
return null; return null;
} }
} }
// // Helper to read uint32 little-endian // Helper to read uint32 little-endian
// int readUint32LE(Uint8List data, int offset) { int readUint32LE(Uint8List data, int offset) {
// return data[offset] | return data[offset] |
// (data[offset + 1] << 8) | (data[offset + 1] << 8) |
// (data[offset + 2] << 16) | (data[offset + 2] << 16) |
// (data[offset + 3] << 24); (data[offset + 3] << 24);
// } }
// // Helper to read uint16 little-endian // Helper to read uint16 little-endian
// int readUint16LE(Uint8List data, int offset) { int readUint16LE(Uint8List data, int offset) {
// return data[offset] | (data[offset + 1] << 8); return data[offset] | (data[offset + 1] << 8);
// } }
// // Helper to read int32 little-endian // Helper to read int32 little-endian
// int readInt32LE(Uint8List data, int offset) { int readInt32LE(Uint8List data, int offset) {
// int val = readUint32LE(data, offset); int val = readUint32LE(data, offset);
// if (val >= 0x80000000) val -= 0x100000000; if (val >= 0x80000000) val -= 0x100000000;
// return val; return val;
// } }
// // Helper to read null-terminated UTF-8 string // Helper to convert uint32 to hex string
// String readCString(Uint8List data, int offset, int maxLen) { String ackHashToHex(int ackHash) {
// int end = offset; return ackHash.toRadixString(16).padLeft(8, '0');
// while (end < offset + maxLen && end < data.length && data[end] != 0) { }
// end++;
// }
// try {
// return utf8.decode(data.sublist(offset, end), allowMalformed: true);
// } catch (e) {
// // Fallback to Latin-1 if UTF-8 decoding fails
// return String.fromCharCodes(data.sublist(offset, end));
// }
// }
// Helper to convert public key to hex string // Helper to convert public key to hex string
String pubKeyToHex(Uint8List pubKey) { String pubKeyToHex(Uint8List pubKey) {
+5 -5
View File
@@ -2,6 +2,7 @@ import 'dart:typed_data';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart'; import '../helpers/reaction_helper.dart';
import '../helpers/smaz.dart'; import '../helpers/smaz.dart';
import '../utils/app_logger.dart';
enum ChannelMessageStatus { pending, sent, failed } enum ChannelMessageStatus { pending, sent, failed }
@@ -114,8 +115,8 @@ class ChannelMessage {
// V3: [0]=code [1]=SNR [2]=rsv1 [3]=rsv2 [4]=channel_idx [5]=path_len [path... optional] [txt_type] [timestamp x4] [text...] // V3: [0]=code [1]=SNR [2]=rsv1 [3]=rsv2 [4]=channel_idx [5]=path_len [path... optional] [txt_type] [timestamp x4] [text...]
// Non-V3: [0]=code [1]=channel_idx [2]=path_len [3]=txt_type [4-7]=timestamp [8+]=text // Non-V3: [0]=code [1]=channel_idx [2]=path_len [3]=txt_type [4-7]=timestamp [8+]=text
if (frame.length < 8) return null; if (frame.length < 8) return null;
final reader = BufferReader(frame);
try { try {
final reader = BufferReader(frame);
final code = reader.readByte(); final code = reader.readByte();
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) { if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
return null; return null;
@@ -133,11 +134,9 @@ class ChannelMessage {
pathLen = reader.readByte(); pathLen = reader.readByte();
txtType = reader.readByte(); txtType = reader.readByte();
final hasPath = (flags & 0x01) != 0; final hasPath = (flags & 0x01) != 0;
if (hasPath) { if (hasPath && pathLen > 0) {
reader.rewind(); // Rewind to read path length again for pathBytes reader.rewind(); // Rewind to read path length again for pathBytes
pathBytes = reader.readBytes(pathLen); pathBytes = reader.readBytes(pathLen);
// Force text type to plain if path is present
txtType = txtTypePlain;
} else { } else {
pathLen = 0; pathLen = 0;
} }
@@ -152,7 +151,7 @@ class ChannelMessage {
return null; return null;
} }
final text = reader.readString(); final text = reader.readCString();
// Extract sender name and actual message from "name: msg" format // Extract sender name and actual message from "name: msg" format
String senderName = 'Unknown'; String senderName = 'Unknown';
@@ -185,6 +184,7 @@ class ChannelMessage {
channelIndex: channelIdx, channelIndex: channelIdx,
); );
} catch (e) { } catch (e) {
appLogger.error('Error parsing channel message frame: $e');
// If parsing fails, return null to avoid crashes // If parsing fails, return null to avoid crashes
return null; return null;
} }
+1 -1
View File
@@ -105,7 +105,7 @@ class Message {
if ((flags >> 2) != txtTypePlain) { if ((flags >> 2) != txtTypePlain) {
return null; return null;
} }
final text = reader.readString(); final text = reader.readCString();
return Message( return Message(
senderKey: senderKey, senderKey: senderKey,
-13
View File
@@ -245,19 +245,6 @@ class BleDebugLogService extends ChangeNotifier {
} }
} }
// Helper to read uint32 little-endian
int readUint32LE(Uint8List data, int offset) {
return data[offset] |
(data[offset + 1] << 8) |
(data[offset + 2] << 16) |
(data[offset + 3] << 24);
}
// // Helper to read uint16 little-endian
int readUint16LE(Uint8List data, int offset) {
return data[offset] | (data[offset + 1] << 8);
}
String _frameDetail(int code, Uint8List frame) { String _frameDetail(int code, Uint8List frame) {
switch (code) { switch (code) {
case respCodeSent: case respCodeSent:
+4 -5
View File
@@ -98,7 +98,7 @@ class MessageRetryService extends ChangeNotifier {
/// Compute expected ACK hash using same algorithm as firmware: /// Compute expected ACK hash using same algorithm as firmware:
/// SHA256([timestamp(4)][attempt(1)][text][sender_pubkey(32)]) -> first 4 bytes /// SHA256([timestamp(4)][attempt(1)][text][sender_pubkey(32)]) -> first 4 bytes
static Uint8List computeExpectedAckHash( static int computeExpectedAckHash(
int timestampSeconds, int timestampSeconds,
int attempt, int attempt,
String text, String text,
@@ -126,7 +126,8 @@ class MessageRetryService extends ChangeNotifier {
// Compute SHA256 and return first 4 bytes // Compute SHA256 and return first 4 bytes
final hash = sha256.convert(buffer); final hash = sha256.convert(buffer);
return Uint8List.fromList(hash.bytes.sublist(0, 4)); final bytes = Uint8List.fromList(hash.bytes.sublist(0, 4));
return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0];
} }
Future<void> sendMessageWithRetry({ Future<void> sendMessageWithRetry({
@@ -324,9 +325,7 @@ class MessageRetryService extends ChangeNotifier {
outboundText, outboundText,
selfPubKey, selfPubKey,
); );
final expectedHashHex = expectedHash final expectedHashHex = expectedHash.toRadixString(16).padLeft(8, '0');
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
_expectedHashToMessageId[expectedHashHex] = messageId; _expectedHashToMessageId[expectedHashHex] = messageId;
final shortText = message.text.length > 20 final shortText = message.text.length > 20
-8
View File
@@ -10,14 +10,6 @@ class DebugFrameViewer {
Uint8List frame, Uint8List frame,
String title, String title,
) { ) {
// Helper to read uint32 little-endian
int readUint32LE(Uint8List data, int offset) {
return data[offset] |
(data[offset + 1] << 8) |
(data[offset + 2] << 16) |
(data[offset + 3] << 24);
}
final hexString = frame final hexString = frame
.map((b) => b.toRadixString(16).padLeft(2, '0')) .map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(' '); .join(' ');
+1 -11
View File
@@ -169,16 +169,6 @@ void main() {
expect(first, equals(second)); expect(first, equals(second));
}); });
test('hash is exactly 4 bytes long', () {
final hash = MessageRetryService.computeExpectedAckHash(
fixedTs,
0,
fixedText,
fixedKey,
);
expect(hash.length, equals(4));
});
test('hash matches manual SHA-256 computation', () { test('hash matches manual SHA-256 computation', () {
for (int attempt = 0; attempt < 4; attempt++) { for (int attempt = 0; attempt < 4; attempt++) {
final actual = MessageRetryService.computeExpectedAckHash( final actual = MessageRetryService.computeExpectedAckHash(
@@ -509,7 +499,7 @@ void main() {
fixedText, fixedText,
fixedKey, fixedKey,
); );
final hex = hash.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); final hex = hash.toRadixString(16).padLeft(8, '0');
expect( expect(
hashes.containsKey(hex), hashes.containsKey(hex),
isFalse, isFalse,