diff options
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/greeneye/structs.py')
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/structs.py | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/structs.py b/debug/risc-v-gsp-rm-vm/greeneye/structs.py new file mode 100644 index 0000000..3081bc9 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/greeneye/structs.py @@ -0,0 +1,197 @@ +"""Declarative structure decoding for firmware memory. + +A schema is plain data (JSON-friendly), so the same description can come from +the repo, a Ghidra export, or a plugin: + + { + "name": "gsp_ctrl", + "fields": [ + {"name": "magic", "offset": 0, "fmt": "u32"}, + {"name": "state", "offset": 8, "fmt": "u32", "enum": {"0": "IDLE", "1": "RUN"}}, + {"name": "buf", "offset": 64, "fmt": "bytes", "size": 256}, + {"name": "name", "offset": 64, "fmt": "cstr", "size": 32} + ] + } + +Supported formats: u8/u16/u32/u64, s8/s16/s32/s64, f32/f64, bool, bytes, +cstr, ptr (u64 by default; override with ``size``). +""" +from __future__ import annotations + +import struct as _struct +from dataclasses import dataclass, field as _field + +from .errors import StructError + +_INT_FORMATS = { + "u8": ("B", 1), + "u16": ("H", 2), + "u32": ("I", 4), + "u64": ("Q", 8), + "s8": ("b", 1), + "s16": ("h", 2), + "s32": ("i", 4), + "s64": ("q", 8), + "bool": ("B", 1), +} +_FLOAT_FORMATS = {"f32": ("f", 4), "f64": ("d", 8)} + + +@dataclass +class Field: + name: str + offset: int + fmt: str + size: int = 0 + enum: dict | None = None + note: str | None = None + count: int = 1 + + @property + def nbytes(self) -> int: + if self.fmt in _INT_FORMATS: + return _INT_FORMATS[self.fmt][1] + if self.fmt in _FLOAT_FORMATS: + return _FLOAT_FORMATS[self.fmt][1] + if self.fmt in ("bytes", "cstr"): + return self.size + if self.fmt == "ptr": + return self.size or 8 + if self.fmt == "u64x": + return 8 + raise StructError(f"field {self.name!r}: unknown format {self.fmt!r}") + + +@dataclass +class Schema: + name: str + fields: list[Field] = _field(default_factory=list) + size: int = 0 + + def __post_init__(self) -> None: + computed = max((f.offset + f.nbytes for f in self.fields), default=0) + if not self.size: + self.size = computed + elif self.size < computed: + raise StructError( + f"schema {self.name!r}: declared size {self.size:#x} < fields end {computed:#x}" + ) + self.validate() + + def validate(self) -> None: + seen: set[str] = set() + for field in self.fields: + if field.name in seen: + raise StructError(f"schema {self.name!r}: duplicate field {field.name!r}") + seen.add(field.name) + if field.offset < 0: + raise StructError(f"schema {self.name!r}: negative offset for {field.name!r}") + if field.nbytes <= 0: + raise StructError(f"schema {self.name!r}: zero-size field {field.name!r}") + + def decode(self, data: bytes, base: int = 0) -> dict: + if len(data) < self.size: + raise StructError( + f"schema {self.name!r}: need {self.size} bytes, got {len(data)}" + ) + out: dict = {} + for field in self.fields: + out[field.name] = _decode_field(field, data, base) + return out + + +def _decode_field(field: Field, data: bytes, base: int): + start = field.offset + if field.fmt in _INT_FORMATS: + code, width = _INT_FORMATS[field.fmt] + value = _struct.unpack_from("<" + code, data, start)[0] + if field.fmt == "bool": + value = bool(value) + return value + if field.fmt in _FLOAT_FORMATS: + code, _width = _FLOAT_FORMATS[field.fmt] + return _struct.unpack_from("<" + code, data, start)[0] + if field.fmt == "ptr": + return int.from_bytes(data[start : start + field.nbytes], "little") + if field.fmt == "bytes": + return bytes(data[start : start + field.size]) + if field.fmt == "cstr": + raw = data[start : start + field.size] + return raw.split(b"\x00", 1)[0].decode("utf-8", "replace") + raise StructError(f"unknown format {field.fmt!r}") + + +def decode(schema: Schema, data: bytes, base: int = 0) -> dict: + return schema.decode(data, base=base) + + +def schema_from_json(payload: dict) -> Schema: + if not isinstance(payload, dict) or "fields" not in payload: + raise StructError("schema JSON must be an object with a 'fields' list") + fields = [] + for item in payload["fields"]: + try: + fields.append( + Field( + name=item["name"], + offset=int(item["offset"]), + fmt=item.get("fmt", "bytes"), + size=int(item.get("size", 0)), + enum=item.get("enum"), + note=item.get("note"), + count=int(item.get("count", 1)), + ) + ) + except KeyError as exc: + raise StructError(f"schema field missing key: {exc}") from exc + return Schema(name=payload.get("name", "schema"), fields=fields, size=int(payload.get("size", 0))) + + +def schema_to_json(schema: Schema) -> dict: + return { + "name": schema.name, + "size": schema.size, + "fields": [ + { + k: v + for k, v in { + "name": f.name, + "offset": f.offset, + "fmt": f.fmt, + "size": f.size or None, + "enum": f.enum, + "note": f.note, + }.items() + if v is not None + } + for f in schema.fields + ], + } + + +def render(schema: Schema, values: dict, indent: str = " ") -> str: + lines = [f"{schema.name} ({{base}}+{schema.size:#x} bytes)"] + width = max((len(f.name) for f in schema.fields), default=0) + for field in schema.fields: + value = values.get(field.name) + lines.append(f"{indent}{field.name:<{width}} @+0x{field.offset:03x} = {_format_value(field, value)}") + if field.note: + lines.append(f"{indent}{'':<{width}} ; {field.note}") + return "\n".join(lines) + + +def _format_value(field: Field, value) -> str: + if isinstance(value, bool): + return str(value) + if isinstance(value, float): + return f"{value:g}" + if isinstance(value, int): + text = f"{value:#x}" if not field.enum else f"{value} ({field.enum.get(str(value), '?')})" + if field.enum and field.fmt not in ("bool",): + text = f"{value:#x} ({field.enum.get(str(value), field.enum.get(value, '?'))})" + return text + if isinstance(value, (bytes, bytearray)): + head = bytes(value[:16]).hex(" ") + suffix = " ..." if len(value) > 16 else "" + return f"[{len(value)}B] {head}{suffix}" + return repr(value) |
