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