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
This commit is contained in:
HDDen
2026-05-28 03:17:08 +03:00
parent 9e18b99e9d
commit 61fb39b9f8
4 changed files with 104 additions and 36 deletions
+55 -13
View File
@@ -2598,6 +2598,24 @@ class MeshCoreConnector extends ChangeNotifier {
} }
} }
bool _isPathLenValidForCurrentMode(int pathLen, List<int> pathBytes) {
return _isPathLenValidForMode(pathLen, pathBytes, _pathHashByteWidth);
}
int? _encodePathLenForCurrentMode(int pathLen, List<int> 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<void> refreshDeviceInfo() async { Future<void> refreshDeviceInfo() async {
if (!isConnected) return; if (!isConnected) return;
if (PlatformInfo.isWeb && if (PlatformInfo.isWeb &&
@@ -2797,11 +2815,10 @@ class MeshCoreConnector extends ChangeNotifier {
try { try {
if (!isConnected) return; if (!isConnected) return;
final mode = _pathHashByteWidth - 1; final encodedPathLen = _encodePathLenForCurrentMode(pathLen, customPath);
if (pathLen < 0 || pathLen > 0x3F) { if (encodedPathLen == null) {
return; return;
} }
final encodedPathLen = pathLen | (mode << 6);
await sendFrame( await sendFrame(
buildUpdateContactPathFrame( buildUpdateContactPathFrame(
contact.publicKey, contact.publicKey,
@@ -2863,11 +2880,11 @@ class MeshCoreConnector extends ChangeNotifier {
: (updatedFlags & ~contactFlagTeleEnv)) : (updatedFlags & ~contactFlagTeleEnv))
: updatedFlags; : updatedFlags;
final mode = _pathHashByteWidth - 1; final encodedPathLen = _encodePathLenForCurrentMode(
final encodedPathLen = latestContact.pathLength,
(latestContact.pathLength >= 0 && latestContact.pathLength != 0xFF) latestContact.path,
? (latestContact.pathLength | (mode << 6)) );
: latestContact.pathLength; if (encodedPathLen == null) return;
await sendFrame( await sendFrame(
buildUpdateContactPathFrame( buildUpdateContactPathFrame(
latestContact.publicKey, latestContact.publicKey,
@@ -2955,6 +2972,19 @@ class MeshCoreConnector extends ChangeNotifier {
tag: 'Connector', 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 // Update contact with new path override
_contacts[index] = _contacts[index].copyWith( _contacts[index] = _contacts[index].copyWith(
pathOverride: pathLen, pathOverride: pathLen,
@@ -3208,11 +3238,11 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> importDiscoveredContact(Contact contact) async { Future<void> importDiscoveredContact(Contact contact) async {
if (!isConnected) return; if (!isConnected) return;
final mode = _pathHashByteWidth - 1; final encodedPathLen = _encodePathLenForCurrentMode(
final encodedPathLen = contact.pathLength,
(contact.pathLength >= 0 && contact.pathLength != 0xFF) contact.path,
? (contact.pathLength | (mode << 6)) );
: contact.pathLength; if (encodedPathLen == null) return;
await sendFrame( await sendFrame(
buildUpdateContactPathFrame( buildUpdateContactPathFrame(
contact.publicKey, contact.publicKey,
@@ -6535,6 +6565,18 @@ int _decodePathHashWidth(int pathLenRaw) {
return ((pathLenRaw >> 6) & 0x03) + 1; return ((pathLenRaw >> 6) & 0x03) + 1;
} }
bool _isPathLenValidForMode(
int pathLen,
List<int> 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) { Uint8List _reversePathByHop(Uint8List pathBytes, int pathHashWidth) {
if (pathBytes.isEmpty) return Uint8List(0); if (pathBytes.isEmpty) return Uint8List(0);
final width = pathHashWidth.clamp(1, 4).toInt(); final width = pathHashWidth.clamp(1, 4).toInt();
+23 -14
View File
@@ -700,6 +700,16 @@ class _MapScreenState extends State<MapScreen> {
int pathHashByteWidth, int pathHashByteWidth,
) { ) {
final result = <_GuessedLocation>[]; final result = <_GuessedLocation>[];
final hopWidth = pathHashByteWidth.clamp(1, 4).toInt();
final anchorsByPrefix = <String, List<Contact>>{};
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) { for (final contact in allContacts) {
if (contact.hasLocation) continue; if (contact.hasLocation) continue;
@@ -722,21 +732,13 @@ class _MapScreenState extends State<MapScreen> {
]; ];
for (final pathBytes in pathSets) { for (final pathBytes in pathSets) {
if (pathBytes.isEmpty) continue; if (pathBytes.isEmpty) continue;
final hopWidth = pathHashByteWidth.clamp(1, 4);
final lastHop = pathBytes.sublist(max(0, pathBytes.length - hopWidth)); final lastHop = pathBytes.sublist(max(0, pathBytes.length - hopWidth));
if (lastHop.isEmpty) continue; if (lastHop.isEmpty) continue;
for (final repeater in withLocation) { final repeaters = anchorsByPrefix[PathHelper.formatHopHex(lastHop)];
if (repeater.type != advTypeRepeater) continue; if (repeaters != null && repeaters.isNotEmpty) {
if (repeater.publicKey.length < lastHop.length) continue; final repeater = repeaters.first;
if (!listEquals(
repeater.publicKey.sublist(0, lastHop.length),
lastHop,
)) {
continue;
}
anchorSet.add(LatLng(repeater.latitude!, repeater.longitude!)); anchorSet.add(LatLng(repeater.latitude!, repeater.longitude!));
break;
} }
} }
@@ -2304,10 +2306,17 @@ class _MapScreenState extends State<MapScreen> {
connector.pathHashByteWidth.clamp(1, pubKeySize), connector.pathHashByteWidth.clamp(1, pubKeySize),
contact.publicKey.length, contact.publicKey.length,
).toInt(); ).toInt();
final hopPrefix = contact.publicKey.sublist(0, hopWidth);
for (final existingHop in PathHelper.splitPathBytes(
_pathTrace,
connector.pathHashByteWidth,
)) {
if (listEquals(existingHop, hopPrefix)) {
return;
}
}
setState(() { setState(() {
_pathTrace.addAll( _pathTrace.addAll(hopPrefix); // Add the hop-width pubkey prefix.
contact.publicKey.sublist(0, hopWidth),
); // Add the hop-width prefix of the public key to the trace
_pathTraceHopWidths.add(hopWidth); _pathTraceHopWidths.add(hopWidth);
_pathTraceContacts.add( _pathTraceContacts.add(
contact.copyWith( contact.copyWith(
+17 -5
View File
@@ -580,11 +580,23 @@ class _SettingsScreenState extends State<SettingsScreen> {
labelText: l10n.repeater_pathHashMode, labelText: l10n.repeater_pathHashMode,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
items: const [ items: [
DropdownMenuItem(value: 0, child: Text('0 - 1 byte')), DropdownMenuItem(
DropdownMenuItem(value: 1, child: Text('1 - 2 bytes')), value: 0,
DropdownMenuItem(value: 2, child: Text('2 - 3 bytes')), child: Text(l10n.repeater_pathHashModeOption0),
DropdownMenuItem(value: 3, child: Text('3 - 4 bytes')), ),
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) { onChanged: (value) {
if (value == null) return; if (value == null) return;
+9 -4
View File
@@ -126,7 +126,8 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
.toList(); .toList();
final pathBytesList = <int>[]; final pathBytesList = <int>[];
final invalidPrefixes = <String>[]; final invalidPrefixes = <String>[];
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) { for (final id in pathIds) {
if (id.length < hexCharsPerHop) { if (id.length < hexCharsPerHop) {
@@ -157,8 +158,12 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
return; return;
} }
// Check max path size in bytes, as defined by the protocol. final hopCount = pathBytesList.length ~/ pathHashByteWidth;
if (pathBytesList.length > maxPathSize) { 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( showDismissibleSnackBar(
context, context,
content: Text(l10n.path_tooLong), content: Text(l10n.path_tooLong),
@@ -235,7 +240,7 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
helperText: l10n.path_helperMaxHops, helperText: l10n.path_helperMaxHops,
), ),
textCapitalization: TextCapitalization.characters, 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 SizedBox(height: 16),
const Divider(), const Divider(),