Skip to content

Commit 6bea1ea

Browse files
bluetoothbotclaudepre-commit-ci[bot]
authored
fix(gap): iterate 128-bit service UUID lists, skip malformed lengths (#226)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 47368a8 commit 6bea1ea

2 files changed

Lines changed: 54 additions & 1 deletion

File tree

src/bluetooth_data_tools/gap.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,14 @@ def _uncached_parse_advertisement_bytes(
252252
}:
253253
if service_uuids is _EMPTY_SERVICE_UUIDS:
254254
service_uuids = []
255-
service_uuids.append(_cached_uint128_bytes_as_uuid(gap_data[start:end]))
255+
# Parse multiple 128-bit UUIDs (each is 16 bytes). The AD length
256+
# may not be a clean multiple of 16 for malformed input — skip
257+
# any trailing remainder rather than emitting a truncated UUID.
258+
for i in range(start, end, 16):
259+
if i + 16 <= end:
260+
service_uuids.append(
261+
_cached_uint128_bytes_as_uuid(gap_data[i : i + 16])
262+
)
256263
elif gap_type_num == TYPE_SERVICE_DATA:
257264
splice_pos = start + 2
258265
if splice_pos > total_length or splice_pos > end:

tests/test_gap.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,3 +1001,49 @@ def test_parse_advertisement_with_empty_service_data():
10011001
assert adv.service_data == {"0000180a-0000-1000-8000-00805f9b34fb": b""}
10021002
assert adv.manufacturer_data == {256: b"\x50\x90\x40\xa2"}
10031003
assert adv.tx_power is None
1004+
1005+
1006+
def test_parse_advertisement_data_multiple_128bit_uuids():
1007+
"""Two 128-bit UUIDs packed into a single AD struct must both be returned.
1008+
1009+
Per Core Spec Vol 3 Part C §11, the Complete/Incomplete List of 128-bit
1010+
Service UUIDs AD types carry a list (length = 1 + 16N), not a single UUID.
1011+
Larger packets (scan response / extended advertising) can carry more than one.
1012+
"""
1013+
uuid1 = bytes.fromhex("00112233445566778899aabbccddeeff")
1014+
uuid2 = bytes.fromhex("0f1e2d3c4b5a69788796a5b4c3d2e1f0")
1015+
# Length = 0x21 (33 = 1 type byte + 32 UUID bytes), type = 0x07 (complete list)
1016+
data = b"\x21\x07" + uuid1 + uuid2
1017+
1018+
adv = parse_advertisement_data((data,))
1019+
1020+
assert adv.local_name is None
1021+
assert adv.service_uuids == [
1022+
# bytes are stored little-endian in BLE — reverse for canonical form
1023+
"ffeeddcc-bbaa-9988-7766-554433221100",
1024+
"f0e1d2c3-b4a5-9687-7869-5a4b3c2d1e0f",
1025+
]
1026+
assert adv.service_data == {}
1027+
assert adv.manufacturer_data == {}
1028+
assert adv.tx_power is None
1029+
1030+
1031+
def test_parse_advertisement_data_128bit_uuid_malformed_length():
1032+
"""Malformed 128-bit UUID payloads (length not 1 + 16N) must be skipped.
1033+
1034+
Previously the parser passed the truncated/excess bytes straight to the
1035+
UUID formatter, producing bogus UUID strings (all zeros for short, a
1036+
64-hex-char garbage string for double-length).
1037+
"""
1038+
# Length=0x0a (10 = 1 type + 9 bytes), type=0x07 — only 9 bytes of "UUID"
1039+
short = b"\x0a\x07" + bytes(9)
1040+
adv = parse_advertisement_data((short,))
1041+
assert adv.service_uuids == []
1042+
1043+
# Length=0x12 (18 = 1 type + 17 bytes), type=0x07 — one valid UUID plus
1044+
# a 1-byte tail that must be ignored, not folded into a second UUID.
1045+
one_and_a_half = (
1046+
b"\x12\x07" + bytes.fromhex("00112233445566778899aabbccddeeff") + b"\x42"
1047+
)
1048+
adv = parse_advertisement_data((one_and_a_half,))
1049+
assert adv.service_uuids == ["ffeeddcc-bbaa-9988-7766-554433221100"]

0 commit comments

Comments
 (0)