Coverage for src / bluetooth_sig / gatt / characteristics / utils / data_parser.py: 93%

104 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Data parsing utilities for basic data types.""" 

2 

3from __future__ import annotations 

4 

5import struct 

6from typing import Literal 

7 

8from ...constants import ( 

9 SINT8_MAX, 

10 SINT8_MIN, 

11 SINT16_MAX, 

12 SINT16_MIN, 

13 SINT24_MAX, 

14 SINT24_MIN, 

15 SINT32_MAX, 

16 SINT32_MIN, 

17 UINT8_MAX, 

18 UINT16_MAX, 

19 UINT24_MAX, 

20 UINT32_MAX, 

21 UINT48_MAX, 

22) 

23from ...exceptions import InsufficientDataError, ValueRangeError 

24 

25# Sign threshold for int8 values (values >= 128 are negative when signed) 

26_INT8_SIGN_THRESHOLD = 128 

27 

28 

29class DataParser: 

30 """Utility class for basic data type parsing and encoding.""" 

31 

32 @staticmethod 

33 def parse_int8( 

34 data: bytes | bytearray, 

35 offset: int = 0, 

36 signed: bool = False, 

37 ) -> int: 

38 """Parse 8-bit integer with optional signed interpretation.""" 

39 if len(data) < offset + 1: 

40 raise InsufficientDataError("int8", data[offset:], 1) 

41 value = data[offset] 

42 if signed and value >= _INT8_SIGN_THRESHOLD: 

43 return value - 256 

44 return value 

45 

46 @staticmethod 

47 def parse_int16( 

48 data: bytes | bytearray, 

49 offset: int = 0, 

50 signed: bool = False, 

51 endian: Literal["little", "big"] = "little", 

52 ) -> int: 

53 """Parse 16-bit integer with configurable endianness and signed interpretation.""" 

54 if len(data) < offset + 2: 

55 raise InsufficientDataError("int16", data[offset:], 2) 

56 return int.from_bytes(data[offset : offset + 2], byteorder=endian, signed=signed) 

57 

58 @staticmethod 

59 def parse_int32( 

60 data: bytes | bytearray, 

61 offset: int = 0, 

62 signed: bool = False, 

63 endian: Literal["little", "big"] = "little", 

64 ) -> int: 

65 """Parse 32-bit integer with configurable endianness and signed interpretation.""" 

66 if len(data) < offset + 4: 

67 raise InsufficientDataError("int32", data[offset:], 4) 

68 return int.from_bytes(data[offset : offset + 4], byteorder=endian, signed=signed) 

69 

70 @staticmethod 

71 def parse_int24( 

72 data: bytes | bytearray, 

73 offset: int = 0, 

74 signed: bool = False, 

75 endian: Literal["little", "big"] = "little", 

76 ) -> int: 

77 """Parse 24-bit integer with configurable endianness and signed interpretation.""" 

78 if len(data) < offset + 3: 

79 raise InsufficientDataError("int24", data[offset:], 3) 

80 return int.from_bytes(data[offset : offset + 3], byteorder=endian, signed=signed) 

81 

82 @staticmethod 

83 def parse_int48( 

84 data: bytes | bytearray, 

85 offset: int = 0, 

86 signed: bool = False, 

87 endian: Literal["little", "big"] = "little", 

88 ) -> int: 

89 """Parse 48-bit integer with configurable endianness and signed interpretation.""" 

90 if len(data) < offset + 6: 

91 raise InsufficientDataError("int48", data[offset:], 6) 

92 return int.from_bytes(data[offset : offset + 6], byteorder=endian, signed=signed) 

93 

94 @staticmethod 

95 def parse_float32(data: bytearray, offset: int = 0) -> float: 

96 """Parse IEEE-754 32-bit float (little-endian).""" 

97 if len(data) < offset + 4: 

98 raise InsufficientDataError("float32", data[offset:], 4) 

99 return float(struct.unpack("<f", data[offset : offset + 4])[0]) 

100 

101 @staticmethod 

102 def parse_float64(data: bytearray, offset: int = 0) -> float: 

103 """Parse IEEE-754 64-bit double (little-endian).""" 

104 if len(data) < offset + 8: 

105 raise InsufficientDataError("float64", data[offset:], 8) 

106 return float(struct.unpack("<d", data[offset : offset + 8])[0]) 

107 

108 @staticmethod 

109 def parse_utf8_string(data: bytearray) -> str: 

110 """Parse UTF-8 string from bytearray with null termination handling.""" 

111 return data.decode("utf-8", errors="replace").rstrip("\x00") 

112 

113 @staticmethod 

114 def parse_variable_length(data: bytes | bytearray, min_length: int, max_length: int) -> bytes: 

115 """Parse variable length data with validation.""" 

116 length = len(data) 

117 if length < min_length: 

118 raise ValueError(f"Data too short: {length} < {min_length}") 

119 if length > max_length: 

120 raise ValueError(f"Data too long: {length} > {max_length}") 

121 return bytes(data) 

122 

123 @staticmethod 

124 def encode_int8(value: int, signed: bool = False) -> bytearray: 

125 """Encode 8-bit integer with signed/unsigned validation.""" 

126 if signed: 

127 if not SINT8_MIN <= value <= SINT8_MAX: 

128 raise ValueRangeError("sint8", value, SINT8_MIN, SINT8_MAX) 

129 elif not 0 <= value <= UINT8_MAX: 

130 raise ValueRangeError("uint8", value, 0, UINT8_MAX) 

131 return bytearray(value.to_bytes(1, byteorder="little", signed=signed)) 

132 

133 @staticmethod 

134 def encode_int16(value: int, signed: bool = False, endian: Literal["little", "big"] = "little") -> bytearray: 

135 """Encode 16-bit integer with configurable endianness and signed/unsigned validation.""" 

136 if signed: 

137 if not SINT16_MIN <= value <= SINT16_MAX: 

138 raise ValueRangeError("sint16", value, SINT16_MIN, SINT16_MAX) 

139 elif not 0 <= value <= UINT16_MAX: 

140 raise ValueRangeError("uint16", value, 0, UINT16_MAX) 

141 return bytearray(value.to_bytes(2, byteorder=endian, signed=signed)) 

142 

143 @staticmethod 

144 def encode_int32(value: int, signed: bool = False, endian: Literal["little", "big"] = "little") -> bytearray: 

145 """Encode 32-bit integer with configurable endianness and signed/unsigned validation.""" 

146 if signed: 

147 if not SINT32_MIN <= value <= SINT32_MAX: 

148 raise ValueRangeError("sint32", value, SINT32_MIN, SINT32_MAX) 

149 elif not 0 <= value <= UINT32_MAX: 

150 raise ValueRangeError("uint32", value, 0, UINT32_MAX) 

151 return bytearray(value.to_bytes(4, byteorder=endian, signed=signed)) 

152 

153 @staticmethod 

154 def encode_int24(value: int, signed: bool = False, endian: Literal["little", "big"] = "little") -> bytearray: 

155 """Encode 24-bit integer with configurable endianness and signed/unsigned validation.""" 

156 if signed: 

157 if not SINT24_MIN <= value <= SINT24_MAX: 

158 raise ValueRangeError("sint24", value, SINT24_MIN, SINT24_MAX) 

159 elif not 0 <= value <= UINT24_MAX: 

160 raise ValueRangeError("uint24", value, 0, UINT24_MAX) 

161 return bytearray(value.to_bytes(3, byteorder=endian, signed=signed)) 

162 

163 @staticmethod 

164 def encode_int48(value: int, signed: bool = False, endian: Literal["little", "big"] = "little") -> bytearray: 

165 """Encode 48-bit integer with configurable endianness and signed/unsigned validation.""" 

166 if signed: 

167 min_val = -(1 << 47) 

168 max_val = (1 << 47) - 1 

169 if not min_val <= value <= max_val: 

170 raise ValueRangeError("sint48", value, min_val, max_val) 

171 elif not 0 <= value <= UINT48_MAX: 

172 raise ValueRangeError("uint48", value, 0, UINT48_MAX) 

173 return bytearray(value.to_bytes(6, byteorder=endian, signed=signed)) 

174 

175 @staticmethod 

176 def encode_float32(value: float) -> bytearray: 

177 """Encode IEEE-754 32-bit float (little-endian).""" 

178 return bytearray(struct.pack("<f", value)) 

179 

180 @staticmethod 

181 def encode_float64(value: float) -> bytearray: 

182 """Encode IEEE-754 64-bit double (little-endian).""" 

183 return bytearray(struct.pack("<d", value))