• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021-2022 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15# -----------------------------------------------------------------------------
16# Imports
17# -----------------------------------------------------------------------------
18from __future__ import annotations
19import logging
20import struct
21from typing import Dict, List, Type, Optional, Tuple, Union, NewType, TYPE_CHECKING
22from typing_extensions import Self
23
24from . import core, l2cap
25from .colors import color
26from .core import InvalidStateError
27from .hci import HCI_Object, name_or_number, key_with_value
28
29if TYPE_CHECKING:
30    from .device import Device, Connection
31
32# -----------------------------------------------------------------------------
33# Logging
34# -----------------------------------------------------------------------------
35logger = logging.getLogger(__name__)
36
37
38# -----------------------------------------------------------------------------
39# Constants
40# -----------------------------------------------------------------------------
41# fmt: off
42# pylint: disable=line-too-long
43
44SDP_CONTINUATION_WATCHDOG = 64  # Maximum number of continuations we're willing to do
45
46SDP_PSM = 0x0001
47
48SDP_ERROR_RESPONSE                    = 0x01
49SDP_SERVICE_SEARCH_REQUEST            = 0x02
50SDP_SERVICE_SEARCH_RESPONSE           = 0x03
51SDP_SERVICE_ATTRIBUTE_REQUEST         = 0x04
52SDP_SERVICE_ATTRIBUTE_RESPONSE        = 0x05
53SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST  = 0x06
54SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE = 0x07
55
56SDP_PDU_NAMES = {
57    SDP_ERROR_RESPONSE:                    'SDP_ERROR_RESPONSE',
58    SDP_SERVICE_SEARCH_REQUEST:            'SDP_SERVICE_SEARCH_REQUEST',
59    SDP_SERVICE_SEARCH_RESPONSE:           'SDP_SERVICE_SEARCH_RESPONSE',
60    SDP_SERVICE_ATTRIBUTE_REQUEST:         'SDP_SERVICE_ATTRIBUTE_REQUEST',
61    SDP_SERVICE_ATTRIBUTE_RESPONSE:        'SDP_SERVICE_ATTRIBUTE_RESPONSE',
62    SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST:  'SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST',
63    SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE: 'SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE'
64}
65
66SDP_INVALID_SDP_VERSION_ERROR                       = 0x0001
67SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR             = 0x0002
68SDP_INVALID_REQUEST_SYNTAX_ERROR                    = 0x0003
69SDP_INVALID_PDU_SIZE_ERROR                          = 0x0004
70SDP_INVALID_CONTINUATION_STATE_ERROR                = 0x0005
71SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR = 0x0006
72
73SDP_ERROR_NAMES = {
74    SDP_INVALID_SDP_VERSION_ERROR:                       'SDP_INVALID_SDP_VERSION_ERROR',
75    SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR:             'SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR',
76    SDP_INVALID_REQUEST_SYNTAX_ERROR:                    'SDP_INVALID_REQUEST_SYNTAX_ERROR',
77    SDP_INVALID_PDU_SIZE_ERROR:                          'SDP_INVALID_PDU_SIZE_ERROR',
78    SDP_INVALID_CONTINUATION_STATE_ERROR:                'SDP_INVALID_CONTINUATION_STATE_ERROR',
79    SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR: 'SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR'
80}
81
82SDP_SERVICE_NAME_ATTRIBUTE_ID_OFFSET        = 0x0000
83SDP_SERVICE_DESCRIPTION_ATTRIBUTE_ID_OFFSET = 0x0001
84SDP_PROVIDER_NAME_ATTRIBUTE_ID_OFFSET       = 0x0002
85
86SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID               = 0X0000
87SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID               = 0X0001
88SDP_SERVICE_RECORD_STATE_ATTRIBUTE_ID                = 0X0002
89SDP_SERVICE_ID_ATTRIBUTE_ID                          = 0X0003
90SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID            = 0X0004
91SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID                   = 0X0005
92SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID     = 0X0006
93SDP_SERVICE_INFO_TIME_TO_LIVE_ATTRIBUTE_ID           = 0X0007
94SDP_SERVICE_AVAILABILITY_ATTRIBUTE_ID                = 0X0008
95SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID   = 0X0009
96SDP_DOCUMENTATION_URL_ATTRIBUTE_ID                   = 0X000A
97SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID               = 0X000B
98SDP_ICON_URL_ATTRIBUTE_ID                            = 0X000C
99SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0X000D
100
101
102# Profile-specific Attribute Identifiers (cf. Assigned Numbers for Service Discovery)
103# used by AVRCP, HFP and A2DP
104SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID = 0x0311
105
106SDP_ATTRIBUTE_ID_NAMES = {
107    SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID:               'SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID',
108    SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID:               'SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID',
109    SDP_SERVICE_RECORD_STATE_ATTRIBUTE_ID:                'SDP_SERVICE_RECORD_STATE_ATTRIBUTE_ID',
110    SDP_SERVICE_ID_ATTRIBUTE_ID:                          'SDP_SERVICE_ID_ATTRIBUTE_ID',
111    SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID:            'SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID',
112    SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID:                   'SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID',
113    SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID:     'SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID',
114    SDP_SERVICE_INFO_TIME_TO_LIVE_ATTRIBUTE_ID:           'SDP_SERVICE_INFO_TIME_TO_LIVE_ATTRIBUTE_ID',
115    SDP_SERVICE_AVAILABILITY_ATTRIBUTE_ID:                'SDP_SERVICE_AVAILABILITY_ATTRIBUTE_ID',
116    SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID:   'SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID',
117    SDP_DOCUMENTATION_URL_ATTRIBUTE_ID:                   'SDP_DOCUMENTATION_URL_ATTRIBUTE_ID',
118    SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID:               'SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID',
119    SDP_ICON_URL_ATTRIBUTE_ID:                            'SDP_ICON_URL_ATTRIBUTE_ID',
120    SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID: 'SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID',
121    SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID:                  'SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID',
122}
123
124SDP_PUBLIC_BROWSE_ROOT = core.UUID.from_16_bits(0x1002, 'PublicBrowseRoot')
125
126# To be used in searches where an attribute ID list allows a range to be specified
127SDP_ALL_ATTRIBUTES_RANGE = (0x0000FFFF, 4)  # Express this as tuple so we can convey the desired encoding size
128
129# fmt: on
130# pylint: enable=line-too-long
131# pylint: disable=invalid-name
132
133
134# -----------------------------------------------------------------------------
135class DataElement:
136    NIL = 0
137    UNSIGNED_INTEGER = 1
138    SIGNED_INTEGER = 2
139    UUID = 3
140    TEXT_STRING = 4
141    BOOLEAN = 5
142    SEQUENCE = 6
143    ALTERNATIVE = 7
144    URL = 8
145
146    TYPE_NAMES = {
147        NIL: 'NIL',
148        UNSIGNED_INTEGER: 'UNSIGNED_INTEGER',
149        SIGNED_INTEGER: 'SIGNED_INTEGER',
150        UUID: 'UUID',
151        TEXT_STRING: 'TEXT_STRING',
152        BOOLEAN: 'BOOLEAN',
153        SEQUENCE: 'SEQUENCE',
154        ALTERNATIVE: 'ALTERNATIVE',
155        URL: 'URL',
156    }
157
158    type_constructors = {
159        NIL: lambda x: DataElement(DataElement.NIL, None),
160        UNSIGNED_INTEGER: lambda x, y: DataElement(
161            DataElement.UNSIGNED_INTEGER,
162            DataElement.unsigned_integer_from_bytes(x),
163            value_size=y,
164        ),
165        SIGNED_INTEGER: lambda x, y: DataElement(
166            DataElement.SIGNED_INTEGER,
167            DataElement.signed_integer_from_bytes(x),
168            value_size=y,
169        ),
170        UUID: lambda x: DataElement(
171            DataElement.UUID, core.UUID.from_bytes(bytes(reversed(x)))
172        ),
173        TEXT_STRING: lambda x: DataElement(DataElement.TEXT_STRING, x),
174        BOOLEAN: lambda x: DataElement(DataElement.BOOLEAN, x[0] == 1),
175        SEQUENCE: lambda x: DataElement(
176            DataElement.SEQUENCE, DataElement.list_from_bytes(x)
177        ),
178        ALTERNATIVE: lambda x: DataElement(
179            DataElement.ALTERNATIVE, DataElement.list_from_bytes(x)
180        ),
181        URL: lambda x: DataElement(DataElement.URL, x.decode('utf8')),
182    }
183
184    def __init__(self, element_type, value, value_size=None):
185        self.type = element_type
186        self.value = value
187        self.value_size = value_size
188        # Used as a cache when parsing from bytes so we can emit a byte-for-byte replica
189        self.bytes = None
190        if element_type in (DataElement.UNSIGNED_INTEGER, DataElement.SIGNED_INTEGER):
191            if value_size is None:
192                raise ValueError('integer types must have a value size specified')
193
194    @staticmethod
195    def nil() -> DataElement:
196        return DataElement(DataElement.NIL, None)
197
198    @staticmethod
199    def unsigned_integer(value: int, value_size: int) -> DataElement:
200        return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size)
201
202    @staticmethod
203    def unsigned_integer_8(value: int) -> DataElement:
204        return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=1)
205
206    @staticmethod
207    def unsigned_integer_16(value: int) -> DataElement:
208        return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=2)
209
210    @staticmethod
211    def unsigned_integer_32(value: int) -> DataElement:
212        return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=4)
213
214    @staticmethod
215    def signed_integer(value: int, value_size: int) -> DataElement:
216        return DataElement(DataElement.SIGNED_INTEGER, value, value_size)
217
218    @staticmethod
219    def signed_integer_8(value: int) -> DataElement:
220        return DataElement(DataElement.SIGNED_INTEGER, value, value_size=1)
221
222    @staticmethod
223    def signed_integer_16(value: int) -> DataElement:
224        return DataElement(DataElement.SIGNED_INTEGER, value, value_size=2)
225
226    @staticmethod
227    def signed_integer_32(value: int) -> DataElement:
228        return DataElement(DataElement.SIGNED_INTEGER, value, value_size=4)
229
230    @staticmethod
231    def uuid(value: core.UUID) -> DataElement:
232        return DataElement(DataElement.UUID, value)
233
234    @staticmethod
235    def text_string(value: bytes) -> DataElement:
236        return DataElement(DataElement.TEXT_STRING, value)
237
238    @staticmethod
239    def boolean(value: bool) -> DataElement:
240        return DataElement(DataElement.BOOLEAN, value)
241
242    @staticmethod
243    def sequence(value: List[DataElement]) -> DataElement:
244        return DataElement(DataElement.SEQUENCE, value)
245
246    @staticmethod
247    def alternative(value: List[DataElement]) -> DataElement:
248        return DataElement(DataElement.ALTERNATIVE, value)
249
250    @staticmethod
251    def url(value: str) -> DataElement:
252        return DataElement(DataElement.URL, value)
253
254    @staticmethod
255    def unsigned_integer_from_bytes(data):
256        if len(data) == 1:
257            return data[0]
258
259        if len(data) == 2:
260            return struct.unpack('>H', data)[0]
261
262        if len(data) == 4:
263            return struct.unpack('>I', data)[0]
264
265        if len(data) == 8:
266            return struct.unpack('>Q', data)[0]
267
268        raise ValueError(f'invalid integer length {len(data)}')
269
270    @staticmethod
271    def signed_integer_from_bytes(data):
272        if len(data) == 1:
273            return struct.unpack('b', data)[0]
274
275        if len(data) == 2:
276            return struct.unpack('>h', data)[0]
277
278        if len(data) == 4:
279            return struct.unpack('>i', data)[0]
280
281        if len(data) == 8:
282            return struct.unpack('>q', data)[0]
283
284        raise ValueError(f'invalid integer length {len(data)}')
285
286    @staticmethod
287    def list_from_bytes(data):
288        elements = []
289        while data:
290            element = DataElement.from_bytes(data)
291            elements.append(element)
292            data = data[len(bytes(element)) :]
293        return elements
294
295    @staticmethod
296    def parse_from_bytes(data, offset):
297        element = DataElement.from_bytes(data[offset:])
298        return offset + len(bytes(element)), element
299
300    @staticmethod
301    def from_bytes(data):
302        element_type = data[0] >> 3
303        size_index = data[0] & 7
304        value_offset = 0
305        if size_index == 0:
306            if element_type == DataElement.NIL:
307                value_size = 0
308            else:
309                value_size = 1
310        elif size_index == 1:
311            value_size = 2
312        elif size_index == 2:
313            value_size = 4
314        elif size_index == 3:
315            value_size = 8
316        elif size_index == 4:
317            value_size = 16
318        elif size_index == 5:
319            value_size = data[1]
320            value_offset = 1
321        elif size_index == 6:
322            value_size = struct.unpack('>H', data[1:3])[0]
323            value_offset = 2
324        else:  # size_index == 7
325            value_size = struct.unpack('>I', data[1:5])[0]
326            value_offset = 4
327
328        value_data = data[1 + value_offset : 1 + value_offset + value_size]
329        constructor = DataElement.type_constructors.get(element_type)
330        if constructor:
331            if element_type in (
332                DataElement.UNSIGNED_INTEGER,
333                DataElement.SIGNED_INTEGER,
334            ):
335                result = constructor(value_data, value_size)
336            else:
337                result = constructor(value_data)
338        else:
339            result = DataElement(element_type, value_data)
340        result.bytes = data[
341            : 1 + value_offset + value_size
342        ]  # Keep a copy so we can re-serialize to an exact replica
343        return result
344
345    def to_bytes(self):
346        return bytes(self)
347
348    def __bytes__(self):
349        # Return early if we have a cache
350        if self.bytes:
351            return self.bytes
352
353        if self.type == DataElement.NIL:
354            data = b''
355        elif self.type == DataElement.UNSIGNED_INTEGER:
356            if self.value < 0:
357                raise ValueError('UNSIGNED_INTEGER cannot be negative')
358
359            if self.value_size == 1:
360                data = struct.pack('B', self.value)
361            elif self.value_size == 2:
362                data = struct.pack('>H', self.value)
363            elif self.value_size == 4:
364                data = struct.pack('>I', self.value)
365            elif self.value_size == 8:
366                data = struct.pack('>Q', self.value)
367            else:
368                raise ValueError('invalid value_size')
369        elif self.type == DataElement.SIGNED_INTEGER:
370            if self.value_size == 1:
371                data = struct.pack('b', self.value)
372            elif self.value_size == 2:
373                data = struct.pack('>h', self.value)
374            elif self.value_size == 4:
375                data = struct.pack('>i', self.value)
376            elif self.value_size == 8:
377                data = struct.pack('>q', self.value)
378            else:
379                raise ValueError('invalid value_size')
380        elif self.type == DataElement.UUID:
381            data = bytes(reversed(bytes(self.value)))
382        elif self.type == DataElement.URL:
383            data = self.value.encode('utf8')
384        elif self.type == DataElement.BOOLEAN:
385            data = bytes([1 if self.value else 0])
386        elif self.type in (DataElement.SEQUENCE, DataElement.ALTERNATIVE):
387            data = b''.join([bytes(element) for element in self.value])
388        else:
389            data = self.value
390
391        size = len(data)
392        size_bytes = b''
393        if self.type == DataElement.NIL:
394            if size != 0:
395                raise ValueError('NIL must be empty')
396            size_index = 0
397        elif self.type in (
398            DataElement.UNSIGNED_INTEGER,
399            DataElement.SIGNED_INTEGER,
400            DataElement.UUID,
401        ):
402            if size <= 1:
403                size_index = 0
404            elif size == 2:
405                size_index = 1
406            elif size == 4:
407                size_index = 2
408            elif size == 8:
409                size_index = 3
410            elif size == 16:
411                size_index = 4
412            else:
413                raise ValueError('invalid data size')
414        elif self.type in (
415            DataElement.TEXT_STRING,
416            DataElement.SEQUENCE,
417            DataElement.ALTERNATIVE,
418            DataElement.URL,
419        ):
420            if size <= 0xFF:
421                size_index = 5
422                size_bytes = bytes([size])
423            elif size <= 0xFFFF:
424                size_index = 6
425                size_bytes = struct.pack('>H', size)
426            elif size <= 0xFFFFFFFF:
427                size_index = 7
428                size_bytes = struct.pack('>I', size)
429            else:
430                raise ValueError('invalid data size')
431        elif self.type == DataElement.BOOLEAN:
432            if size != 1:
433                raise ValueError('boolean must be 1 byte')
434            size_index = 0
435
436        self.bytes = bytes([self.type << 3 | size_index]) + size_bytes + data
437        return self.bytes
438
439    def to_string(self, pretty=False, indentation=0):
440        prefix = '  ' * indentation
441        type_name = name_or_number(self.TYPE_NAMES, self.type)
442        if self.type == DataElement.NIL:
443            value_string = ''
444        elif self.type in (DataElement.SEQUENCE, DataElement.ALTERNATIVE):
445            container_separator = '\n' if pretty else ''
446            element_separator = '\n' if pretty else ','
447            elements = [
448                element.to_string(pretty, indentation + 1 if pretty else 0)
449                for element in self.value
450            ]
451            value_string = (
452                f'[{container_separator}'
453                f'{element_separator.join(elements)}'
454                f'{container_separator}{prefix}]'
455            )
456        elif self.type in (DataElement.UNSIGNED_INTEGER, DataElement.SIGNED_INTEGER):
457            value_string = f'{self.value}#{self.value_size}'
458        elif isinstance(self.value, DataElement):
459            value_string = self.value.to_string(pretty, indentation)
460        else:
461            value_string = str(self.value)
462        return f'{prefix}{type_name}({value_string})'
463
464    def __str__(self):
465        return self.to_string()
466
467
468# -----------------------------------------------------------------------------
469class ServiceAttribute:
470    def __init__(self, attribute_id: int, value: DataElement) -> None:
471        self.id = attribute_id
472        self.value = value
473
474    @staticmethod
475    def list_from_data_elements(elements: List[DataElement]) -> List[ServiceAttribute]:
476        attribute_list = []
477        for i in range(0, len(elements) // 2):
478            attribute_id, attribute_value = elements[2 * i : 2 * (i + 1)]
479            if attribute_id.type != DataElement.UNSIGNED_INTEGER:
480                logger.warning('attribute ID element is not an integer')
481                continue
482            attribute_list.append(ServiceAttribute(attribute_id.value, attribute_value))
483
484        return attribute_list
485
486    @staticmethod
487    def find_attribute_in_list(
488        attribute_list: List[ServiceAttribute], attribute_id: int
489    ) -> Optional[DataElement]:
490        return next(
491            (
492                attribute.value
493                for attribute in attribute_list
494                if attribute.id == attribute_id
495            ),
496            None,
497        )
498
499    @staticmethod
500    def id_name(id_code):
501        return name_or_number(SDP_ATTRIBUTE_ID_NAMES, id_code)
502
503    @staticmethod
504    def is_uuid_in_value(uuid: core.UUID, value: DataElement) -> bool:
505        # Find if a uuid matches a value, either directly or recursing into sequences
506        if value.type == DataElement.UUID:
507            return value.value == uuid
508
509        if value.type == DataElement.SEQUENCE:
510            for element in value.value:
511                if ServiceAttribute.is_uuid_in_value(uuid, element):
512                    return True
513            return False
514
515        return False
516
517    def to_string(self, with_colors=False):
518        if with_colors:
519            return (
520                f'Attribute(id={color(self.id_name(self.id),"magenta")},'
521                f'value={self.value})'
522            )
523
524        return f'Attribute(id={self.id_name(self.id)},value={self.value})'
525
526    def __str__(self):
527        return self.to_string()
528
529
530# -----------------------------------------------------------------------------
531class SDP_PDU:
532    '''
533    See Bluetooth spec @ Vol 3, Part B - 4.2 PROTOCOL DATA UNIT FORMAT
534    '''
535
536    sdp_pdu_classes: Dict[int, Type[SDP_PDU]] = {}
537    name = None
538    pdu_id = 0
539
540    @staticmethod
541    def from_bytes(pdu):
542        pdu_id, transaction_id, _parameters_length = struct.unpack_from('>BHH', pdu, 0)
543
544        cls = SDP_PDU.sdp_pdu_classes.get(pdu_id)
545        if cls is None:
546            instance = SDP_PDU(pdu)
547            instance.name = SDP_PDU.pdu_name(pdu_id)
548            instance.pdu_id = pdu_id
549            instance.transaction_id = transaction_id
550            return instance
551        self = cls.__new__(cls)
552        SDP_PDU.__init__(self, pdu, transaction_id)
553        if hasattr(self, 'fields'):
554            self.init_from_bytes(pdu, 5)
555        return self
556
557    @staticmethod
558    def parse_service_record_handle_list_preceded_by_count(
559        data: bytes, offset: int
560    ) -> Tuple[int, List[int]]:
561        count = struct.unpack_from('>H', data, offset - 2)[0]
562        handle_list = [
563            struct.unpack_from('>I', data, offset + x * 4)[0] for x in range(count)
564        ]
565        return offset + count * 4, handle_list
566
567    @staticmethod
568    def parse_bytes_preceded_by_length(data, offset):
569        length = struct.unpack_from('>H', data, offset - 2)[0]
570        return offset + length, data[offset : offset + length]
571
572    @staticmethod
573    def error_name(error_code):
574        return name_or_number(SDP_ERROR_NAMES, error_code)
575
576    @staticmethod
577    def pdu_name(code):
578        return name_or_number(SDP_PDU_NAMES, code)
579
580    @staticmethod
581    def subclass(fields):
582        def inner(cls):
583            name = cls.__name__
584
585            # add a _ character before every uppercase letter, except the SDP_ prefix
586            location = len(name) - 1
587            while location > 4:
588                if not name[location].isupper():
589                    location -= 1
590                    continue
591                name = name[:location] + '_' + name[location:]
592                location -= 1
593
594            cls.name = name.upper()
595            cls.pdu_id = key_with_value(SDP_PDU_NAMES, cls.name)
596            if cls.pdu_id is None:
597                raise KeyError(f'PDU name {cls.name} not found in SDP_PDU_NAMES')
598            cls.fields = fields
599
600            # Register a factory for this class
601            SDP_PDU.sdp_pdu_classes[cls.pdu_id] = cls
602
603            return cls
604
605        return inner
606
607    def __init__(self, pdu=None, transaction_id=0, **kwargs):
608        if hasattr(self, 'fields') and kwargs:
609            HCI_Object.init_from_fields(self, self.fields, kwargs)
610        if pdu is None:
611            parameters = HCI_Object.dict_to_bytes(kwargs, self.fields)
612            pdu = (
613                struct.pack('>BHH', self.pdu_id, transaction_id, len(parameters))
614                + parameters
615            )
616        self.pdu = pdu
617        self.transaction_id = transaction_id
618
619    def init_from_bytes(self, pdu, offset):
620        return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
621
622    def to_bytes(self):
623        return self.pdu
624
625    def __bytes__(self):
626        return self.to_bytes()
627
628    def __str__(self):
629        result = f'{color(self.name, "blue")} [TID={self.transaction_id}]'
630        if fields := getattr(self, 'fields', None):
631            result += ':\n' + HCI_Object.format_fields(self.__dict__, fields, '  ')
632        elif len(self.pdu) > 1:
633            result += f': {self.pdu.hex()}'
634        return result
635
636
637# -----------------------------------------------------------------------------
638@SDP_PDU.subclass([('error_code', {'size': 2, 'mapper': SDP_PDU.error_name})])
639class SDP_ErrorResponse(SDP_PDU):
640    '''
641    See Bluetooth spec @ Vol 3, Part B - 4.4.1 SDP_ErrorResponse PDU
642    '''
643
644
645# -----------------------------------------------------------------------------
646@SDP_PDU.subclass(
647    [
648        ('service_search_pattern', DataElement.parse_from_bytes),
649        ('maximum_service_record_count', '>2'),
650        ('continuation_state', '*'),
651    ]
652)
653class SDP_ServiceSearchRequest(SDP_PDU):
654    '''
655    See Bluetooth spec @ Vol 3, Part B - 4.5.1 SDP_ServiceSearchRequest PDU
656    '''
657
658    service_search_pattern: DataElement
659    maximum_service_record_count: int
660    continuation_state: bytes
661
662
663# -----------------------------------------------------------------------------
664@SDP_PDU.subclass(
665    [
666        ('total_service_record_count', '>2'),
667        ('current_service_record_count', '>2'),
668        (
669            'service_record_handle_list',
670            SDP_PDU.parse_service_record_handle_list_preceded_by_count,
671        ),
672        ('continuation_state', '*'),
673    ]
674)
675class SDP_ServiceSearchResponse(SDP_PDU):
676    '''
677    See Bluetooth spec @ Vol 3, Part B - 4.5.2 SDP_ServiceSearchResponse PDU
678    '''
679
680    service_record_handle_list: List[int]
681    total_service_record_count: int
682    current_service_record_count: int
683    continuation_state: bytes
684
685
686# -----------------------------------------------------------------------------
687@SDP_PDU.subclass(
688    [
689        ('service_record_handle', '>4'),
690        ('maximum_attribute_byte_count', '>2'),
691        ('attribute_id_list', DataElement.parse_from_bytes),
692        ('continuation_state', '*'),
693    ]
694)
695class SDP_ServiceAttributeRequest(SDP_PDU):
696    '''
697    See Bluetooth spec @ Vol 3, Part B - 4.6.1 SDP_ServiceAttributeRequest PDU
698    '''
699
700    service_record_handle: int
701    maximum_attribute_byte_count: int
702    attribute_id_list: DataElement
703    continuation_state: bytes
704
705
706# -----------------------------------------------------------------------------
707@SDP_PDU.subclass(
708    [
709        ('attribute_list_byte_count', '>2'),
710        ('attribute_list', SDP_PDU.parse_bytes_preceded_by_length),
711        ('continuation_state', '*'),
712    ]
713)
714class SDP_ServiceAttributeResponse(SDP_PDU):
715    '''
716    See Bluetooth spec @ Vol 3, Part B - 4.6.2 SDP_ServiceAttributeResponse PDU
717    '''
718
719    attribute_list_byte_count: int
720    attribute_list: bytes
721    continuation_state: bytes
722
723
724# -----------------------------------------------------------------------------
725@SDP_PDU.subclass(
726    [
727        ('service_search_pattern', DataElement.parse_from_bytes),
728        ('maximum_attribute_byte_count', '>2'),
729        ('attribute_id_list', DataElement.parse_from_bytes),
730        ('continuation_state', '*'),
731    ]
732)
733class SDP_ServiceSearchAttributeRequest(SDP_PDU):
734    '''
735    See Bluetooth spec @ Vol 3, Part B - 4.7.1 SDP_ServiceSearchAttributeRequest PDU
736    '''
737
738    service_search_pattern: DataElement
739    maximum_attribute_byte_count: int
740    attribute_id_list: DataElement
741    continuation_state: bytes
742
743
744# -----------------------------------------------------------------------------
745@SDP_PDU.subclass(
746    [
747        ('attribute_lists_byte_count', '>2'),
748        ('attribute_lists', SDP_PDU.parse_bytes_preceded_by_length),
749        ('continuation_state', '*'),
750    ]
751)
752class SDP_ServiceSearchAttributeResponse(SDP_PDU):
753    '''
754    See Bluetooth spec @ Vol 3, Part B - 4.7.2 SDP_ServiceSearchAttributeResponse PDU
755    '''
756
757    attribute_list_byte_count: int
758    attribute_list: bytes
759    continuation_state: bytes
760
761
762# -----------------------------------------------------------------------------
763class Client:
764    channel: Optional[l2cap.ClassicChannel]
765
766    def __init__(self, connection: Connection) -> None:
767        self.connection = connection
768        self.pending_request = None
769        self.channel = None
770
771    async def connect(self) -> None:
772        self.channel = await self.connection.create_l2cap_channel(
773            spec=l2cap.ClassicChannelSpec(SDP_PSM)
774        )
775
776    async def disconnect(self) -> None:
777        if self.channel:
778            await self.channel.disconnect()
779            self.channel = None
780
781    async def search_services(self, uuids: List[core.UUID]) -> List[int]:
782        if self.pending_request is not None:
783            raise InvalidStateError('request already pending')
784        if self.channel is None:
785            raise InvalidStateError('L2CAP not connected')
786
787        service_search_pattern = DataElement.sequence(
788            [DataElement.uuid(uuid) for uuid in uuids]
789        )
790
791        # Request and accumulate until there's no more continuation
792        service_record_handle_list = []
793        continuation_state = bytes([0])
794        watchdog = SDP_CONTINUATION_WATCHDOG
795        while watchdog > 0:
796            response_pdu = await self.channel.send_request(
797                SDP_ServiceSearchRequest(
798                    transaction_id=0,  # Transaction ID TODO: pick a real value
799                    service_search_pattern=service_search_pattern,
800                    maximum_service_record_count=0xFFFF,
801                    continuation_state=continuation_state,
802                )
803            )
804            response = SDP_PDU.from_bytes(response_pdu)
805            logger.debug(f'<<< Response: {response}')
806            service_record_handle_list += response.service_record_handle_list
807            continuation_state = response.continuation_state
808            if len(continuation_state) == 1 and continuation_state[0] == 0:
809                break
810            logger.debug(f'continuation: {continuation_state.hex()}')
811            watchdog -= 1
812
813        return service_record_handle_list
814
815    async def search_attributes(
816        self, uuids: List[core.UUID], attribute_ids: List[Union[int, Tuple[int, int]]]
817    ) -> List[List[ServiceAttribute]]:
818        if self.pending_request is not None:
819            raise InvalidStateError('request already pending')
820        if self.channel is None:
821            raise InvalidStateError('L2CAP not connected')
822
823        service_search_pattern = DataElement.sequence(
824            [DataElement.uuid(uuid) for uuid in uuids]
825        )
826        attribute_id_list = DataElement.sequence(
827            [
828                (
829                    DataElement.unsigned_integer(
830                        attribute_id[0], value_size=attribute_id[1]
831                    )
832                    if isinstance(attribute_id, tuple)
833                    else DataElement.unsigned_integer_16(attribute_id)
834                )
835                for attribute_id in attribute_ids
836            ]
837        )
838
839        # Request and accumulate until there's no more continuation
840        accumulator = b''
841        continuation_state = bytes([0])
842        watchdog = SDP_CONTINUATION_WATCHDOG
843        while watchdog > 0:
844            response_pdu = await self.channel.send_request(
845                SDP_ServiceSearchAttributeRequest(
846                    transaction_id=0,  # Transaction ID TODO: pick a real value
847                    service_search_pattern=service_search_pattern,
848                    maximum_attribute_byte_count=0xFFFF,
849                    attribute_id_list=attribute_id_list,
850                    continuation_state=continuation_state,
851                )
852            )
853            response = SDP_PDU.from_bytes(response_pdu)
854            logger.debug(f'<<< Response: {response}')
855            accumulator += response.attribute_lists
856            continuation_state = response.continuation_state
857            if len(continuation_state) == 1 and continuation_state[0] == 0:
858                break
859            logger.debug(f'continuation: {continuation_state.hex()}')
860            watchdog -= 1
861
862        # Parse the result into attribute lists
863        attribute_lists_sequences = DataElement.from_bytes(accumulator)
864        if attribute_lists_sequences.type != DataElement.SEQUENCE:
865            logger.warning('unexpected data type')
866            return []
867
868        return [
869            ServiceAttribute.list_from_data_elements(sequence.value)
870            for sequence in attribute_lists_sequences.value
871            if sequence.type == DataElement.SEQUENCE
872        ]
873
874    async def get_attributes(
875        self,
876        service_record_handle: int,
877        attribute_ids: List[Union[int, Tuple[int, int]]],
878    ) -> List[ServiceAttribute]:
879        if self.pending_request is not None:
880            raise InvalidStateError('request already pending')
881        if self.channel is None:
882            raise InvalidStateError('L2CAP not connected')
883
884        attribute_id_list = DataElement.sequence(
885            [
886                (
887                    DataElement.unsigned_integer(
888                        attribute_id[0], value_size=attribute_id[1]
889                    )
890                    if isinstance(attribute_id, tuple)
891                    else DataElement.unsigned_integer_16(attribute_id)
892                )
893                for attribute_id in attribute_ids
894            ]
895        )
896
897        # Request and accumulate until there's no more continuation
898        accumulator = b''
899        continuation_state = bytes([0])
900        watchdog = SDP_CONTINUATION_WATCHDOG
901        while watchdog > 0:
902            response_pdu = await self.channel.send_request(
903                SDP_ServiceAttributeRequest(
904                    transaction_id=0,  # Transaction ID TODO: pick a real value
905                    service_record_handle=service_record_handle,
906                    maximum_attribute_byte_count=0xFFFF,
907                    attribute_id_list=attribute_id_list,
908                    continuation_state=continuation_state,
909                )
910            )
911            response = SDP_PDU.from_bytes(response_pdu)
912            logger.debug(f'<<< Response: {response}')
913            accumulator += response.attribute_list
914            continuation_state = response.continuation_state
915            if len(continuation_state) == 1 and continuation_state[0] == 0:
916                break
917            logger.debug(f'continuation: {continuation_state.hex()}')
918            watchdog -= 1
919
920        # Parse the result into a list of attributes
921        attribute_list_sequence = DataElement.from_bytes(accumulator)
922        if attribute_list_sequence.type != DataElement.SEQUENCE:
923            logger.warning('unexpected data type')
924            return []
925
926        return ServiceAttribute.list_from_data_elements(attribute_list_sequence.value)
927
928    async def __aenter__(self) -> Self:
929        await self.connect()
930        return self
931
932    async def __aexit__(self, *args) -> None:
933        await self.disconnect()
934
935
936# -----------------------------------------------------------------------------
937class Server:
938    CONTINUATION_STATE = bytes([0x01, 0x43])
939    channel: Optional[l2cap.ClassicChannel]
940    Service = NewType('Service', List[ServiceAttribute])
941    service_records: Dict[int, Service]
942    current_response: Union[None, bytes, Tuple[int, List[int]]]
943
944    def __init__(self, device: Device) -> None:
945        self.device = device
946        self.service_records = {}  # Service records maps, by record handle
947        self.channel = None
948        self.current_response = None
949
950    def register(self, l2cap_channel_manager: l2cap.ChannelManager) -> None:
951        l2cap_channel_manager.create_classic_server(
952            spec=l2cap.ClassicChannelSpec(psm=SDP_PSM), handler=self.on_connection
953        )
954
955    def send_response(self, response):
956        logger.debug(f'{color(">>> Sending SDP Response", "blue")}: {response}')
957        self.channel.send_pdu(response)
958
959    def match_services(self, search_pattern: DataElement) -> Dict[int, Service]:
960        # Find the services for which the attributes in the pattern is a subset of the
961        # service's attribute values (NOTE: the value search recurses into sequences)
962        matching_services = {}
963        for handle, service in self.service_records.items():
964            for uuid in search_pattern.value:
965                found = False
966                for attribute in service:
967                    if ServiceAttribute.is_uuid_in_value(uuid.value, attribute.value):
968                        found = True
969                        break
970                if found:
971                    matching_services[handle] = service
972                    break
973
974        return matching_services
975
976    def on_connection(self, channel):
977        self.channel = channel
978        self.channel.sink = self.on_pdu
979
980    def on_pdu(self, pdu):
981        try:
982            sdp_pdu = SDP_PDU.from_bytes(pdu)
983        except Exception as error:
984            logger.warning(color(f'failed to parse SDP Request PDU: {error}', 'red'))
985            self.send_response(
986                SDP_ErrorResponse(
987                    transaction_id=0, error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR
988                )
989            )
990
991        logger.debug(f'{color("<<< Received SDP Request", "green")}: {sdp_pdu}')
992
993        # Find the handler method
994        handler_name = f'on_{sdp_pdu.name.lower()}'
995        handler = getattr(self, handler_name, None)
996        if handler:
997            try:
998                handler(sdp_pdu)
999            except Exception as error:
1000                logger.exception(f'{color("!!! Exception in handler:", "red")} {error}')
1001                self.send_response(
1002                    SDP_ErrorResponse(
1003                        transaction_id=sdp_pdu.transaction_id,
1004                        error_code=SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR,
1005                    )
1006                )
1007        else:
1008            logger.error(color('SDP Request not handled???', 'red'))
1009            self.send_response(
1010                SDP_ErrorResponse(
1011                    transaction_id=sdp_pdu.transaction_id,
1012                    error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR,
1013                )
1014            )
1015
1016    def get_next_response_payload(self, maximum_size):
1017        if len(self.current_response) > maximum_size:
1018            payload = self.current_response[:maximum_size]
1019            continuation_state = Server.CONTINUATION_STATE
1020            self.current_response = self.current_response[maximum_size:]
1021        else:
1022            payload = self.current_response
1023            continuation_state = bytes([0])
1024            self.current_response = None
1025
1026        return (payload, continuation_state)
1027
1028    @staticmethod
1029    def get_service_attributes(
1030        service: Service, attribute_ids: List[DataElement]
1031    ) -> DataElement:
1032        attributes = []
1033        for attribute_id in attribute_ids:
1034            if attribute_id.value_size == 4:
1035                # Attribute ID range
1036                id_range_start = attribute_id.value >> 16
1037                id_range_end = attribute_id.value & 0xFFFF
1038            else:
1039                id_range_start = attribute_id.value
1040                id_range_end = attribute_id.value
1041            attributes += [
1042                attribute
1043                for attribute in service
1044                if attribute.id >= id_range_start and attribute.id <= id_range_end
1045            ]
1046
1047        # Return the matching attributes, sorted by attribute id
1048        attributes.sort(key=lambda x: x.id)
1049        attribute_list = DataElement.sequence([])
1050        for attribute in attributes:
1051            attribute_list.value.append(DataElement.unsigned_integer_16(attribute.id))
1052            attribute_list.value.append(attribute.value)
1053
1054        return attribute_list
1055
1056    def on_sdp_service_search_request(self, request: SDP_ServiceSearchRequest) -> None:
1057        # Check if this is a continuation
1058        if len(request.continuation_state) > 1:
1059            if self.current_response is None:
1060                self.send_response(
1061                    SDP_ErrorResponse(
1062                        transaction_id=request.transaction_id,
1063                        error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
1064                    )
1065                )
1066                return
1067        else:
1068            # Cleanup any partial response leftover
1069            self.current_response = None
1070
1071            # Find the matching services
1072            matching_services = self.match_services(request.service_search_pattern)
1073            service_record_handles = list(matching_services.keys())
1074
1075            # Only return up to the maximum requested
1076            service_record_handles_subset = service_record_handles[
1077                : request.maximum_service_record_count
1078            ]
1079
1080            # Serialize to a byte array, and remember the total count
1081            logger.debug(f'Service Record Handles: {service_record_handles}')
1082            self.current_response = (
1083                len(service_record_handles),
1084                service_record_handles_subset,
1085            )
1086
1087        # Respond, keeping any unsent handles for later
1088        assert isinstance(self.current_response, tuple)
1089        service_record_handles = self.current_response[1][
1090            : request.maximum_service_record_count
1091        ]
1092        self.current_response = (
1093            self.current_response[0],
1094            self.current_response[1][request.maximum_service_record_count :],
1095        )
1096        continuation_state = (
1097            Server.CONTINUATION_STATE if self.current_response[1] else bytes([0])
1098        )
1099        service_record_handle_list = b''.join(
1100            [struct.pack('>I', handle) for handle in service_record_handles]
1101        )
1102        self.send_response(
1103            SDP_ServiceSearchResponse(
1104                transaction_id=request.transaction_id,
1105                total_service_record_count=self.current_response[0],
1106                current_service_record_count=len(service_record_handles),
1107                service_record_handle_list=service_record_handle_list,
1108                continuation_state=continuation_state,
1109            )
1110        )
1111
1112    def on_sdp_service_attribute_request(
1113        self, request: SDP_ServiceAttributeRequest
1114    ) -> None:
1115        # Check if this is a continuation
1116        if len(request.continuation_state) > 1:
1117            if self.current_response is None:
1118                self.send_response(
1119                    SDP_ErrorResponse(
1120                        transaction_id=request.transaction_id,
1121                        error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
1122                    )
1123                )
1124                return
1125        else:
1126            # Cleanup any partial response leftover
1127            self.current_response = None
1128
1129            # Check that the service exists
1130            service = self.service_records.get(request.service_record_handle)
1131            if service is None:
1132                self.send_response(
1133                    SDP_ErrorResponse(
1134                        transaction_id=request.transaction_id,
1135                        error_code=SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR,
1136                    )
1137                )
1138                return
1139
1140            # Get the attributes for the service
1141            attribute_list = Server.get_service_attributes(
1142                service, request.attribute_id_list.value
1143            )
1144
1145            # Serialize to a byte array
1146            logger.debug(f'Attributes: {attribute_list}')
1147            self.current_response = bytes(attribute_list)
1148
1149        # Respond, keeping any pending chunks for later
1150        attribute_list_response, continuation_state = self.get_next_response_payload(
1151            request.maximum_attribute_byte_count
1152        )
1153        self.send_response(
1154            SDP_ServiceAttributeResponse(
1155                transaction_id=request.transaction_id,
1156                attribute_list_byte_count=len(attribute_list_response),
1157                attribute_list=attribute_list,
1158                continuation_state=continuation_state,
1159            )
1160        )
1161
1162    def on_sdp_service_search_attribute_request(
1163        self, request: SDP_ServiceSearchAttributeRequest
1164    ) -> None:
1165        # Check if this is a continuation
1166        if len(request.continuation_state) > 1:
1167            if self.current_response is None:
1168                self.send_response(
1169                    SDP_ErrorResponse(
1170                        transaction_id=request.transaction_id,
1171                        error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
1172                    )
1173                )
1174        else:
1175            # Cleanup any partial response leftover
1176            self.current_response = None
1177
1178            # Find the matching services
1179            matching_services = self.match_services(
1180                request.service_search_pattern
1181            ).values()
1182
1183            # Filter the required attributes
1184            attribute_lists = DataElement.sequence([])
1185            for service in matching_services:
1186                attribute_list = Server.get_service_attributes(
1187                    service, request.attribute_id_list.value
1188                )
1189                if attribute_list.value:
1190                    attribute_lists.value.append(attribute_list)
1191
1192            # Serialize to a byte array
1193            logger.debug(f'Search response: {attribute_lists}')
1194            self.current_response = bytes(attribute_lists)
1195
1196        # Respond, keeping any pending chunks for later
1197        attribute_lists_response, continuation_state = self.get_next_response_payload(
1198            request.maximum_attribute_byte_count
1199        )
1200        self.send_response(
1201            SDP_ServiceSearchAttributeResponse(
1202                transaction_id=request.transaction_id,
1203                attribute_lists_byte_count=len(attribute_lists_response),
1204                attribute_lists=attribute_lists,
1205                continuation_state=continuation_state,
1206            )
1207        )
1208