Skip to content

Commit b79548a

Browse files
committed
fix(gap): allow empty service and manufacturer data payloads
1 parent c65b433 commit b79548a

2 files changed

Lines changed: 65 additions & 9 deletions

File tree

src/bluetooth_data_tools/gap.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,8 @@ def _uncached_parse_advertisement_bytes(
214214
local_name = gap_data[start:end].decode("utf-8", "replace")
215215
elif gap_type_num == TYPE_MANUFACTURER_SPECIFIC_DATA:
216216
splice_pos = start + 2
217-
if splice_pos >= total_length or splice_pos >= end:
218-
break
217+
if splice_pos > total_length or splice_pos > end:
218+
continue
219219
if manufacturer_data is _EMPTY_MANUFACTURER_DATA:
220220
manufacturer_data = {}
221221
manufacturer_data[gap_data[start] | (gap_data[start + 1] << 8)] = gap_data[
@@ -254,26 +254,26 @@ def _uncached_parse_advertisement_bytes(
254254
service_uuids.append(_cached_uint128_bytes_as_uuid(gap_data[start:end]))
255255
elif gap_type_num == TYPE_SERVICE_DATA:
256256
splice_pos = start + 2
257-
if splice_pos >= total_length or splice_pos >= end:
258-
break
257+
if splice_pos > total_length or splice_pos > end:
258+
continue
259259
if service_data is _EMPTY_SERVICE_DATA:
260260
service_data = {}
261261
service_data[_cached_uint16_bytes_as_uuid(gap_data[start:splice_pos])] = (
262262
gap_data[splice_pos:end]
263263
)
264264
elif gap_type_num == TYPE_SERVICE_DATA_32BIT_UUID:
265265
splice_pos = start + 4
266-
if splice_pos >= total_length or splice_pos >= end:
267-
break
266+
if splice_pos > total_length or splice_pos > end:
267+
continue
268268
if service_data is _EMPTY_SERVICE_DATA:
269269
service_data = {}
270270
service_data[_cached_uint32_bytes_as_uuid(gap_data[start:splice_pos])] = (
271271
gap_data[splice_pos:end]
272272
)
273273
elif gap_type_num == TYPE_SERVICE_DATA_128BIT_UUID:
274274
splice_pos = start + 16
275-
if splice_pos >= total_length or splice_pos >= end:
276-
break
275+
if splice_pos > total_length or splice_pos > end:
276+
continue
277277
if service_data is _EMPTY_SERVICE_DATA:
278278
service_data = {}
279279
service_data[_cached_uint128_bytes_as_uuid(gap_data[start:splice_pos])] = (

tests/test_gap.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,12 +630,42 @@ def test_manufacturer_data_short_by_two():
630630

631631

632632
def test_manufacturer_data_short_by_three():
633-
"""Test short manufacturer data."""
633+
"""Test manufacturer data with minimum size (company ID only, no payload).
634+
635+
This is now valid after fixing issue #179 - manufacturer data with just
636+
a company ID and empty payload is accepted, consistent with service data.
637+
"""
634638

635639
data = (b"\x03\xff\x01\x01",)
636640

637641
adv = parse_advertisement_data(data)
638642

643+
assert adv.local_name is None
644+
assert adv.service_uuids == []
645+
assert adv.service_data == {}
646+
assert adv.manufacturer_data == {257: b""}
647+
assert adv.tx_power is None
648+
649+
assert parse_advertisement_data_tuple(tuple(data)) == (
650+
None,
651+
[],
652+
{},
653+
{257: b""},
654+
None,
655+
)
656+
657+
658+
def test_manufacturer_data_short_by_four():
659+
"""Test short manufacturer data.
660+
661+
Manufacturer data claims length of 4 but only has 3 bytes available
662+
(type + company ID, no payload bytes when 1 payload byte is claimed).
663+
"""
664+
665+
data = (b"\x04\xff\x01\x01",)
666+
667+
adv = parse_advertisement_data(data)
668+
639669
assert adv.local_name is None
640670
assert adv.service_uuids == []
641671
assert adv.service_data == {}
@@ -917,3 +947,29 @@ def test_negative_splice_pos_does_not_crash(data: tuple[bytes, bytes, bytes]) ->
917947
{},
918948
None,
919949
)
950+
951+
952+
def test_parse_advertisement_with_empty_service_data():
953+
"""Test parsing advertisement with empty service data payload (issue #179).
954+
955+
This tests the case where service data contains only a UUID with no payload bytes.
956+
The parser should accept this as valid empty service data and continue parsing
957+
subsequent advertisement structures.
958+
"""
959+
# Data from issue #179:
960+
# 02 01 06 - Flags (type 0x01, data 0x06)
961+
# 03 16 0a 18 - Service Data (type 0x16, UUID 0x180a, empty payload)
962+
# 03 03 fa ff - Complete 16-bit Service UUIDs (type 0x03, UUID 0xfffa)
963+
# 07 ff 00 01 50 90 40 a2 - Manufacturer Data (type 0xff, company 0x0100, data)
964+
# 10 09 4b 54... - Complete Local Name (type 0x09, "KT12200-B-00100")
965+
data = bytes.fromhex(
966+
"02010603160a180303faff07ff0001509040a210094b5431323230302d422d3030313030"
967+
)
968+
969+
adv = parse_advertisement_data((data,))
970+
971+
assert adv.local_name == "KT12200-B-00100"
972+
assert adv.service_uuids == ["0000fffa-0000-1000-8000-00805f9b34fb"]
973+
assert adv.service_data == {"0000180a-0000-1000-8000-00805f9b34fb": b""}
974+
assert adv.manufacturer_data == {256: b"\x50\x90\x40\xa2"}
975+
assert adv.tx_power is None

0 commit comments

Comments
 (0)