From 61fb39b9f86874093bf2e39f463733b677e04e86 Mon Sep 17 00:00:00 2001 From: HDDen <62592944+HDDen@users.noreply.github.com> Date: Thu, 28 May 2026 03:17:08 +0300 Subject: [PATCH] Multibyte: some path fixes - Validate encoded path hop counts before saving or sending custom paths - Fix multi-byte path reversal by reversing hops instead of raw bytes - Decode raw log path lengths using packed path_len format - Localize path hash mode options and optimize map anchor lookup --- lib/connector/meshcore_connector.dart | 68 +++++++++++++++++++++----- lib/screens/map_screen.dart | 37 ++++++++------ lib/screens/settings_screen.dart | 22 +++++++-- lib/widgets/path_selection_dialog.dart | 13 +++-- 4 files changed, 104 insertions(+), 36 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 1f4ba0e5..51fe11bc 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2598,6 +2598,24 @@ class MeshCoreConnector extends ChangeNotifier { } } + bool _isPathLenValidForCurrentMode(int pathLen, List pathBytes) { + return _isPathLenValidForMode(pathLen, pathBytes, _pathHashByteWidth); + } + + int? _encodePathLenForCurrentMode(int pathLen, List pathBytes) { + if (pathLen < 0 || pathLen == 0xFF) return pathLen; + if (!_isPathLenValidForCurrentMode(pathLen, pathBytes)) { + appLogger.warn( + 'Invalid path_len for mode: pathLen=$pathLen, ' + 'bytesLen=${pathBytes.length}, width=$_pathHashByteWidth', + tag: 'Connector', + ); + return null; + } + final mode = (_pathHashByteWidth - 1) & 0x03; + return (pathLen & 0x3F) | (mode << 6); + } + Future refreshDeviceInfo() async { if (!isConnected) return; if (PlatformInfo.isWeb && @@ -2797,11 +2815,10 @@ class MeshCoreConnector extends ChangeNotifier { try { if (!isConnected) return; - final mode = _pathHashByteWidth - 1; - if (pathLen < 0 || pathLen > 0x3F) { + final encodedPathLen = _encodePathLenForCurrentMode(pathLen, customPath); + if (encodedPathLen == null) { return; } - final encodedPathLen = pathLen | (mode << 6); await sendFrame( buildUpdateContactPathFrame( contact.publicKey, @@ -2863,11 +2880,11 @@ 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; + final encodedPathLen = _encodePathLenForCurrentMode( + latestContact.pathLength, + latestContact.path, + ); + if (encodedPathLen == null) return; await sendFrame( buildUpdateContactPathFrame( latestContact.publicKey, @@ -2955,6 +2972,19 @@ class MeshCoreConnector extends ChangeNotifier { tag: 'Connector', ); + if (pathLen != null && + pathLen >= 0 && + (pathBytes == null || + !_isPathLenValidForCurrentMode(pathLen, pathBytes))) { + appLogger.warn( + 'setPathOverride: invalid path for ${contact.name}: ' + 'pathLen=$pathLen, bytesLen=${pathBytes?.length ?? 0}, ' + 'width=$_pathHashByteWidth', + tag: 'Connector', + ); + return; + } + // Update contact with new path override _contacts[index] = _contacts[index].copyWith( pathOverride: pathLen, @@ -3208,11 +3238,11 @@ class MeshCoreConnector extends ChangeNotifier { Future 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; + final encodedPathLen = _encodePathLenForCurrentMode( + contact.pathLength, + contact.path, + ); + if (encodedPathLen == null) return; await sendFrame( buildUpdateContactPathFrame( contact.publicKey, @@ -6535,6 +6565,18 @@ int _decodePathHashWidth(int pathLenRaw) { return ((pathLenRaw >> 6) & 0x03) + 1; } +bool _isPathLenValidForMode( + int pathLen, + List pathBytes, + int pathHashWidth, +) { + if (pathLen < 0 || pathLen > 0x3F) return false; + final width = pathHashWidth.clamp(1, 4).toInt(); + final maxHopCountByBytes = maxPathSize ~/ width; + if (pathLen > maxHopCountByBytes) return false; + return pathBytes.length <= maxPathSize && pathBytes.length == pathLen * width; +} + Uint8List _reversePathByHop(Uint8List pathBytes, int pathHashWidth) { if (pathBytes.isEmpty) return Uint8List(0); final width = pathHashWidth.clamp(1, 4).toInt(); diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index b6634813..efd00718 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -700,6 +700,16 @@ class _MapScreenState extends State { int pathHashByteWidth, ) { final result = <_GuessedLocation>[]; + final hopWidth = pathHashByteWidth.clamp(1, 4).toInt(); + final anchorsByPrefix = >{}; + for (final repeater in withLocation) { + if (repeater.type != advTypeRepeater) continue; + if (repeater.publicKey.length < hopWidth) continue; + final prefix = PathHelper.formatHopHex( + repeater.publicKey.sublist(0, hopWidth), + ); + anchorsByPrefix.putIfAbsent(prefix, () => []).add(repeater); + } for (final contact in allContacts) { if (contact.hasLocation) continue; @@ -722,21 +732,13 @@ class _MapScreenState extends State { ]; for (final pathBytes in pathSets) { if (pathBytes.isEmpty) continue; - final hopWidth = pathHashByteWidth.clamp(1, 4); final lastHop = pathBytes.sublist(max(0, pathBytes.length - hopWidth)); if (lastHop.isEmpty) continue; - for (final repeater in withLocation) { - if (repeater.type != advTypeRepeater) continue; - if (repeater.publicKey.length < lastHop.length) continue; - if (!listEquals( - repeater.publicKey.sublist(0, lastHop.length), - lastHop, - )) { - continue; - } + final repeaters = anchorsByPrefix[PathHelper.formatHopHex(lastHop)]; + if (repeaters != null && repeaters.isNotEmpty) { + final repeater = repeaters.first; anchorSet.add(LatLng(repeater.latitude!, repeater.longitude!)); - break; } } @@ -2304,10 +2306,17 @@ class _MapScreenState extends State { connector.pathHashByteWidth.clamp(1, pubKeySize), contact.publicKey.length, ).toInt(); + final hopPrefix = contact.publicKey.sublist(0, hopWidth); + for (final existingHop in PathHelper.splitPathBytes( + _pathTrace, + connector.pathHashByteWidth, + )) { + if (listEquals(existingHop, hopPrefix)) { + return; + } + } setState(() { - _pathTrace.addAll( - contact.publicKey.sublist(0, hopWidth), - ); // Add the hop-width prefix of the public key to the trace + _pathTrace.addAll(hopPrefix); // Add the hop-width pubkey prefix. _pathTraceHopWidths.add(hopWidth); _pathTraceContacts.add( contact.copyWith( diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 3294546f..49181fcc 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -580,11 +580,23 @@ class _SettingsScreenState extends State { labelText: l10n.repeater_pathHashMode, border: const OutlineInputBorder(), ), - items: const [ - DropdownMenuItem(value: 0, child: Text('0 - 1 byte')), - DropdownMenuItem(value: 1, child: Text('1 - 2 bytes')), - DropdownMenuItem(value: 2, child: Text('2 - 3 bytes')), - DropdownMenuItem(value: 3, child: Text('3 - 4 bytes')), + items: [ + DropdownMenuItem( + value: 0, + child: Text(l10n.repeater_pathHashModeOption0), + ), + DropdownMenuItem( + value: 1, + child: Text(l10n.repeater_pathHashModeOption1), + ), + DropdownMenuItem( + value: 2, + child: Text(l10n.repeater_pathHashModeOption2), + ), + DropdownMenuItem( + value: 3, + child: Text(l10n.repeater_pathHashModeOption3), + ), ], onChanged: (value) { if (value == null) return; diff --git a/lib/widgets/path_selection_dialog.dart b/lib/widgets/path_selection_dialog.dart index 949ebbe8..c87b5a9b 100644 --- a/lib/widgets/path_selection_dialog.dart +++ b/lib/widgets/path_selection_dialog.dart @@ -126,7 +126,8 @@ class _PathSelectionDialogState extends State { .toList(); final pathBytesList = []; final invalidPrefixes = []; - final hexCharsPerHop = widget.pathHashByteWidth.clamp(1, 4) * 2; + final pathHashByteWidth = widget.pathHashByteWidth.clamp(1, 4).toInt(); + final hexCharsPerHop = pathHashByteWidth * 2; for (final id in pathIds) { if (id.length < hexCharsPerHop) { @@ -157,8 +158,12 @@ class _PathSelectionDialogState extends State { return; } - // Check max path size in bytes, as defined by the protocol. - if (pathBytesList.length > maxPathSize) { + final hopCount = pathBytesList.length ~/ pathHashByteWidth; + final maxHopCountByBytes = maxPathSize ~/ pathHashByteWidth; + final maxHopCount = maxHopCountByBytes < 0x3F ? maxHopCountByBytes : 0x3F; + + // path_len stores hop count in 6 bits and the path buffer is byte-limited. + if (hopCount > maxHopCount || pathBytesList.length > maxPathSize) { showDismissibleSnackBar( context, content: Text(l10n.path_tooLong), @@ -235,7 +240,7 @@ class _PathSelectionDialogState extends State { helperText: l10n.path_helperMaxHops, ), textCapitalization: TextCapitalization.characters, - maxLength: 191, // 64 hops * 2 chars + 63 commas + maxLength: 188, // 63 one-byte hops * 2 chars + 62 commas ), const SizedBox(height: 16), const Divider(),