Coverage for src / bluetooth_sig / types / uri.py: 100%
35 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
1"""URI data types for Bluetooth advertising."""
3from __future__ import annotations
5import msgspec
7from bluetooth_sig.registry.core.uri_schemes import uri_schemes_registry
8from bluetooth_sig.types.registry.uri_schemes import UriSchemeInfo
11class URIData(msgspec.Struct, frozen=True, kw_only=True):
12 """Parsed URI from Bluetooth advertising data.
14 The Bluetooth SIG URI AD type (0x24) uses a compressed format where
15 the first byte is a scheme code from the URI Schemes registry, followed
16 by the remainder of the URI encoded as UTF-8.
18 For example:
19 - 0x16 = "http:" prefix
20 - 0x17 = "https:" prefix
22 Attributes:
23 scheme_code: Raw scheme code from first byte (0 if plain URI)
24 scheme_info: Resolved scheme information from registry
25 full_uri: Complete decoded URI with scheme prefix
26 raw_data: Original raw bytes from advertising data
27 """
29 scheme_code: int
30 """Raw URI scheme code from the first byte of encoded data."""
32 scheme_info: UriSchemeInfo | None = None
33 """Resolved scheme info from UriSchemesRegistry, or None if unknown."""
35 full_uri: str = ""
36 """Complete URI with resolved scheme prefix."""
38 raw_data: bytes = b""
39 """Original raw advertising data bytes."""
41 @property
42 def scheme_name(self) -> str:
43 """Get the URI scheme name (e.g., 'http:', 'https:').
45 Returns:
46 Scheme name from registry, or empty string if unknown.
47 """
48 return self.scheme_info.name if self.scheme_info else ""
50 @property
51 def is_known_scheme(self) -> bool:
52 """Check if the URI scheme is a known Bluetooth SIG registered scheme.
54 Returns:
55 True if scheme_code resolved to a known scheme.
56 """
57 return self.scheme_info is not None
59 @classmethod
60 def from_raw_data(cls, data: bytes) -> URIData:
61 r"""Parse URI advertising data using Bluetooth SIG encoding.
63 The first byte is a URI scheme code from the registry. The remaining
64 bytes are the URI suffix encoded as UTF-8.
66 Args:
67 data: Raw bytes from URI AD type (ADType 0x24)
69 Returns:
70 URIData with decoded URI and scheme information
72 Example:
73 >>> # 0x17 = "https:", followed by "//example.com"
74 >>> uri_data = URIData.from_raw_data(b"\x17//example.com")
75 >>> uri_data.full_uri
76 'https://example.com'
77 >>> uri_data.scheme_name
78 'https:'
79 """
80 if not data:
81 return cls(scheme_code=0, raw_data=data)
83 scheme_code = data[0]
84 scheme_info = uri_schemes_registry.get_uri_scheme_info(scheme_code)
86 # Decode the URI suffix (remaining bytes after scheme code)
87 try:
88 uri_suffix = data[1:].decode("utf-8") if len(data) > 1 else ""
89 except UnicodeDecodeError:
90 # Fall back to hex representation if not valid UTF-8
91 uri_suffix = data[1:].hex() if len(data) > 1 else ""
93 # Build full URI by combining scheme prefix with suffix
94 scheme_prefix = scheme_info.name if scheme_info else ""
95 full_uri = f"{scheme_prefix}{uri_suffix}"
97 return cls(
98 scheme_code=scheme_code,
99 scheme_info=scheme_info,
100 full_uri=full_uri,
101 raw_data=data,
102 )
104 @classmethod
105 def from_plain_uri(cls, uri: str) -> URIData:
106 """Create URIData from a plain URI string (no scheme encoding).
108 Use this for URIs that aren't using Bluetooth SIG compressed encoding.
110 Args:
111 uri: Plain URI string
113 Returns:
114 URIData with the URI stored directly
115 """
116 return cls(
117 scheme_code=0,
118 full_uri=uri,
119 raw_data=uri.encode("utf-8"),
120 )