Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/bluetooth_data_tools/gap.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ def _uncached_parse_advertisement_bytes(
local_name = gap_data[start:end].decode("utf-8", "replace")
elif gap_type_num == TYPE_MANUFACTURER_SPECIFIC_DATA:
splice_pos = start + 2
if splice_pos >= total_length or splice_pos >= end:
break
if splice_pos > total_length or splice_pos > end:
continue
if manufacturer_data is _EMPTY_MANUFACTURER_DATA:
manufacturer_data = {}
manufacturer_data[gap_data[start] | (gap_data[start + 1] << 8)] = gap_data[
Expand Down Expand Up @@ -254,26 +254,26 @@ def _uncached_parse_advertisement_bytes(
service_uuids.append(_cached_uint128_bytes_as_uuid(gap_data[start:end]))
elif gap_type_num == TYPE_SERVICE_DATA:
splice_pos = start + 2
if splice_pos >= total_length or splice_pos >= end:
break
if splice_pos > total_length or splice_pos > end:
continue
if service_data is _EMPTY_SERVICE_DATA:
service_data = {}
service_data[_cached_uint16_bytes_as_uuid(gap_data[start:splice_pos])] = (
gap_data[splice_pos:end]
)
elif gap_type_num == TYPE_SERVICE_DATA_32BIT_UUID:
splice_pos = start + 4
if splice_pos >= total_length or splice_pos >= end:
break
if splice_pos > total_length or splice_pos > end:
continue
if service_data is _EMPTY_SERVICE_DATA:
service_data = {}
service_data[_cached_uint32_bytes_as_uuid(gap_data[start:splice_pos])] = (
gap_data[splice_pos:end]
)
elif gap_type_num == TYPE_SERVICE_DATA_128BIT_UUID:
splice_pos = start + 16
if splice_pos >= total_length or splice_pos >= end:
break
if splice_pos > total_length or splice_pos > end:
continue
if service_data is _EMPTY_SERVICE_DATA:
service_data = {}
service_data[_cached_uint128_bytes_as_uuid(gap_data[start:splice_pos])] = (
Expand Down
58 changes: 57 additions & 1 deletion tests/test_gap.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,12 +630,42 @@ def test_manufacturer_data_short_by_two():


def test_manufacturer_data_short_by_three():
"""Test short manufacturer data."""
"""Test manufacturer data with minimum size (company ID only, no payload).

This is now valid after fixing issue #179 - manufacturer data with just
a company ID and empty payload is accepted, consistent with service data.
"""

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

adv = parse_advertisement_data(data)

assert adv.local_name is None
assert adv.service_uuids == []
assert adv.service_data == {}
assert adv.manufacturer_data == {257: b""}
assert adv.tx_power is None

assert parse_advertisement_data_tuple(tuple(data)) == (
None,
[],
{},
{257: b""},
None,
)


def test_manufacturer_data_short_by_four():
"""Test short manufacturer data.

Manufacturer data claims length of 4 but only has 3 bytes available
(type + company ID, no payload bytes when 1 payload byte is claimed).
"""

data = (b"\x04\xff\x01\x01",)

adv = parse_advertisement_data(data)

assert adv.local_name is None
assert adv.service_uuids == []
assert adv.service_data == {}
Expand Down Expand Up @@ -917,3 +947,29 @@ def test_negative_splice_pos_does_not_crash(data: tuple[bytes, bytes, bytes]) ->
{},
None,
)


def test_parse_advertisement_with_empty_service_data():
"""Test parsing advertisement with empty service data payload (issue #179).

This tests the case where service data contains only a UUID with no payload bytes.
The parser should accept this as valid empty service data and continue parsing
subsequent advertisement structures.
"""
# Data from issue #179:
# 02 01 06 - Flags (type 0x01, data 0x06)
# 03 16 0a 18 - Service Data (type 0x16, UUID 0x180a, empty payload)
# 03 03 fa ff - Complete 16-bit Service UUIDs (type 0x03, UUID 0xfffa)
# 07 ff 00 01 50 90 40 a2 - Manufacturer Data (type 0xff, company 0x0100, data)
# 10 09 4b 54... - Complete Local Name (type 0x09, "KT12200-B-00100")
data = bytes.fromhex(
"02010603160a180303faff07ff0001509040a210094b5431323230302d422d3030313030"
)

adv = parse_advertisement_data((data,))

assert adv.local_name == "KT12200-B-00100"
assert adv.service_uuids == ["0000fffa-0000-1000-8000-00805f9b34fb"]
assert adv.service_data == {"0000180a-0000-1000-8000-00805f9b34fb": b""}
assert adv.manufacturer_data == {256: b"\x50\x90\x40\xa2"}
assert adv.tx_power is None
Loading