Skip to content

fix: guard device get_basic_info parsers against short responses#500

Draft
bluetoothbot wants to merge 4 commits into
sblibs:masterfrom
bluetoothbot:koan/device-basic-info-guards
Draft

fix: guard device get_basic_info parsers against short responses#500
bluetoothbot wants to merge 4 commits into
sblibs:masterfrom
bluetoothbot:koan/device-basic-info-guards

Conversation

@bluetoothbot
Copy link
Copy Markdown
Collaborator

@bluetoothbot bluetoothbot commented May 17, 2026

What

Adds length guards to every device-class get_basic_info() (and a few related response parsers) so a truncated BLE reply returns None instead of raising IndexError.

Why

The base _get_basic_info() only filters single-byte error replies (b\"\x07\", b\"\x00\"). A 2-byte non-matching reply (e.g. b\"\x02\x00\" from firmware error states, or a payload truncated by a flaky BLE proxy) slips through and crashes the device-level parser when it indexes past the buffer end (e.g. bot.get_basic_info reads _data[10]).

This is the command-response counterpart to the adv_parsers audit (#494 / #495 / #496) — same class of bug, different layer.

How

One-liner length guards in each device's get_basic_info, sized to max_index + 1:

device max index min len
bot _data[10] 11
bulb _data[10] 11 (+ version_info ≥ 3)
ceiling_light _data[3:5] 5 (+ version_info ≥ 3)
fan _data[9] 10 (+ _data1 ≥ 3)
keypad_vision get_basic_info _data[14] 15
keypad_vision get_password_count _data[5/7] 6 (Vision) / 8 (Pro)
air_purifier _data[15] 16 (+ led_settings ≥ 6, led_status ≥ 2)
blind_tilt _data[7] 8
curtain _data[7] 8
evaporative_humidifier _data[10] 11
light_strip _data[10] 11
candle_warmer_lamp _data[2] 3
roller_shade _data[6] 7
smart_thermostat_radiator _data[14] 15
vacuum _data[2] 3
art_frame _data[6] + dynamic image-count guard 7+

Also tightens three short-circuits that used the same brittle (b\"\x07\", b\"\x00\") membership test:

  • fan._get_basic_infolen <= 1
  • blind_tilt.get_extended_info_summarylen < 2
  • curtain.get_extended_info_summarylen < 3

Testing

  • 46 new tests in tests/test_device_basic_info_guards.py covering every patched function with short payloads.
  • Full suite: 1259 passed.
  • tests/test_art_frame.py next/prev/set_image tests updated to mock _get_current_image_index directly — they previously relied on a bare AsyncMock slipping past the length-blind parser (same trap as PR fix: guard relay_switch parsers against short payloads (#369) #492).

🤖 Generated with Claude Code


Quality Report

Changes: 17 files changed, 383 insertions(+), 5 deletions(-)

Code scan: clean

Tests: passed (1264 passed)

Branch hygiene: clean

Generated by Kōan post-mission quality pipeline

bluetoothbot and others added 2 commits May 17, 2026 19:11
Each device's get_basic_info() reads fixed byte offsets in the command
response. The base _get_basic_info() only filters single-byte error
replies (b"\x07", b"\x00"); a truncated payload from a flaky BLE proxy
or device firmware error slips through and raises IndexError when the
device-level parser indexes past the buffer end.

Mirrors the adv_parsers audit pattern (sblibs#494/sblibs#495/sblibs#496) at the
command-response layer:
  bot           _data[10]  → len >= 11
  bulb          _data[10]  → len >= 11, version_info >= 3
  ceiling_light _data[3:5] → len >= 5,  version_info >= 3
  fan           _data[9]   → len >= 10, version_info >= 3
                            + fan._get_basic_info: tighten filter
                              from {b"\x07", b"\x00"} to len <= 1
  keypad_vision _data[14]  → len >= 15
                _data[5/7] → password_count >= 6 (Vision) / >= 8 (Pro)
  air_purifier  _data[15]  → len >= 16, led_settings >= 6, led_status >= 2
  blind_tilt    _data[7]   → len >= 8
                + get_extended_info_summary: tighten filter
  curtain       _data[7]   → len >= 8
                + get_extended_info_summary: tighten filter
  evap_humid.   _data[10]  → len >= 11
  light_strip   _data[10]  → len >= 11, version_info >= 3
  candle_lamp   _data[2]   → len >= 3,  version_info >= 3
  roller_shade  _data[6]   → len >= 7
  smart_therm.  _data[14]  → len >= 15
  vacuum        _data[2]   → len >= 3
  art_frame     _data[6]   → len >= 7 + dynamic guard against
                              total_num_of_images overrunning buffer

Adds 46 regression tests in test_device_basic_info_guards.py. Also
patches test_art_frame next/prev/set_image tests to mock
_get_current_image_index directly — they previously relied on a bare
AsyncMock slipping past the old length-blind parsers (per the same
trap noted in PR sblibs#492).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
switchbot/devices/air_purifier.py 100.00% <100.00%> (ø)
switchbot/devices/art_frame.py 100.00% <100.00%> (ø)
switchbot/devices/base_cover.py 100.00% <100.00%> (ø)
switchbot/devices/blind_tilt.py 98.68% <100.00%> (+0.03%) ⬆️
switchbot/devices/bot.py 50.74% <100.00%> (+6.13%) ⬆️
switchbot/devices/bulb.py 100.00% <100.00%> (ø)
switchbot/devices/ceiling_light.py 100.00% <100.00%> (ø)
switchbot/devices/curtain.py 98.71% <100.00%> (+0.03%) ⬆️
switchbot/devices/evaporative_humidifier.py 100.00% <100.00%> (ø)
switchbot/devices/fan.py 100.00% <100.00%> (ø)
... and 5 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

bluetoothbot and others added 2 commits May 18, 2026 03:55
Same pattern as the rest of this PR. SwitchbotBaseCover.get_extended_info_adv
indexes _data[3] for device0 and _data[6] for the chained-device block, but
only filters single-byte error replies (b"\x07", b"\x00"). A 2-3 byte
truncated reply (BLE proxy strip, firmware error) IndexErrors on _data[3];
a 5-6 byte reply with _data[4] set IndexErrors on _data[5]/_data[6].

Tightens the filter to len < 4 (covers any unparseable device0 payload)
and gates the device1 block on len >= 7 so a truncated chain reply parses
device0 and skips device1 instead of crashing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant