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

1"""URI data types for Bluetooth advertising.""" 

2 

3from __future__ import annotations 

4 

5import msgspec 

6 

7from bluetooth_sig.registry.core.uri_schemes import uri_schemes_registry 

8from bluetooth_sig.types.registry.uri_schemes import UriSchemeInfo 

9 

10 

11class URIData(msgspec.Struct, frozen=True, kw_only=True): 

12 """Parsed URI from Bluetooth advertising data. 

13 

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. 

17 

18 For example: 

19 - 0x16 = "http:" prefix 

20 - 0x17 = "https:" prefix 

21 

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 """ 

28 

29 scheme_code: int 

30 """Raw URI scheme code from the first byte of encoded data.""" 

31 

32 scheme_info: UriSchemeInfo | None = None 

33 """Resolved scheme info from UriSchemesRegistry, or None if unknown.""" 

34 

35 full_uri: str = "" 

36 """Complete URI with resolved scheme prefix.""" 

37 

38 raw_data: bytes = b"" 

39 """Original raw advertising data bytes.""" 

40 

41 @property 

42 def scheme_name(self) -> str: 

43 """Get the URI scheme name (e.g., 'http:', 'https:'). 

44 

45 Returns: 

46 Scheme name from registry, or empty string if unknown. 

47 """ 

48 return self.scheme_info.name if self.scheme_info else "" 

49 

50 @property 

51 def is_known_scheme(self) -> bool: 

52 """Check if the URI scheme is a known Bluetooth SIG registered scheme. 

53 

54 Returns: 

55 True if scheme_code resolved to a known scheme. 

56 """ 

57 return self.scheme_info is not None 

58 

59 @classmethod 

60 def from_raw_data(cls, data: bytes) -> URIData: 

61 r"""Parse URI advertising data using Bluetooth SIG encoding. 

62 

63 The first byte is a URI scheme code from the registry. The remaining 

64 bytes are the URI suffix encoded as UTF-8. 

65 

66 Args: 

67 data: Raw bytes from URI AD type (ADType 0x24) 

68 

69 Returns: 

70 URIData with decoded URI and scheme information 

71 

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) 

82 

83 scheme_code = data[0] 

84 scheme_info = uri_schemes_registry.get_uri_scheme_info(scheme_code) 

85 

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 "" 

92 

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}" 

96 

97 return cls( 

98 scheme_code=scheme_code, 

99 scheme_info=scheme_info, 

100 full_uri=full_uri, 

101 raw_data=data, 

102 ) 

103 

104 @classmethod 

105 def from_plain_uri(cls, uri: str) -> URIData: 

106 """Create URIData from a plain URI string (no scheme encoding). 

107 

108 Use this for URIs that aren't using Bluetooth SIG compressed encoding. 

109 

110 Args: 

111 uri: Plain URI string 

112 

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 )