fix: BATTERY handler drops frames shorter than 3 bytes (level field guard)

A BATTERY frame with len(data) < 3 caused dbuf.read(2) to return short
bytes; int.from_bytes(b"", ...) silently yielded 0, propagating a bogus
level=0 to HA sensors.  Same silent-zero class as N07 (storage fields).

Option B: early-return with debug log, matching the NEW-C pattern for
STATUS_RESPONSE.  No BATTERY event is dispatched for malformed frames.

Not in the original forensics report — discovered during G1 N07 work and
logged in issues_log.md.  Resolved here because no later branch touches
this handler.

Files changed:
- src/meshcore/reader.py: add `if len(data) < 3: return` guard before
  the level read in the BATTERY branch
- tests/unit/test_reader.py: add test_g1_battery_too_short_for_level —
  sends a 1-byte frame (type only), asserts no BATTERY event dispatched
  and debug log emitted
This commit is contained in:
Matthew Wolter 2026-04-11 19:24:26 -07:00
parent f3973151d6
commit 2bf3f1b9dd
2 changed files with 39 additions and 4 deletions

View file

@ -341,12 +341,20 @@ class MessageReader:
await self.dispatcher.dispatch(Event(EventType.CONTACT_URI, result))
elif packet_type_value == PacketType.BATTERY.value:
# Full RESP_CODE_BATT_AND_STORAGE: 1 type + 2 level + 4 used_kb + 4 total_kb = 11 bytes.
# Minimum viable frame is 3 bytes (type + level). Shorter frames are
# malformed — dbuf.read(2) would return short bytes and
# int.from_bytes(b"", ...) silently yields 0 (same class as N07).
if len(data) < 3:
logger.debug(
"BATTERY frame too short for level field "
f"({len(data)} bytes < 3), skipping"
)
return
battery_level = int.from_bytes(dbuf.read(2), byteorder="little")
result = {"level": battery_level}
# Full RESP_CODE_BATT_AND_STORAGE frame is 11 bytes:
# 1 type + 2 level + 4 used_kb + 4 total_kb. The previous
# `len(data) > 3` guard let 4-10 byte truncated frames through,
# producing silent zero values for used_kb/total_kb because
# The previous `len(data) > 3` guard let 4-10 byte truncated frames
# through, producing silent zero values for used_kb/total_kb because
# io.BytesIO.read() returns short data without raising.
if len(data) >= 11: # has storage info as well
result["used_kb"] = int.from_bytes(dbuf.read(4), byteorder="little")