Просмотр исходного кода

fix: guard leak, presence_sensor, contact parsers against short mfr_data (#495)

Bluetooth Devices Bot 15 часов назад
Родитель
Сommit
226b170b70

+ 8 - 3
switchbot/adv_parsers/contact.py

@@ -10,10 +10,15 @@ def process_wocontact(
     if data is None and mfr_data is None:
         return {}
 
-    battery = data[2] & 0b01111111 if data else None
-    tested = bool(data[1] & 0b10000000) if data else None
+    has_full_mfr = mfr_data is not None and len(mfr_data) >= 13
+    has_full_data = data is not None and len(data) >= 9
+    if not has_full_mfr and not has_full_data:
+        return {}
+
+    battery = data[2] & 0b01111111 if data and len(data) >= 3 else None
+    tested = bool(data[1] & 0b10000000) if data and len(data) >= 2 else None
 
-    if mfr_data and len(mfr_data) >= 13:
+    if has_full_mfr:
         motion_detected = bool(mfr_data[7] & 0b10000000)
         contact_open = bool(mfr_data[7] & 0b00010000)
         contact_timeout = bool(mfr_data[7] & 0b00100000)

+ 1 - 1
switchbot/adv_parsers/leak.py

@@ -3,7 +3,7 @@
 
 def process_leak(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool | int]:
     """Process SwitchBot Water Leak Detector advertisement data."""
-    if data is None or len(data) < 3 or mfr_data is None or len(mfr_data) < 2:
+    if data is None or len(data) < 3 or mfr_data is None or len(mfr_data) < 9:
         return {}
 
     water_leak_detected = None

+ 1 - 1
switchbot/adv_parsers/presence_sensor.py

@@ -13,7 +13,7 @@ def process_presence_sensor(
     data: bytes | None, mfr_data: bytes | None
 ) -> dict[str, bool | int | str]:
     """Process Presence Sensor data."""
-    if mfr_data is None:
+    if mfr_data is None or len(mfr_data) < 12:
         return {}
 
     seq_number = mfr_data[6]

+ 76 - 0
tests/test_short_mfr_guards.py

@@ -0,0 +1,76 @@
+"""Regression tests: parsers must not raise IndexError on short mfr_data (#494)."""
+
+from __future__ import annotations
+
+import pytest
+
+from switchbot.adv_parsers.contact import process_wocontact
+from switchbot.adv_parsers.leak import process_leak
+from switchbot.adv_parsers.presence_sensor import process_presence_sensor
+
+
+@pytest.mark.parametrize(
+    "mfr_data",
+    [b"", b"\x00", b"\x00\x01", b"\x00" * 8],
+)
+def test_process_leak_short_mfr_returns_empty(mfr_data: bytes) -> None:
+    """Leak parser must not crash when mfr_data is shorter than 9 bytes."""
+    data = b"&\x00N"
+    assert process_leak(data, mfr_data) == {}
+
+
+@pytest.mark.parametrize(
+    "mfr_data",
+    [b"", b"\x00" * 6, b"\x00" * 11],
+)
+def test_process_presence_sensor_short_mfr_returns_empty(mfr_data: bytes) -> None:
+    """Presence sensor parser must not crash when mfr_data is shorter than 12 bytes."""
+    assert process_presence_sensor(None, mfr_data) == {}
+
+
+def test_process_presence_sensor_none_mfr_returns_empty() -> None:
+    assert process_presence_sensor(None, None) == {}
+
+
+@pytest.mark.parametrize(
+    ("data", "mfr_data"),
+    [
+        (None, b""),
+        (None, b"\x00" * 12),
+        (b"", None),
+        (b"\x00" * 8, None),
+        (b"\x00", b"\x00" * 12),
+        (b"\x00" * 8, b"\x00" * 12),
+    ],
+)
+def test_process_wocontact_short_payloads_return_empty(
+    data: bytes | None, mfr_data: bytes | None
+) -> None:
+    """Contact parser must not crash when neither payload is long enough."""
+    assert process_wocontact(data, mfr_data) == {}
+
+
+def test_process_wocontact_uses_mfr_when_data_short() -> None:
+    """Long mfr_data lets the parser succeed even with short service data."""
+    # mfr_data byte 7 = 0xF0 -> motion+light+contact_open+contact_timeout all True
+    mfr_data = b"\x00\x00\x00\x00\x00\x00\x00\xf0\x00\x00\x00\x00\x02"
+    result = process_wocontact(None, mfr_data)
+    assert result["motion_detected"] is True
+    assert result["contact_open"] is True
+    assert result["contact_timeout"] is True
+    assert result["is_light"] is True
+    assert result["button_count"] == 2
+    assert result["battery"] is None
+    assert result["tested"] is None
+
+
+def test_process_wocontact_uses_data_when_mfr_missing() -> None:
+    """Long service data lets the parser succeed without mfr_data."""
+    data = b"d@d\x05\x00u\x00\xf8\x12"
+    result = process_wocontact(data, None)
+    assert result["battery"] == 100
+    assert result["motion_detected"] is True
+    assert result["contact_open"] is True
+    assert result["contact_timeout"] is True
+    assert result["is_light"] is True
+    assert result["button_count"] == 2