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

88 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +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 return int.from_bytes(data[offset : offset + 3], byteorder=endian, signed=signed) 

74 

75 @staticmethod 

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

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

78 if len(data) < offset + 4: 

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

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

81 

82 @staticmethod 

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

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

85 if len(data) < offset + 8: 

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

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

88 

89 @staticmethod 

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

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

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

93 

94 @staticmethod 

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

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

97 length = len(data) 

98 if length < min_length: 

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

100 if length > max_length: 

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

102 return bytes(data) 

103 

104 @staticmethod 

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

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

107 if signed: 

108 if not SINT8_MIN <= value <= SINT8_MAX: 

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

110 else: 

111 if not 0 <= value <= UINT8_MAX: 

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

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

114 

115 @staticmethod 

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

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

118 if signed: 

119 if not SINT16_MIN <= value <= SINT16_MAX: 

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

121 else: 

122 if not 0 <= value <= UINT16_MAX: 

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

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

125 

126 @staticmethod 

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

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

129 if signed: 

130 if not SINT32_MIN <= value <= SINT32_MAX: 

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

132 else: 

133 if not 0 <= value <= UINT32_MAX: 

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

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

136 

137 @staticmethod 

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

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

140 if signed: 

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

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

143 else: 

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

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

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

147 

148 @staticmethod 

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

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

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

152 

153 @staticmethod 

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

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

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