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 {
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<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;
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<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) {
if (pathBytes.isEmpty) return Uint8List(0);
final width = pathHashWidth.clamp(1, 4).toInt();
+23 -14
View File
@@ -700,6 +700,16 @@ class _MapScreenState extends State<MapScreen> {
int pathHashByteWidth,
) {
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) {
if (contact.hasLocation) continue;
@@ -722,21 +732,13 @@ class _MapScreenState extends State<MapScreen> {
];
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<MapScreen> {
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(
+17 -5
View File
@@ -580,11 +580,23 @@ class _SettingsScreenState extends State<SettingsScreen> {
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;
+9 -4
View File
@@ -126,7 +126,8 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
.toList();
final pathBytesList = <int>[];
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) {
if (id.length < hexCharsPerHop) {
@@ -157,8 +158,12 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
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<PathSelectionDialog> {
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(),