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
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +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 SINT32_MAX,
14 SINT32_MIN,
15 UINT8_MAX,
16 UINT16_MAX,
17 UINT32_MAX,
18)
19from ...exceptions import InsufficientDataError, ValueRangeError
22class DataParser:
23 """Utility class for basic data type parsing and encoding."""
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
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)
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)
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
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])
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])
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")
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)
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))
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))
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))
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))
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))
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))