fix(#71): testable offband_caps read + byte-82 offset verification (N1)

Gemini MAJOR #4 (offset unverified / short-frame robustness):
- Offset VERIFIED against firmware MyMesh.cpp: the device-info tail is three
  consecutive out_frame[i++] writes -- client_repeat@80, path_hash_mode@81,
  offband_caps@82. Bytes 80/81 are shipping (path-hash), so caps@82 is correct
  by adjacency, not a guess.
- Extracted MeshCoreConnector.parseOffbandCaps (pure, bounds-checked) so the
  guard + offset are unit-testable; _handleDeviceInfo delegates to it.
- 4 tests pin: pre-v14 short frame -> null, len-83 reads byte 82 verbatim,
  observer bit round-trips supportsConfig, hostile truncated frames never OOB.

Live observer node confirms end-to-end at G2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/64-observer-config
Strycher 4 weeks ago
parent 7b450535dd
commit 1b5987feaa

@ -4031,6 +4031,17 @@ class MeshCoreConnector extends ChangeNotifier {
_maybeStartInitialChannelSync();
}
/// Extract the additive `offband_caps` byte from a device-info reply.
///
/// Offset verified against firmware MyMesh.cpp: the reply tail is three
/// consecutive `out_frame[i++]` writes client_repeat (byte 80, v9+),
/// path_hash_mode (byte 81, v10+), offband_caps (byte 82, v14+). Bytes 80/81
/// are shipping/working (path-hash feature), so byte 82 is the adjacent next
/// byte by construction. Pre-v14 firmware sends a shorter frame -> null.
/// Bounds-checked: a truncated or hostile short frame never indexes OOB.
static int? parseOffbandCaps(Uint8List frame) =>
frame.length >= 83 ? frame[82] : null;
void _handleDeviceInfo(Uint8List frame) {
if (frame.length < 4) return;
if (_shouldGateInitialChannelSync) {
@ -4049,9 +4060,9 @@ class MeshCoreConnector extends ChangeNotifier {
} else {
_pathHashByteWidth = 1;
}
// Offband config capability v14+ (byte 82): offband_caps bitfield, appended
// after path_hash_mode (additive older firmware sends a shorter frame).
_offbandCaps = frame.length >= 83 ? frame[82] : null;
// Offband config capability v14+ (byte 82). Extracted + bounds-checked in a
// testable helper; offset verified against firmware (see parseOffbandCaps).
_offbandCaps = parseOffbandCaps(frame);
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
final reportedContacts = frame[2];

@ -0,0 +1,60 @@
// byte-82 offset + short-frame robustness for the device-info offband_caps
// read (#64 N1, Gemini MAJOR #4).
//
// The offset itself is verified against firmware source (MyMesh.cpp writes
// client_repeat@80, path_hash_mode@81, offband_caps@82 as consecutive
// out_frame[i++]; bytes 80/81 are shipping). These tests pin the bounds guard
// so a truncated or hostile short device-info frame can never index OOB, and
// that byte 82 is read verbatim when present. A live observer node confirms
// end-to-end at G2.
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/connector/observer_config_client.dart';
void main() {
// A device-info frame of [len] bytes, byte 82 = [caps] when the frame is long
// enough to carry it.
Uint8List frame(int len, {int caps = 0}) {
final f = Uint8List(len);
if (len > 82) f[82] = caps;
return f;
}
group('parseOffbandCaps — offset 82 + short-frame robustness', () {
test('pre-v14 frame without the caps byte (len 82) -> null', () {
expect(MeshCoreConnector.parseOffbandCaps(frame(82)), isNull);
});
test('len 83 reads byte 82 verbatim', () {
expect(MeshCoreConnector.parseOffbandCaps(frame(83, caps: 0x01)), 0x01);
expect(MeshCoreConnector.parseOffbandCaps(frame(83, caps: 0x00)), 0x00);
expect(MeshCoreConnector.parseOffbandCaps(frame(90, caps: 0xFF)), 0xFF);
});
test('observer capability bit round-trips through supportsConfig', () {
final caps = MeshCoreConnector.parseOffbandCaps(
frame(83, caps: ObserverConfigClient.capWifiObserver),
)!;
expect(
ObserverConfigClient.supportsConfig(
firmwareVerCode: ObserverConfigClient.minVerCode,
offbandCaps: caps,
),
isTrue,
);
});
test('hostile/truncated short frames never throw, always null', () {
for (final n in [0, 1, 4, 80, 81, 82]) {
expect(
MeshCoreConnector.parseOffbandCaps(frame(n)),
isNull,
reason: 'frame length $n must not index byte 82',
);
}
});
});
}
Loading…
Cancel
Save

Powered by TurnKey Linux.