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

91 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-30 00:10 +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 SINT32_MAX, 

14 SINT32_MIN, 

15 UINT8_MAX, 

16 UINT16_MAX, 

17 UINT32_MAX, 

18) 

19from ...exceptions import InsufficientDataError, ValueRangeError 

20 

21 

22class DataParser: 

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

24 

25 @staticmethod 

26 def parse_int8( 

27 data: bytes | bytearray, 

28 offset: int = 0, 

29 signed: bool = False, 

30 ) -> int: 

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

32 if len(data) < offset + 1: 

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

34 value = data[offset] 

35 if signed and value >= 128: 

36 return value - 256 

37 return value 

38 

39 @staticmethod 

40 def parse_int16( 

41 data: bytes | bytearray, 

42 offset: int = 0, 

43 signed: bool = False, 

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

45 ) -> int: 

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

47 if len(data) < offset + 2: 

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

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

50 

51 @staticmethod 

52 def parse_int32( 

53 data: bytes | bytearray, 

54 offset: int = 0, 

55 signed: bool = False, 

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

57 ) -> int: 

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

59 if len(data) < offset + 4: 

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

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

62 

63 @staticmethod 

64 def parse_int24( 

65 data: bytes | bytearray, 

66 offset: int = 0, 

67 signed: bool = False, 

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

69 ) -> int: 

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

71 if len(data) < offset + 3: 

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

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

74 # For signed 24-bit, extend sign bit if needed 

75 if signed and (value & 0x800000): 

76 value -= 0x1000000 

77 return value 

78 

79 @staticmethod 

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

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

82 if len(data) < offset + 4: 

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

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

85 

86 @staticmethod 

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

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

89 if len(data) < offset + 8: 

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

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

92 

93 @staticmethod 

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

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

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

97 

98 @staticmethod 

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

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

101 length = len(data) 

102 if length < min_length: 

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

104 if length > max_length: 

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

106 return bytes(data) 

107 

108 @staticmethod 

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

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

111 if signed: 

112 if not SINT8_MIN <= value <= SINT8_MAX: 

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

114 else: 

115 if not 0 <= value <= UINT8_MAX: 

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

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

118 

119 @staticmethod 

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

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

122 if signed: 

123 if not SINT16_MIN <= value <= SINT16_MAX: 

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

125 else: 

126 if not 0 <= value <= UINT16_MAX: 

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

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

129 

130 @staticmethod 

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

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

133 if signed: 

134 if not SINT32_MIN <= value <= SINT32_MAX: 

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

136 else: 

137 if not 0 <= value <= UINT32_MAX: 

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

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

140 

141 @staticmethod 

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

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

144 if signed: 

145 if not -0x800000 <= value <= 0x7FFFFF: 

146 raise ValueRangeError("sint24", value, -0x800000, 0x7FFFFF) 

147 else: 

148 if not 0 <= value <= 0xFFFFFF: 

149 raise ValueRangeError("uint24", value, 0, 0xFFFFFF) 

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

151 

152 @staticmethod 

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

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

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

156 

157 @staticmethod 

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

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

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