From 87263e7c50af7103ec53405d5719a8566d9f5925 Mon Sep 17 00:00:00 2001 From: Mohammad Abdul Sahil <127765312+abdulsaheel@users.noreply.github.com> Date: Sat, 19 Sep 2026 01:28:29 +0530 Subject: [PATCH] gate get_data_range on outer cmd status too, not just the body revision byte same stale-buffer hazard getClock already guards against - a failed reply leaves the body untouched so range/backlog numbers from the last good read get reported as current. mirrors getClock's gen5-only gate, gen4 stays ungated since its status byte semantics are still unconfirmed --- lib/src/control.dart | 12 +++++++-- test/control_plane_offsets_test.dart | 37 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/lib/src/control.dart b/lib/src/control.dart index bd75bf8..3bc5d1c 100644 --- a/lib/src/control.dart +++ b/lib/src/control.dart @@ -953,7 +953,15 @@ CmdResponse? parseCommandResponse(Uint8List inner, // (mod 4), so the grid could never land on either and range_oldest / // range_newest were never emitted at all. final revOk = payload.length > 2 && payload[2] == 1; - if (revOk && payload.length >= 63) { + // Status-gated like the clock read above: a failure reply leaves the body + // unpopulated, so a stale revision byte and stale-but-plausible page/ + // backlog numbers from a prior successful read would otherwise be + // reported as current. Same shared-opcode shape as getClock/getClockGen5 + // (this opcode is used by both profiles — see commands.dart), so gen4 + // is deliberately left ungated for the same unconfirmed-status-byte + // reason documented there. + final statusOk = !profile.isGen5 || status == 1; + if (statusOk && revOk && payload.length >= 63) { final oldest = u32(payload, 35); final newest = u32(payload, 59); if (_plausibleUnix(oldest) && _plausibleUnix(newest) && oldest <= newest) { @@ -965,7 +973,7 @@ CmdResponse? parseCommandResponse(Uint8List inner, // strap's own view of its ring buffer, which is what separates a stalled // offload from an idle one. Degrades safely: implausible values emit // nothing at all. - if (revOk && payload.length >= 35) { + if (statusOk && revOk && payload.length >= 35) { final writePage = u32(payload, 11); final capacity = u32(payload, 23); // TotalPages if (capacity > 0 && writePage <= capacity) { diff --git a/test/control_plane_offsets_test.dart b/test/control_plane_offsets_test.dart index d1d5dda..dd1ed13 100644 --- a/test/control_plane_offsets_test.dart +++ b/test/control_plane_offsets_test.dart @@ -180,6 +180,43 @@ void main() { dataRangeBody(writePage: 200000, totalPages: 131072)))!; expect(r.decoded.containsKey('pages_behind'), isFalse); }); + + test('gen5: a non-success outer status emits neither range nor backlog', + () { + // A failure reply's body bytes are stale leftovers from a prior + // successful read; the revision byte alone (payload[2]==1) is not + // enough to trust them. + for (final status in [0, 2, 3]) { + final r = parseCommandResponse( + cmdResponse(Cmd.getDataRange, dataRangeBody(), status: status), + profile: BandProfile.gen5)!; + expect(r.decoded.containsKey('range_oldest'), isFalse, + reason: 'status=$status'); + expect(r.decoded.containsKey('range_newest'), isFalse, + reason: 'status=$status'); + expect(r.decoded.containsKey('pages_behind'), isFalse, + reason: 'status=$status'); + } + }); + + test('gen5: a success outer status still decodes normally', () { + final r = parseCommandResponse( + cmdResponse(Cmd.getDataRange, dataRangeBody(), status: 1), + profile: BandProfile.gen5)!; + expect(r.decoded['range_oldest'], 1780000000); + expect(r.decoded['range_newest'], 1786000000); + expect((r.decoded['pages_behind'] as Map)['written'], 30); + }); + + test('gen4 is left ungated on outer status, unlike gen5', () { + // Matches getClock's gen4 policy a few groups up: the status byte's + // semantics are unconfirmed on gen4, so gating it would silently drop + // range/backlog emission rather than risk a stale read. + final r = parseCommandResponse( + cmdResponse(Cmd.getDataRange, dataRangeBody(), status: 0))!; + expect(r.decoded['range_oldest'], 1780000000); + expect(r.decoded['range_newest'], 1786000000); + }); }); // The alarm/haptics status byte: the SET and RUN