Files

595 lines
30 KiB
Python

#!/usr/bin/env python3
"""tools/struct_layout.py — the o32 struct layout engine, the field map, the canonical-type WRITER and the layout INVARIANT
(Phase 37 T3, 2026-09-12). Factored out of tools/type_census.py (T1) so the census, the rewrite engine (tools/restruct.py) and
the type writer compute ONE layout for one definition (R33: derive, don't re-derive).
WHAT IT KNOWS
* SCALARS / SDK_SIZES the o32 widths of every scalar spelling the tree uses, and the PsyQ aggregate sizes.
* parse_struct_body `struct/union … { … }` body text -> fields [dict(name, type, stars, dims, bits, nested, attrs)].
* Resolver type name -> (size, align, leaves[(off, width, sign)]) under the o32 rules (arrays, nested
aggregates, bit-fields, `packed`, per-field `aligned(N)`); cc1-validated on 5,283 file-scope
definitions / 29,248 named fields at T2 (`tools/restruct.py --audit-layouts`, 0 rejected).
* layout_hash / field_offsets the identity of a layout; the top-level named fields with their byte offsets (the audit's assertions).
* field_map a definition's {offset: (name, width, sign, type_text)} over its top-level scalar/pointer fields —
what the rewrite engine consults to turn `*(u16 *)(p + 0x3C)` into `p->unk3C`.
* render_struct THE WRITER: the final style of a canonical definition (owner decision 2, gate 1) —
typedef struct {
/* 0x00 */ s16 unk0;
/* 0x02 */ u8 pad2[0x2];
/* 0x04 */ s32 unk4;
} Unkstruct_80078E00; /* size = 0x8 */
`unk<HEX>` = a field the code ACCESSES whose meaning is unknown; `u8 pad<HEX>[n]` = bytes the code
never touches (Phase 38 knows there is nothing to name there); a field is NAMED only where the
record cites evidence, and the citation rides in its comment. One decision, applied everywhere.
* audit_definition THE INVARIANT (sotn's SYNC_FIELD, made derivable): every `unk<HEX>` / `pad<HEX>` field sits at
offset HEX, every `/* 0xNN */` offset comment equals the computed offset, and the `size = 0x..`
trailer equals sizeof. A drifted definition (a field inserted by hand) fails the census's --check
(T8 wires it into tools-health) — no ledger and no PC build needed.
"""
import hashlib
import json
import re
# ----------------------------------------------------------------------------------------------------------------------
# types and widths (o32)
# ----------------------------------------------------------------------------------------------------------------------
SCALARS = {
"u8": (1, "u"), "s8": (1, "s"), "char": (1, "s"), "signed char": (1, "s"), "unsigned char": (1, "u"),
"u16": (2, "u"), "s16": (2, "s"), "short": (2, "s"), "unsigned short": (2, "u"), "signed short": (2, "s"),
"short int": (2, "s"), "unsigned short int": (2, "u"),
"u32": (4, "u"), "s32": (4, "s"), "int": (4, "s"), "unsigned": (4, "u"), "unsigned int": (4, "u"), "signed": (4, "s"),
"signed int": (4, "s"), "long": (4, "s"), "unsigned long": (4, "u"), "long int": (4, "s"), "unsigned long int": (4, "u"),
"uint": (4, "u"), "uint32_t": (4, "u"), "int32_t": (4, "s"), "uint16_t": (2, "u"), "int16_t": (2, "s"),
"uint8_t": (1, "u"), "int8_t": (1, "s"),
"u64": (8, "u"), "s64": (8, "s"), "long long": (8, "s"), "unsigned long long": (8, "u"),
"f32": (4, "f"), "float": (4, "f"), "f64": (8, "f"), "double": (8, "f"),
"M2C_UNK": (4, "s"), "M2C_UNK8": (1, "s"), "M2C_UNK16": (2, "s"), "M2C_UNK32": (4, "s"), "M2C_UNK64": (8, "s"),
"void": (0, "v"),
}
# PsyQ layouts the SDK headers fix (include/psyq/*.h) — sizes only; used when a cast names them and for the MATRIX control
SDK_SIZES = {"MATRIX": 32, "SVECTOR": 8, "VECTOR": 16, "CVECTOR": 4, "DVECTOR": 4, "RECT": 8, "RECT32": 16,
"POLY_F3": 20, "POLY_F4": 24, "POLY_FT3": 32, "POLY_FT4": 40, "POLY_G3": 28, "POLY_G4": 36, "POLY_GT3": 40,
"POLY_GT4": 52, "LINE_F2": 16, "LINE_F3": 20, "LINE_F4": 24, "LINE_G2": 20, "LINE_G3": 28, "LINE_G4": 36,
"SPRT": 20, "SPRT_8": 16, "SPRT_16": 16, "TILE": 16, "TILE_1": 12, "TILE_8": 12, "TILE_16": 12,
"DR_TPAGE": 8, "DR_ENV": 64, "DRAWENV": 92, "DISPENV": 20, "TMD_PRIM": 60, "GsDOBJ2": 32, "GsCOORDINATE2": 80,
"GsRVIEW2": 36, "GsF_LIGHT": 16, "GsOT": 12, "GsOT_TAG": 4, "CdlLOC": 4, "CdlFILE": 20, "CdlFILTER": 4,
"CdlATV": 4, "CdlCB": 4}
def _norm_type(t):
t = re.sub(r"\b(const|volatile|register|extern|static)\b", "", t)
return re.sub(r"\s+", " ", t).strip()
def scalar_of(t):
"""(width, sign) for a scalar/pointer type text; None for an aggregate or unknown name."""
t = _norm_type(t)
if t.endswith("*"):
return (4, "p")
if t in SCALARS:
return SCALARS[t]
return None
# ----------------------------------------------------------------------------------------------------------------------
# the definitions: parse `struct/union … { … }` bodies and compute o32 layouts
# ----------------------------------------------------------------------------------------------------------------------
DEF_START = re.compile(r"(?<![\w.])(typedef\s+)?(struct|union|enum)\s*([A-Za-z_]\w*)?\s*(__attribute__\s*\(\([^)]*\)\)\s*)?\{")
FWD_DECL = re.compile(r"(?<![\w.])(struct|union|enum)\s+([A-Za-z_]\w*)\s*;")
ATTR_RX = re.compile(r"__attribute__\s*\(\(((?:[^()]|\([^()]*\))*)\)\)") # nested parens: aligned(4), packed
def _match_brace(text, open_idx):
depth = 0
i = open_idx
n = len(text)
while i < n:
c = text[i]
if c == "{":
depth += 1
elif c == "}":
depth -= 1
if depth == 0:
return i
i += 1
return -1
def _split_top(text, sep=";"):
"""Split on `sep` at brace/paren depth 0."""
out, depth, cur = [], 0, []
for c in text:
if c in "{(":
depth += 1
elif c in "})":
depth -= 1
if c == sep and depth == 0:
out.append("".join(cur))
cur = []
else:
cur.append(c)
if "".join(cur).strip():
out.append("".join(cur))
return out
def _split_declarators(decl):
"""`type a, *b, c[4]` -> (type, [(name, stars, dims, bits)])"""
parts = _split_top(decl, ",")
if not parts:
return None, []
first = parts[0].strip()
# bitfield? `s16 : 16` / `u8 x : 3`
mb = re.search(r":\s*(\d+)\s*$", first)
if mb:
left = first[:mb.start()].strip()
toks = left.split()
if len(toks) >= 2 and re.match(r"^[A-Za-z_]\w*$", toks[-1]) and toks[-1] not in SCALARS and toks[-2] not in ("struct", "union", "enum"):
return _norm_type(" ".join(toks[:-1])), [(toks[-1], 0, [], int(mb.group(1)))]
return _norm_type(left), [("", 0, [], int(mb.group(1)))]
# split the first part into type + declarator: the declarator is the trailing `*... name [dims]` or a function pointer
fp = re.match(r"^(.*?)\(\s*\*\s*([A-Za-z_]\w*)\s*\)\s*\(.*\)\s*((?:\[[^\]]*\])*)$", first)
decls = []
if fp:
base_type = "void *" # a function pointer is 4 bytes; the callee type is irrelevant to the layout
dims = re.findall(r"\[([^\]]*)\]", fp.group(3))
decls.append((fp.group(2), 1, dims, None))
for extra in parts[1:]:
e = extra.strip()
m2 = re.match(r"^(\**)\s*([A-Za-z_]\w*)\s*((?:\[[^\]]*\])*)$", e)
if m2:
decls.append((m2.group(2), len(m2.group(1)), re.findall(r"\[([^\]]*)\]", m2.group(3)), None))
return base_type, decls
m = re.match(r"^(.*?)\s*(\**)\s*([A-Za-z_]\w*)\s*((?:\[[^\]]*\])*)\s*$", first, re.S)
if not m:
return _norm_type(first), []
base_type = _norm_type(m.group(1))
decls.append((m.group(3), len(m.group(2)), re.findall(r"\[([^\]]*)\]", m.group(4)), None))
for extra in parts[1:]:
e = extra.strip()
m2 = re.match(r"^(\**)\s*([A-Za-z_]\w*)\s*((?:\[[^\]]*\])*)\s*(?::\s*(\d+))?$", e)
if m2:
decls.append((m2.group(2), len(m2.group(1)), re.findall(r"\[([^\]]*)\]", m2.group(3)),
int(m2.group(4)) if m2.group(4) else None))
return base_type, decls
def _eval_dim(d, consts):
d = d.strip()
if not d:
return None # flexible/unsized
try:
return int(eval(d, {"__builtins__": {}}, dict(consts))) # hex, sums, `0x20-0x10`, sizeof-free; the tree's dims are literals
except Exception:
return None
def parse_struct_body(body, consts=None):
"""Fields of one struct/union body text (the text between the braces). Nested definitions are parsed recursively.
Returns [dict(name, type, stars, dims, bits, nested)] in order."""
fields = []
consts = consts or {}
i, n = 0, len(body)
while i < n:
# a nested struct/union body?
m = DEF_START.search(body, i)
stmt_end = body.find(";", i)
if m and (stmt_end == -1 or m.start() < stmt_end):
close = _match_brace(body, m.end() - 1)
if close == -1:
break
inner = body[m.end():close]
# the declarators after the nested body up to `;`
j = body.find(";", close)
tail = body[close + 1:j if j != -1 else n]
nested = dict(kind=m.group(2), tag=m.group(3), fields=parse_struct_body(inner, consts) if m.group(2) != "enum" else [],
attrs=(m.group(4) or "") + " ".join(ATTR_RX.findall(tail)))
tail = ATTR_RX.sub("", tail).strip()
decls = []
if tail:
for part in _split_top(tail, ","):
e = part.strip()
m2 = re.match(r"^(\**)\s*([A-Za-z_]\w*)\s*((?:\[[^\]]*\])*)\s*(?::\s*(\d+))?$", e)
if m2:
decls.append((m2.group(2), len(m2.group(1)), re.findall(r"\[([^\]]*)\]", m2.group(3)),
int(m2.group(4)) if m2.group(4) else None))
if not decls:
decls = [("", 0, [], None)] # an anonymous member (a union inside a struct)
for (nm, stars, dims, bits) in decls:
fields.append(dict(name=nm, type=(m.group(2) + " " + (m.group(3) or "")).strip(), stars=stars,
dims=[_eval_dim(d, consts) for d in dims], bits=bits, nested=nested))
i = (j + 1) if j != -1 else n
continue
if stmt_end == -1:
break
stmt = body[i:stmt_end].strip()
i = stmt_end + 1
if not stmt:
continue
fattrs = " ".join(ATTR_RX.findall(stmt))
stmt = ATTR_RX.sub("", stmt)
base_type, decls = _split_declarators(stmt)
if base_type is None:
continue
for (nm, stars, dims, bits) in decls:
fields.append(dict(name=nm, type=base_type, stars=stars, dims=[_eval_dim(d, consts) for d in dims], bits=bits, nested=None,
attrs=fattrs or None))
return fields
class Resolver:
"""Type name -> (size, align, leaves) under the o32 ABI. Scope: a TU's own definitions first, then the shared headers, then the SDK."""
def __init__(self, defs_by_name):
self.defs = defs_by_name # name -> definition record (tag or typedef name)
self.cache = {}
self.stack = set()
def layout(self, type_text, stars=0, dims=(), bits=None):
"""(size, align, leaves) of one field's type; leaves = [(off, width, sign)]; None when unknown."""
if stars:
sz, al, leaves = 4, 4, [(0, 4, "p")]
else:
t = _norm_type(type_text)
sc_ = scalar_of(t)
if sc_:
w, s = sc_
if w == 0:
return None
sz, al, leaves = w, w, [(0, w, s)]
else:
r = self.resolve_name(t)
if r is None:
return None
sz, al, leaves = r
for d in reversed(list(dims)):
if d is None:
d = 0
leaves = [(k * sz + off, w, s) for k in range(d) for (off, w, s) in leaves]
sz = sz * d
return sz, al, leaves
def resolve_name(self, name):
name = _norm_type(name)
if name in self.cache:
return self.cache[name]
if name in self.stack:
return None
d = self.defs.get(name)
if d is None:
m = re.match(r"^(struct|union|enum)\s+(\w+)$", name)
if m and m.group(1) == "enum":
return (4, 4, [(0, 4, "s")])
if m and m.group(2) in self.defs and self.defs[m.group(2)].get("kind") == m.group(1):
d = self.defs[m.group(2)]
elif name in SDK_SIZES:
sz = SDK_SIZES[name]
r = (sz, 4, [(0, 4, "s")] * (sz // 4) if sz % 4 == 0 else [(0, sz, "b")])
self.cache[name] = r
return r
else:
return None
if d.get("kind") == "enum":
return (4, 4, [(0, 4, "s")])
if d.get("alias_of"): # `typedef T Name;` — a scalar/pointer alias or an alias of another aggregate
self.stack.add(name)
r = self.layout(d["alias_of"], d.get("alias_stars", 0), d.get("alias_dims", ()))
self.stack.discard(name)
self.cache[name] = r
return r
self.stack.add(name)
r = self.layout_of_fields(d["fields"], d["kind"], packed=("packed" in (d.get("attrs") or "")))
self.stack.discard(name)
self.cache[name] = r
return r
def layout_of_fields(self, fields, kind, packed=False):
off, maxal, leaves, size = 0, 1, [], 0
bit_off = 0
unknown = False
for f in fields:
if f.get("nested"):
nd = f["nested"]
r = self.layout_of_fields(nd["fields"], nd["kind"], packed=("packed" in (nd.get("attrs") or ""))) if nd["kind"] != "enum" else (4, 4, [(0, 4, "s")])
else:
r = self.layout(f["type"], f["stars"], f["dims"])
if r is None:
unknown = True
r = (0, 1, [])
fsz, fal, fleaves = r
if f.get("dims") and f["nested"]:
for d in reversed(f["dims"]):
d = d or 0
fleaves = [(k * fsz + o, w, s) for k in range(d) for (o, w, s) in fleaves]
fsz = fsz * d
fa = f.get("attrs") or ""
ma = re.search(r"aligned\s*\(\s*(\d+)\s*\)", fa)
if packed:
fal = 1
if ma:
fal = max(fal, int(ma.group(1)))
if f.get("bits") is not None:
# bitfields: pack into the base type's storage unit; an anonymous `s16 : 16` is a pad of the base width
w = fsz or 4
if bit_off == 0:
off = (off + fal - 1) // fal * fal
unit_off = off
self._unit = (unit_off, w)
unit_off, uw = self._unit
if bit_off + f["bits"] > uw * 8:
off = unit_off + uw
off = (off + fal - 1) // fal * fal
unit_off = off
self._unit = (unit_off, w)
bit_off = 0
if f["name"]:
leaves.append((unit_off, w, "bf"))
bit_off += f["bits"]
maxal = max(maxal, fal)
end = unit_off + w
if bit_off >= uw * 8:
bit_off = 0
off = end
else:
off = unit_off
size = max(size, end)
if kind == "union":
off = 0
continue
bit_off = 0
if kind == "union":
leaves.extend((0 + o, w, s) for (o, w, s) in fleaves)
size = max(size, fsz)
maxal = max(maxal, fal)
continue
off = (off + fal - 1) // fal * fal
leaves.extend((off + o, w, s) for (o, w, s) in fleaves)
off += fsz
size = max(size, off)
maxal = max(maxal, fal)
size = (size + maxal - 1) // maxal * maxal
if unknown:
return None
return size, maxal, sorted(set(leaves))
def layout_hash(lay):
if lay is None:
return None
size, al, leaves = lay
return hashlib.sha1(json.dumps([size, leaves]).encode()).hexdigest()[:12]
# ----------------------------------------------------------------------------------------------------------------------
# the field map (what a rewrite consults) and the naming invariant (what the census asserts)
# ----------------------------------------------------------------------------------------------------------------------
def mask_comments(text):
"""Same-length masking of /* */ and // comments (the census masks through share_census.mask_text; this is the standalone
equivalent for a definition's own text — parse_struct_body expects MASKED text, never raw)."""
return re.sub(r"/\*.*?\*/|//[^\n]*", lambda m: re.sub(r"[^\n]", " ", m.group(0)), text, flags=re.S)
UNK_RX = re.compile(r"^unk([0-9A-F]+)$")
PAD_RX = re.compile(r"^pad([0-9A-F]+)$")
# the LEADING offset comment of the final style: ` /* 0x0004 */ s32 unk4;` — a TRAILING legacy comment (`s32 unk8; /* 0x8 */`) is not judged
# (it would otherwise be paired with the NEXT field: the first audit run read 1,013 "violations" in the legacy header that way)
OFF_COMMENT_RX = re.compile(r"^[ \t]*/\*\s*0x([0-9A-Fa-f]+)\s*\*/[ \t]*([^;/\n]*;)", re.M)
SIZE_TRAILER_RX = re.compile(r"/\*\s*size\s*=\s*0x([0-9A-Fa-f]+)\s*\*/")
TYPE_OF = {(1, "u"): "u8", (1, "s"): "s8", (2, "u"): "u16", (2, "s"): "s16", (4, "u"): "u32", (4, "s"): "s32",
(8, "u"): "u64", (8, "s"): "s64", (4, "f"): "f32", (8, "f"): "f64"}
def field_offsets(res, d):
"""Top-level NAMED, non-bitfield fields with their byte offsets under the engine's rules — the audit's assertions.
(Moved from tools/restruct.py T2 form.)"""
out, off, kind = [], 0, d["kind"]
packed = "packed" in (d.get("attrs") or "")
for f in d["fields"]:
if f.get("nested"):
nd = f["nested"]
r = res.layout_of_fields(nd["fields"], nd["kind"], packed=("packed" in (nd.get("attrs") or ""))) if nd["kind"] != "enum" else (4, 4, [])
else:
r = res.layout(f["type"], f["stars"], f["dims"])
if r is None:
return out
fsz, fal, _ = r
if f.get("dims") and f["nested"]:
for dd in reversed(f["dims"]):
fsz *= (dd or 0)
fa = f.get("attrs") or ""
ma = re.search(r"aligned\s*\(\s*(\d+)\s*\)", fa)
if packed:
fal = 1
if ma:
fal = max(fal, int(ma.group(1)))
if f.get("bits") is not None:
return out # bit-field layouts are not asserted (the unit rule is compiler-specific); stop here
if kind == "union":
if f["name"]:
out.append((f["name"], 0))
continue
off = (off + fal - 1) // fal * fal
if f["name"]:
out.append((f["name"], off))
off += fsz
return out
def field_map(res, d):
"""{offset: dict(name, width, sign, type, stars, dims)} for the top-level SCALAR / POINTER fields of a struct definition
(an aggregate member is listed with sign 'agg' and its size — the engine never rewrites into an aggregate without an
explicit member path). None when a field's layout is unknown."""
out = {}
offs = dict(field_offsets(res, d))
for f in d["fields"]:
if not f["name"] or f["name"] not in offs or f.get("bits") is not None:
continue
if f.get("nested"):
nd = f["nested"]
r = res.layout_of_fields(nd["fields"], nd["kind"], packed=("packed" in (nd.get("attrs") or ""))) if nd["kind"] != "enum" else (4, 4, [(0, 4, "s")])
if r is None:
return None
out[offs[f["name"]]] = dict(name=f["name"], width=r[0], sign="agg", type=f["type"], stars=0, dims=f.get("dims") or [])
continue
if f.get("dims"):
r = res.layout(f["type"], f["stars"], f["dims"])
if r is None:
return None
out[offs[f["name"]]] = dict(name=f["name"], width=r[0], sign="agg", type=f["type"], stars=f["stars"], dims=f["dims"])
continue
if f["stars"]:
out[offs[f["name"]]] = dict(name=f["name"], width=4, sign="p", type=f["type"], stars=f["stars"], dims=[])
continue
w = scalar_of(f["type"])
if w is None:
r = res.layout(f["type"])
if r is None:
return None
out[offs[f["name"]]] = dict(name=f["name"], width=r[0], sign="agg", type=f["type"], stars=0, dims=[])
continue
out[offs[f["name"]]] = dict(name=f["name"], width=w[0], sign=w[1], type=f["type"], stars=0, dims=[])
return out
def leaf_at(lay, off, width, sign):
"""True when the layout's leaves carry (off, width) with a compatible sign — a store's signedness leaves no byte, so a
store may hit a field of the other sign; a load may not. `sign` None = width only."""
if lay is None:
return False
for (o, w, s) in lay[2]:
if o == off and w == width:
if sign is None or s == sign or {s, sign} <= {"s", "u"} and sign == "store":
return True
if sign in ("s", "u") and s in ("s", "u") and sign != s:
continue
if s == sign:
return True
return False
def render_struct(name, entries, size=None, kind="struct", indent=" ", comment=None, tag=None):
"""THE WRITER. entries = [(off, type_text, field_name, dims_or_None, note_or_None)] sorted by offset, non-overlapping; the gaps
become `u8 pad<HEX>[n]`; `size` (else the end of the last field) rounds to the alignment of the widest scalar; the trailer
names the size. Returns the text (ending with a newline). Refuses overlaps and misaligned scalars (ValueError)."""
lines = [f"typedef {kind} {tag + ' ' if tag else ''}{{"]
if comment:
lines.append(f"{indent}/* {comment} */")
cur, maxal = 0, 1
for (off, ttext, fname, dims, note) in sorted(entries, key=lambda e: e[0]):
if off < cur:
raise ValueError(f"render_struct {name}: {fname} at {off:#x} overlaps the previous field ending at {cur:#x}")
sc_ = scalar_of(ttext)
w = sc_[0] if sc_ else None
al = (8 if w == 8 else min(w, 4)) if w else 4
if w and off % al:
raise ValueError(f"render_struct {name}: {fname} ({ttext}) at {off:#x} is not {al}-aligned")
if off > cur:
lines.append(f"{indent}/* {cur:#06x} */ u8 pad{cur:X}[{off - cur:#x}];")
dtxt = "".join(f"[{d:#x}]" if isinstance(d, int) else f"[{d}]" for d in (dims or []))
lines.append(f"{indent}/* {off:#06x} */ {ttext} {fname}{dtxt};" + (f" /* {note} */" if note else ""))
n = 1
for d in (dims or []):
n *= (d if isinstance(d, int) else 1)
cur = off + (w or 0) * n if w else off + 4 * n
maxal = max(maxal, al)
end = size if size is not None else (cur + maxal - 1) // maxal * maxal
if end < cur:
raise ValueError(f"render_struct {name}: size {end:#x} is smaller than the last field's end {cur:#x}")
if end > cur:
lines.append(f"{indent}/* {cur:#06x} */ u8 pad{cur:X}[{end - cur:#x}];")
lines.append(f"}} {name}; /* size = {end:#x} */")
return "\n".join(lines) + "\n"
def entries_from_layout(layout, ctypes_by_off=None, prefer_pointee=None):
"""The struct map's layout [(off, w, sign, n)] -> render_struct entries [(off, type, unk<HEX>, None, None)], skipping what a
struct cannot hold: overlaps (the earlier/wider-supported entry wins by count), misaligned widths, unknown widths. Returns
(entries, skipped[(off, w, sign, reason)]). A pointer field takes the sites' pointee when they agree, else `void *`."""
entries, skipped, cur = [], [], 0
for (off, w, sg, n) in sorted(layout, key=lambda x: (x[0], -x[3])):
if w is None or w == 0:
skipped.append((off, w, sg, "width"))
continue
if off < cur:
skipped.append((off, w, sg, "overlap"))
continue
al = 8 if w == 8 else min(w, 4)
if off % al:
skipped.append((off, w, sg, "misaligned"))
continue
if sg == "p":
pt = {}
for c in (ctypes_by_off or {}).get(off, []):
if c and c.count("*") >= 2:
k = c.rstrip("* ").strip() + " *"
pt[k] = pt.get(k, 0) + 1
t = (prefer_pointee or {}).get(off) or (next(iter(pt)) if len(pt) == 1 else "void *")
else:
t = TYPE_OF.get((w, sg))
if t is None:
skipped.append((off, w, sg, "sign"))
continue
entries.append((off, t, f"unk{off:X}", None, None))
cur = off + w
return entries, skipped
def audit_definition(d, res, text=None):
"""THE INVARIANT on one definition record (type_census.find_definitions shape): [violations as strings]; [] = OK.
(1) every `unk<HEX>`/`pad<HEX>` top-level field sits at offset HEX; (2) when `text` (the definition's raw source text) is
given, every `/* 0xNN */ … ;` field comment equals the computed offset of the field it annotates and the `size = 0x..`
trailer equals sizeof. Fields with other names are not judged by (1) — the record's evidence-cited names carry a comment."""
out = []
offs = field_offsets(res, d)
name = (d.get("names") or [d.get("tag") or "?"])[0]
for (fname, off) in offs:
for rx, what in ((UNK_RX, "unk"), (PAD_RX, "pad")):
m = rx.match(fname)
if m and int(m.group(1), 16) != off:
out.append(f"{name}.{fname}: named for offset {int(m.group(1), 16):#x} but laid out at {off:#x}")
if text:
by_name = dict(offs)
for m in OFF_COMMENT_RX.finditer(text):
want = int(m.group(1), 16)
decl = m.group(2)
first = decl.split(",")[0] # `u16 cx, cy, cz;` — the comment names the FIRST declarator's offset
m2 = re.search(r"([A-Za-z_]\w*)\s*(?:\[[^\]]*\])*\s*(?::\s*\d+)?\s*[;,]?\s*$", first.rstrip(";") + ";")
if not m2:
continue
fname = m2.group(1)
if fname in by_name and by_name[fname] != want:
out.append(f"{name}.{fname}: comment says {want:#x}, laid out at {by_name[fname]:#x}")
ms = SIZE_TRAILER_RX.search(text)
lay = res.layout_of_fields(d["fields"], d["kind"], packed=("packed" in (d.get("attrs") or "")))
if ms and lay and int(ms.group(1), 16) != lay[0]:
out.append(f"{name}: trailer says size {int(ms.group(1), 16):#x}, sizeof is {lay[0]:#x}")
return out
def selftest():
"""The writer round-trips through the parser and the invariant holds; a drifted definition is caught; a hand layout agrees."""
checks = []
entries = [(0, "s16", "unk0", None, None), (4, "s32", "unk4", None, None), (8, "u8", "unk8", [4], None), (0x10, "void *", "unk10", None, None)]
txt = render_struct("Unkstruct_TEST", entries)
checks.append(("writer emits pad2", "u8 pad2[0x2]" in txt))
checks.append(("writer trailer", "/* size = 0x14 */" in txt))
body = mask_comments(txt)[txt.index("{") + 1:txt.rindex("}")]
fields = parse_struct_body(body)
d = dict(kind="struct", tag=None, names=["Unkstruct_TEST"], fields=fields, attrs="")
res = Resolver({"Unkstruct_TEST": d})
lay = res.resolve_name("Unkstruct_TEST")
checks.append(("parsed size 0x14", lay is not None and lay[0] == 0x14))
checks.append(("field_map", field_map(res, d)[4]["name"] == "unk4" and field_map(res, d)[8]["sign"] == "agg"))
checks.append(("audit OK", audit_definition(d, res, txt) == []))
# a drifted definition: a field inserted by hand shifts unk4 to 0x8 -> the invariant names it
drift = txt.replace("/* 0x0002 */ u8 pad2[0x2];", "/* 0x0002 */ u8 pad2[0x2];\n s32 inserted;")
fields2 = parse_struct_body(mask_comments(drift)[drift.index("{") + 1:drift.rindex("}")])
d2 = dict(kind="struct", tag=None, names=["Unkstruct_TEST"], fields=fields2, attrs="")
v = audit_definition(d2, Resolver({"Unkstruct_TEST": d2}), drift)
checks.append(("drift caught", any("unk4" in x for x in v)))
# the layout skip rules: an overlap, a misaligned half, an unknown width
ents, skipped = entries_from_layout([(0, 4, "s", 5), (2, 2, "u", 1), (5, 2, "u", 3), (8, 2, "u", 2), (12, None, None, 1)])
checks.append(("overlap/misaligned/width skipped", [s[3] for s in skipped] == ["overlap", "misaligned", "width"] and [e[0] for e in ents] == [0, 8]))
checks.append(("leaf_at", leaf_at(lay, 4, 4, "s") and not leaf_at(lay, 4, 2, "s") and not leaf_at(lay, 6, 2, "u")))
try:
render_struct("X", [(0, "s32", "a", None, None), (2, "s16", "b", None, None)])
checks.append(("overlap refused", False))
except ValueError:
checks.append(("overlap refused", True))
bad = [c for c, ok in checks if not ok]
print(f"struct_layout --selftest: {len(checks) - len(bad)}/{len(checks)} OK" + (f"; FAILED: {bad}" if bad else ""))
return 0 if not bad else 1
if __name__ == "__main__":
import sys
sys.exit(selftest() if "--selftest" in sys.argv else 0)