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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Data parsing utilities for basic data types."""
3from __future__ import annotations
5import struct
6from typing import Literal
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
25# Sign threshold for int8 values (values >= 128 are negative when signed)
26_INT8_SIGN_THRESHOLD = 128
29class DataParser:
30 """Utility class for basic data type parsing and encoding."""
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
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)
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)
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)
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)
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])
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])
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")
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)
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))
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))
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))
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))
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))
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))
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))