Coverage for src / bluetooth_sig / gatt / characteristics / templates / string.py: 95%
66 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"""String templates for UTF-8 and UTF-16LE variable-length parsing.
3Covers Utf8StringTemplate and Utf16StringTemplate.
4"""
6from __future__ import annotations
8from ...context import CharacteristicContext
9from .base import CodingTemplate
12class Utf8StringTemplate(CodingTemplate[str]):
13 """Template for UTF-8 string parsing with variable length."""
15 def __init__(self, max_length: int = 256) -> None:
16 """Initialize with maximum string length.
18 Args:
19 max_length: Maximum string length in bytes
21 """
22 self.max_length = max_length
24 @property
25 def data_size(self) -> int:
26 """Size: Variable (0 to max_length)."""
27 return self.max_length
29 def decode_value(
30 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
31 ) -> str:
32 """Parse UTF-8 string from remaining data."""
33 if offset >= len(data):
34 return ""
36 # Take remaining data from offset
37 string_data = data[offset:]
39 # Remove null terminator if present
40 if b"\x00" in string_data:
41 null_index = string_data.index(b"\x00")
42 string_data = string_data[:null_index]
44 try:
45 return string_data.decode("utf-8")
46 except UnicodeDecodeError as e:
47 if validate:
48 raise ValueError(f"Invalid UTF-8 string data: {e}") from e
49 return ""
51 def encode_value(self, value: str, *, validate: bool = True) -> bytearray:
52 """Encode string to UTF-8 bytes."""
53 encoded = value.encode("utf-8")
54 if validate and len(encoded) > self.max_length:
55 raise ValueError(f"String too long: {len(encoded)} > {self.max_length}")
56 return bytearray(encoded)
59class Utf16StringTemplate(CodingTemplate[str]):
60 """Template for UTF-16LE string parsing with variable length."""
62 # Unicode constants for UTF-16 validation
63 UNICODE_SURROGATE_START = 0xD800
64 UNICODE_SURROGATE_END = 0xDFFF
65 UNICODE_BOM = "\ufeff"
67 def __init__(self, max_length: int = 256) -> None:
68 """Initialize with maximum string length.
70 Args:
71 max_length: Maximum string length in bytes (must be even)
73 """
74 if max_length % 2 != 0:
75 raise ValueError("max_length must be even for UTF-16 strings")
76 self.max_length = max_length
78 @property
79 def data_size(self) -> int:
80 """Size: Variable (0 to max_length, even bytes only)."""
81 return self.max_length
83 def decode_value(
84 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
85 ) -> str:
86 """Parse UTF-16LE string from remaining data."""
87 if offset >= len(data):
88 return ""
90 # Take remaining data from offset
91 string_data = data[offset:]
93 # Find null terminator at even positions (UTF-16 alignment)
94 null_index = len(string_data)
95 for i in range(0, len(string_data) - 1, 2):
96 if string_data[i : i + 2] == bytearray(b"\x00\x00"):
97 null_index = i
98 break
99 string_data = string_data[:null_index]
101 # UTF-16 requires even number of bytes
102 if validate and len(string_data) % 2 != 0:
103 raise ValueError(f"UTF-16 data must have even byte count, got {len(string_data)}")
105 try:
106 decoded = string_data.decode("utf-16-le")
107 # Strip BOM if present (robustness)
108 if decoded.startswith(self.UNICODE_BOM):
109 decoded = decoded[1:]
110 # Check for invalid surrogate pairs
111 if validate and any(self.UNICODE_SURROGATE_START <= ord(c) <= self.UNICODE_SURROGATE_END for c in decoded):
112 raise ValueError("Invalid UTF-16LE string data: contains unpaired surrogates")
113 except UnicodeDecodeError as e:
114 if validate:
115 raise ValueError(f"Invalid UTF-16LE string data: {e}") from e
116 return ""
117 else:
118 return decoded
120 def encode_value(self, value: str, *, validate: bool = True) -> bytearray:
121 """Encode string to UTF-16LE bytes."""
122 encoded = value.encode("utf-16-le")
123 if validate and len(encoded) > self.max_length:
124 raise ValueError(f"String too long: {len(encoded)} > {self.max_length}")
125 return bytearray(encoded)