Refactor data handling of contacts#267
Conversation
…oved readability and efficiency
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee7fa14f96
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| type: type, | ||
| flags: flags, | ||
| pathLength: pathLen, | ||
| pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1, |
There was a problem hiding this comment.
Preserve direct path length when decoding contact frames
pathLen is read as an unsigned byte, but this assignment maps every pathLen <= 0 to -1, so a valid direct route (pathLen == 0) is now decoded as flood. That changes routing behavior (pathLength < 0 is treated as flood throughout the connector) and can break follow-up writes that pass this value back into buildUpdateContactPathFrame (which writes pathLen as a byte). Keep 0 as direct and only map the flood sentinel to -1.
Useful? React with 👍 / 👎.
| final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen); | ||
| final name = reader.readCStringGreedy(maxNameSize); | ||
|
|
||
| final lastMod = reader.readUInt32LE(); |
There was a problem hiding this comment.
Read contact last-seen from the correct offset
This refactor now stores lastSeen from the first 4-byte field after the name, but the protocol defines that field as contactTimestampOffset (132) and the real last-modified value at contactLastModOffset (144) in lib/connector/meshcore_protocol.dart. Because the parser no longer skips the timestamp field before assigning lastSeen, contact freshness/order can regress to stale or incorrect times.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Refactors contact and RX packet parsing to use BufferReader consistently, and adjusts handling for transport route types so flood/direct packets are parsed correctly.
Changes:
- Refactor
Contact.fromFrameto parse sequentially withBufferReaderand add error logging on parse failure. - Update RX parsing in
MeshCoreConnectorto skip transport-specific bytes forrouteTransportFlood/routeTransportDirect. - Extend
BufferReaderwith pointer tracking (rewind,resetPointer) and add a greedy C-string reader variant.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| lib/models/contact.dart | Refactors contact frame parsing to use BufferReader and logs parsing failures. |
| lib/connector/meshcore_protocol.dart | Enhances BufferReader with pointer rewind/reset and adds readCStringGreedy. |
| lib/connector/meshcore_connector.dart | Fixes RX parsing for transport route types by skipping transport-specific bytes before reading path/payload. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| String readCString(int maxLength) { | ||
| _lastPointer = _pointer; | ||
| final value = <int>[]; | ||
| int counter = 0; | ||
| while (counter < maxLength) { | ||
| final byte = readByte(); | ||
| if (byte == 0) break; | ||
| value.add(byte); | ||
| counter++; | ||
| } | ||
| try { | ||
| return utf8.decode(Uint8List.fromList(value), allowMalformed: true); | ||
| } catch (e) { | ||
| return String.fromCharCodes(value); // Latin-1 fallback | ||
| } | ||
| } |
There was a problem hiding this comment.
readCString() sets _lastPointer at the start, but then calls readByte() in a loop; readByte() delegates to readBytes(), which overwrites _lastPointer on every iteration. As a result, rewind() after readCString() will only rewind a single byte (and the initial _lastPointer = _pointer in readCString() is effectively lost). If rewind() is meant to undo the whole readCString() call, preserve the starting offset (e.g. store it locally and restore _lastPointer at the end, or implement the scan using a single readBytes() call).
| final pathLen = reader.readByte(); | ||
| final safePathLen = pathLen > 0 | ||
| ? (pathLen > maxPathSize ? maxPathSize : pathLen) | ||
| : 0; | ||
| final pathBytes = safePathLen > 0 | ||
| ? Uint8List.fromList( | ||
| data.sublist(contactPathOffset, contactPathOffset + safePathLen), | ||
| ) | ||
| : Uint8List(0); | ||
| final name = readCString(data, contactNameOffset, maxNameSize); | ||
| final lastmod = readUint32LE(data, contactLastModOffset); | ||
| final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen); | ||
| final name = reader.readCStringGreedy(maxNameSize); |
There was a problem hiding this comment.
pathLen is read as an unsigned byte, but contact frames previously treated it as a signed int8 (e.g. 0xFF => -1). With the current readByte() approach, a flood sentinel (0xFF) becomes 255, which makes safePathLen clamp to maxPathSize and populates pathBytes even when there should be no path. Read this as a signed value (e.g. readInt8() / .toSigned(8)) and keep safePathLen based on the signed value.
| type: type, | ||
| flags: flags, | ||
| pathLength: pathLen, | ||
| pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1, | ||
| path: pathBytes, | ||
| latitude: lat, | ||
| longitude: lon, | ||
| lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000), | ||
| lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000), | ||
| ); |
There was a problem hiding this comment.
pathLength is being normalized to -1 when pathLen <= 0, which turns the direct case (pathLen == 0) into flood. This will break Contact.pathLabel and outgoing path selection (the code treats pathLength == 0 as direct and < 0 as flood). Preserve 0 for direct and only map negative values (and/or invalid > max) to flood.
| name: name, | ||
| type: type, | ||
| pathLength: pathBytes.length, | ||
| pathLength: pathBytes.isEmpty ? -1 : pathBytes.length, |
There was a problem hiding this comment.
pathLength is set to -1 when pathBytes is empty, which reclassifies direct adverts (empty path, non-flood route) as flood. This also creates an inconsistency with _handlePayloadAdvertReceived, which later refreshes existing contacts using pathLength: path.length (0 for empty). Consider deriving flood vs direct from routeType (or keep pathLength: pathBytes.length and only force -1 when the route type is a flood variant).
| pathLength: pathBytes.isEmpty ? -1 : pathBytes.length, | |
| pathLength: pathBytes.length, |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| final pubKey = reader.readBytes(pubKeySize); | ||
| final type = reader.readByte(); | ||
| final flags = reader.readByte(); | ||
| final pathLen = reader.readByte(); |
There was a problem hiding this comment.
pathLen is read as an unsigned byte (readByte()), but contact pathLength semantics in this codebase rely on a signed int8 (<0=flood, 0=direct, >0=hops). This makes flood routes impossible to represent and will also mis-handle values >= 128. Read this field as signed (e.g., readInt8() / .toSigned(8)) and keep safePathLen logic based on the signed value.
| final pathLen = reader.readByte(); | |
| final pathLen = reader.readByte().toSigned(8); |
| type: type, | ||
| flags: flags, | ||
| pathLength: pathLen, | ||
| pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1, |
There was a problem hiding this comment.
The pathLength mapping here converts pathLen == 0 (direct) into -1 (flood), which contradicts the pathLabel logic and model docs (-1=flood, 0=direct). Preserve 0 for direct, and only use <0 for flood (or derive flood/direct from the signed pathLen field).
| pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1, | |
| pathLength: pathLen == 0 ? 0 : (pathLen > maxPathSize ? -1 : pathLen), |
| final backupPointer = _pointer; | ||
| final value = <int>[]; | ||
| int counter = 0; | ||
| while (counter < maxLength) { | ||
| final byte = readByte(); | ||
| if (byte == 0) break; | ||
| value.add(byte); | ||
| counter++; | ||
| } | ||
| _lastPointer = backupPointer; |
There was a problem hiding this comment.
readCString() sets _lastPointer at the start, but then calls readByte() in a loop; readByte() updates _lastPointer on every iteration via readBytes(1). As a result, rewind() will jump back only one byte (or some mid-string position) instead of rewinding the whole CString read. Consider implementing readCString() without calling readByte()/readBytes() (read from _buffer[_pointer] directly) or ensure _lastPointer remains the starting pointer for the entire CString operation.
| final backupPointer = _pointer; | |
| final value = <int>[]; | |
| int counter = 0; | |
| while (counter < maxLength) { | |
| final byte = readByte(); | |
| if (byte == 0) break; | |
| value.add(byte); | |
| counter++; | |
| } | |
| _lastPointer = backupPointer; | |
| // Mark the start of this logical read so rewind() can return here. | |
| _lastPointer = _pointer; | |
| final value = <int>[]; | |
| int counter = 0; | |
| // Read directly from the underlying buffer to avoid updating _lastPointer | |
| // on each byte via readByte()/readBytes(). | |
| while (counter < maxLength && _pointer < _buffer.length) { | |
| final byte = _buffer[_pointer]; | |
| _pointer++; | |
| counter++; | |
| if (byte == 0) { | |
| break; | |
| } | |
| value.add(byte); | |
| } |
| return value; | ||
| } | ||
|
|
||
| void resetPointer() => _pointer = 0; |
There was a problem hiding this comment.
resetPointer() resets _pointer but leaves _lastPointer unchanged, so a subsequent rewind() after a reset can jump to an unrelated earlier offset. Resetting should likely also reset _lastPointer to 0 (or to the new _pointer) to keep rewind behavior consistent.
| void resetPointer() => _pointer = 0; | |
| void resetPointer() { | |
| _pointer = 0; | |
| _lastPointer = 0; | |
| } |
| type: type, | ||
| pathLength: pathBytes.length, | ||
| pathLength: pathBytes.isEmpty ? -1 : pathBytes.length, | ||
| path: Uint8List.fromList( |
There was a problem hiding this comment.
Setting pathLength to -1 whenever pathBytes is empty loses the distinction between direct (0 hops) and flood (-1). Since routeType is already parsed above, consider setting pathLength based on routeType when pathBytes.isEmpty (e.g., direct -> 0, flood -> -1) to keep DiscoveryContact.pathLabel accurate.
Refactor data handling in MeshCoreConnector for contacts.
And improved BufferReader.
This fixes the routeTransportFlood and routeTransportDirect packet types not being parsed correctly.
Add reset and rewind to the BufferReader.