mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-09 14:03:42 +08:00
145 lines
4.6 KiB
Python
145 lines
4.6 KiB
Python
"""Bounded protobuf wire helpers shared by the Android Auto session and wireless bootstrap.
|
|
|
|
No generated protobuf runtime is needed: only varint and length-delimited fields
|
|
appear in the subset of messages StarPilot sends or reads. Unknown fields are
|
|
preserved by ``parse_fields`` so diagnostics can report them without guessing.
|
|
|
|
Adapted from yummydirtx/openpilot ``tools/starport/session.py`` (MIT),
|
|
pinned at 672a16f6183567c0ada53654f8527d97e1a483fa.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
def varint(value: int) -> bytes:
|
|
if not 0 <= value < 1 << 64:
|
|
raise ValueError("Expected an unsigned 64-bit protobuf integer")
|
|
out = bytearray()
|
|
while value > 127:
|
|
out.append((value & 127) | 128)
|
|
value >>= 7
|
|
out.append(value)
|
|
return bytes(out)
|
|
|
|
|
|
def field(number: int, value: int | str | bytes) -> bytes:
|
|
"""Encode one protobuf field. Negative ints use proto2 int32/int64 sign extension."""
|
|
if isinstance(value, str):
|
|
value = value.encode()
|
|
if isinstance(value, bytes):
|
|
return varint(number << 3 | 2) + varint(len(value)) + value
|
|
if value < 0:
|
|
value += 1 << 64
|
|
return varint(number << 3) + varint(value)
|
|
|
|
|
|
def parse_fields(data: bytes) -> dict[int, list[int | bytes]]:
|
|
"""Decode protobuf wire fields into ``{number: [values]}`` with strict bounds."""
|
|
pos = 0
|
|
|
|
def integer() -> int:
|
|
nonlocal pos
|
|
result = 0
|
|
for shift in range(0, 70, 7):
|
|
if pos >= len(data):
|
|
raise ValueError("Truncated protobuf varint")
|
|
byte = data[pos]
|
|
pos += 1
|
|
if shift == 63 and byte > 1:
|
|
raise ValueError("Overflowing protobuf varint")
|
|
result |= (byte & 127) << shift
|
|
if byte < 128:
|
|
return result
|
|
raise ValueError("Overlong protobuf varint")
|
|
|
|
result: dict[int, list[int | bytes]] = {}
|
|
while pos < len(data):
|
|
tag = integer()
|
|
number, wire = tag >> 3, tag & 7
|
|
if not 0 < number < 1 << 29:
|
|
raise ValueError("Invalid protobuf field number")
|
|
value: int | bytes
|
|
if wire == 0:
|
|
value = integer()
|
|
else:
|
|
if wire == 2:
|
|
size = integer()
|
|
elif wire in (1, 5):
|
|
size = 8 if wire == 1 else 4
|
|
else:
|
|
raise ValueError(f"Unsupported protobuf wire type {wire}")
|
|
if size > len(data) - pos:
|
|
raise ValueError("Truncated protobuf field")
|
|
value = bytes(data[pos:pos + size])
|
|
pos += size
|
|
result.setdefault(number, []).append(value)
|
|
return result
|
|
|
|
|
|
def one(fields: dict, number: int, default=None):
|
|
return fields.get(number, [default])[0]
|
|
|
|
|
|
def signed(value: int | None) -> int | None:
|
|
"""Interpret a decoded varint as a two's-complement int64 (proto2 int32 negatives)."""
|
|
if value is None:
|
|
return None
|
|
return value - (1 << 64) if value >= 1 << 63 else value
|
|
|
|
|
|
def text(fields: dict, number: int, default: str = "") -> str:
|
|
value = one(fields, number)
|
|
if not isinstance(value, bytes):
|
|
return default
|
|
return value.decode("utf-8", "replace")
|
|
|
|
|
|
def json_fields(fields: dict) -> dict:
|
|
"""Keep unknown protobuf bytes inspectable without guessing their schema."""
|
|
return {number: [{"hex": value.hex()} if isinstance(value, bytes) else value for value in values]
|
|
for number, values in fields.items()}
|
|
|
|
|
|
DESCRIBE_TEXT = 512
|
|
DESCRIBE_DEPTH = 4
|
|
DESCRIBE_REPEATED = 32 # values kept per field; a head unit lists at most a few dozen keycodes or sensors
|
|
DESCRIBE_HEX = 64 # bytes of an opaque value kept as hex
|
|
|
|
|
|
def _describe_value(value, depth: int):
|
|
if not isinstance(value, bytes):
|
|
return value
|
|
try:
|
|
decoded = value.decode("utf-8")
|
|
if decoded and decoded.isprintable():
|
|
return decoded[:DESCRIBE_TEXT] if len(decoded) <= DESCRIBE_TEXT else decoded[:DESCRIBE_TEXT] + "…"
|
|
except UnicodeDecodeError:
|
|
pass
|
|
if depth > 0 and value:
|
|
try:
|
|
return describe_fields(parse_fields(value), depth - 1)
|
|
except ValueError:
|
|
pass
|
|
described = {"hex": value[:DESCRIBE_HEX].hex()}
|
|
if len(value) > DESCRIBE_HEX:
|
|
described["bytes"] = len(value)
|
|
return described
|
|
|
|
|
|
def describe_fields(fields: dict, depth: int = DESCRIBE_DEPTH) -> dict:
|
|
"""A bounded, readable view of a message whose schema we may not know, for diagnostics.
|
|
|
|
Text stays text and nested messages are decoded, so a head unit's make, model and
|
|
capabilities read directly in a log; anything else is a short hex prefix.
|
|
"""
|
|
return {number: [_describe_value(value, depth) for value in values[:DESCRIBE_REPEATED]]
|
|
for number, values in fields.items()}
|
|
|
|
|
|
def describe(data: bytes, depth: int = DESCRIBE_DEPTH):
|
|
"""``describe_fields`` of raw bytes, or their hex prefix when they are not protobuf."""
|
|
try:
|
|
return describe_fields(parse_fields(data), depth)
|
|
except ValueError:
|
|
return _describe_value(data, 0)
|