#!/usr/bin/env python3 """ heat2.py -- self-contained Blaze Heat2 TDF encoder/decoder + Fire2 framing. CLEAN ROOM PROVENANCE --------------------- Everything here was derived from: * the wire bytes of our own FIFA17 client's first Blaze RPC (fifa17-recon/captures/blaze/blaze_fire2_37161.bin), and * our own decoder (decode_fire2.py) written against those bytes. Complex-type layouts that the capture does NOT exercise (list/map/union/ varintlist/objtype/objid/float) are marked UNVERIFIED below; they are consistent with independent third-party clean-room BlazeSDK-15.x reimplementations (the `tdf` crate cloned in this scratchpad), which were used only as a cross-check of *structure*, never copied. NO EA/FIFA leaked source was consulted. VALIDATED RULES (byte-exact round-trip against the 219-byte capture) -------------------------------------------------------------------- Fire2 frame header, 16 bytes big-endian: [0:4] u32 payload length (bytes after the header) [4:6] u16 always 0 (observed) [6:8] u16 component [8:10] u16 command [10:12]u16 error / msgId [12] u8 msgType (0x01 ping, 0x02 request, 0x03 pong/response) [13:16]3 reserved bytes (observed 00 00 00) Heat2 field = 3-byte packed tag + 1 type byte + value. TAG PACKING (validated): Take the 4-char label, right-pad with spaces to exactly 4 chars, truncate to 4. Each char c -> 6-bit code (ord(c) - 0x20) & 0x3F (so ' ' -> 0). The four 6-bit codes are concatenated MSB-first into 24 bits = 3 bytes: b0 = c0<<2 | c1>>4 b1 = (c1 & 0x0F)<<4 | c2>>2 b2 = (c2 & 0x03)<<6 | c3 Decode is the exact inverse; code 0 decodes to ' ' and trailing spaces are stripped, so "ENV" round-trips as "ENV" (encoded as "ENV "). VARINT (validated): First byte carries only 6 data bits (mask 0x3F); bit 0x80 = "more". Bit 0x40 of the first byte is the sign/negative flag (UNVERIFIED - never set in our capture; we encode non-negative values only by default). Every following byte carries 7 data bits (mask 0x7F) with bit 0x80 = more. Little-endian group order: first byte = least significant 6 bits, then 7 bits per byte at shifts 6, 13, 20, 27, ... Canonical form: emit the shortest sequence; value < 0x40 is one byte. e.g. LANG = 0x656E5553 ("enUS") -> 93 d5 f2 d6 0c STRING (validated): varint length INCLUDING the NUL terminator, then that many bytes, the last of which is 0x00. Empty string = varint 1 + b"\\x00". STRUCT / group (validated): type byte 0x03, then the member fields, then a single 0x00 terminator byte. No group-start marker byte. The top-level payload is NOT terminated (it is delimited by the Fire2 length). FIELD ORDER (validated): Members are serialized in ascending order of the *packed 3-byte tag* (equivalently ascending by the space-padded label under this 6-bit packing). Observed: CDAT int STRING -> str (no trailing NUL) or bytes BLOB -> bytes STRUCT -> dict as above LIST -> (elem_type, [value, ...]) MAP -> (key_type, val_type, [(k, v), ...]) UNION -> (active_key:int, (tag, type, value) | None) VARLIST -> [int, ...] OBJTYPE -> (component, type) OBJID -> (component, type, id) FLOAT -> float """ from __future__ import annotations import struct from collections import OrderedDict # ---------------------------------------------------------------- types INT = 0x00 STRING = 0x01 BLOB = 0x02 STRUCT = 0x03 LIST = 0x04 MAP = 0x05 UNION = 0x06 VARLIST = 0x07 OBJTYPE = 0x08 OBJID = 0x09 FLOAT = 0x0A TYPE_NAMES = { INT: "int", STRING: "string", BLOB: "blob", STRUCT: "struct", LIST: "list", MAP: "map", UNION: "union", VARLIST: "varintlist", OBJTYPE: "objtype", OBJID: "objid", FLOAT: "float", } UNION_UNSET = 0x7F # UNVERIFIED (not present in capture) # ---------------------------------------------------------------- tags def encode_tag(label) -> bytes: """4-char label -> 3 packed bytes. Shorter labels are space padded.""" if isinstance(label, bytes): label = label.decode("ascii") s = (label + " ")[:4] c = [(ord(ch) - 0x20) & 0x3F for ch in s] return bytes(( (c[0] << 2) | (c[1] >> 4), ((c[1] & 0x0F) << 4) | (c[2] >> 2), ((c[2] & 0x03) << 6) | c[3], )) def decode_tag(b: bytes) -> str: """3 packed bytes -> label with trailing padding stripped.""" a, b1, c = b[0], b[1], b[2] v = ( (a >> 2) & 0x3F, ((a & 0x03) << 4) | ((b1 >> 4) & 0x0F), ((b1 & 0x0F) << 2) | ((c >> 6) & 0x03), c & 0x3F, ) return "".join(chr(x + 0x20) if x else " " for x in v).rstrip() def tag_key(label) -> bytes: """Sort key enforcing Blaze's ascending-tag member ordering.""" return encode_tag(label) # ---------------------------------------------------------------- varint def encode_varint(value: int) -> bytes: """Heat2 varint: 6 data bits in byte 0 (0x80=more), 7 bits thereafter.""" neg = value < 0 v = -value if neg else value first = v & 0x3F v >>= 6 if neg: first |= 0x40 # UNVERIFIED sign convention if v == 0: return bytes((first,)) out = bytearray((first | 0x80,)) while v >= 0x80: out.append((v & 0x7F) | 0x80) v >>= 7 out.append(v) return bytes(out) def decode_varint(buf: bytes, i: int): """-> (value, next_index)""" b = buf[i] i += 1 val = b & 0x3F neg = bool(b & 0x40) if b & 0x80: shift = 6 while True: b = buf[i] i += 1 val |= (b & 0x7F) << shift shift += 7 if not (b & 0x80): break return (-val if neg else val), i # ---------------------------------------------------------------- encoder def _enc_value(typ: int, value, out: bytearray) -> None: if typ == INT: out += encode_varint(int(value)) elif typ == STRING: raw = value.encode("utf-8") if isinstance(value, str) else bytes(value) raw = raw.rstrip(b"\x00") out += encode_varint(len(raw) + 1) out += raw out += b"\x00" elif typ == BLOB: raw = bytes(value) out += encode_varint(len(raw)) out += raw elif typ == STRUCT: _enc_struct_body(value, out) out += b"\x00" elif typ == LIST: # UNVERIFIED etype, items = value out.append(etype & 0xFF) out += encode_varint(len(items)) for it in items: _enc_value(etype, it, out) elif typ == MAP: # UNVERIFIED ktype, vtype, items = value out.append(ktype & 0xFF) out.append(vtype & 0xFF) out += encode_varint(len(items)) for k, v in items: _enc_value(ktype, k, out) _enc_value(vtype, v, out) elif typ == UNION: # UNVERIFIED key, member = value out.append(key & 0xFF) if key != UNION_UNSET and member is not None: mtag, mtype, mval = member out += encode_tag(mtag) out.append(mtype & 0xFF) _enc_value(mtype, mval, out) elif typ == VARLIST: # UNVERIFIED out += encode_varint(len(value)) for n in value: out += encode_varint(int(n)) elif typ == OBJTYPE: # UNVERIFIED comp, t = value out += encode_varint(comp) out += encode_varint(t) elif typ == OBJID: # UNVERIFIED comp, t, oid = value out += encode_varint(comp) out += encode_varint(t) out += encode_varint(oid) elif typ == FLOAT: # UNVERIFIED out += struct.pack(">f", float(value)) else: raise ValueError("cannot encode unknown TDF type 0x%02x" % typ) def _enc_struct_body(fields, out: bytearray) -> None: """Serialize members in ascending packed-tag order (Blaze requirement).""" if isinstance(fields, dict): items = list(fields.items()) else: # allow [(tag, (type, value)), ...] items = list(fields) items.sort(key=lambda kv: tag_key(kv[0])) for tag, tv in items: typ, val = tv out += encode_tag(tag) out.append(typ & 0xFF) _enc_value(typ, val, out) def encode_tdf(fields) -> bytes: """Serialize a top-level TDF struct body (no trailing 0x00 terminator).""" out = bytearray() _enc_struct_body(fields, out) return bytes(out) # convenient aliases build_tdf = encode_tdf encode_struct = encode_tdf # ---------------------------------------------------------------- decoder def _dec_value(buf: bytes, i: int, typ: int): if typ == INT: return decode_varint(buf, i) if typ == STRING: ln, i = decode_varint(buf, i) raw = buf[i:i + ln] i += ln return raw.rstrip(b"\x00").decode("utf-8", "replace"), i if typ == BLOB: ln, i = decode_varint(buf, i) return bytes(buf[i:i + ln]), i + ln if typ == STRUCT: return _dec_struct_body(buf, i, terminated=True) if typ == LIST: etype = buf[i]; i += 1 n, i = decode_varint(buf, i) items = [] for _ in range(n): v, i = _dec_value(buf, i, etype) items.append(v) return (etype, items), i if typ == MAP: ktype = buf[i]; i += 1 vtype = buf[i]; i += 1 n, i = decode_varint(buf, i) items = [] for _ in range(n): k, i = _dec_value(buf, i, ktype) v, i = _dec_value(buf, i, vtype) items.append((k, v)) return (ktype, vtype, items), i if typ == UNION: key = buf[i]; i += 1 if key == UNION_UNSET: return (key, None), i mtag = decode_tag(buf[i:i + 3]); mtype = buf[i + 3]; i += 4 mval, i = _dec_value(buf, i, mtype) return (key, (mtag, mtype, mval)), i if typ == VARLIST: n, i = decode_varint(buf, i) out = [] for _ in range(n): v, i = decode_varint(buf, i) out.append(v) return out, i if typ == OBJTYPE: c, i = decode_varint(buf, i) t, i = decode_varint(buf, i) return (c, t), i if typ == OBJID: c, i = decode_varint(buf, i) t, i = decode_varint(buf, i) o, i = decode_varint(buf, i) return (c, t, o), i if typ == FLOAT: return struct.unpack(">f", buf[i:i + 4])[0], i + 4 raise ValueError("cannot decode unknown TDF type 0x%02x at %d" % (typ, i)) def _dec_struct_body(buf: bytes, i: int, terminated: bool, end: int = None): """Read fields until 0x00 terminator (nested) or `end` (top level).""" if end is None: end = len(buf) fields = OrderedDict() while i < end: if terminated and buf[i] == 0x00: i += 1 break tag = decode_tag(buf[i:i + 3]) typ = buf[i + 3] i += 4 val, i = _dec_value(buf, i, typ) fields[tag] = (typ, val) return fields, i def decode_tdf(payload: bytes): """Decode a top-level TDF payload -> OrderedDict {tag: (type, value)}.""" fields, _ = _dec_struct_body(payload, 0, terminated=False) return fields # ---------------------------------------------------------------- Fire2 FIRE2_HEADER_LEN = 16 MSG_PING = 0x01 MSG_REQUEST = 0x02 MSG_RESPONSE = 0x03 # also seen as pong MSG_NOTIFY = 0x04 # UNVERIFIED MSG_ERROR = 0x05 # UNVERIFIED def build_fire2_frame(component: int, command: int, msgType: int, msgId: int, tdf_bytes: bytes) -> bytes: """16-byte big-endian Fire2 header + TDF payload.""" tdf_bytes = bytes(tdf_bytes) hdr = struct.pack(">IHHHHB3s", len(tdf_bytes), 0, component & 0xFFFF, command & 0xFFFF, msgId & 0xFFFF, msgType & 0xFF, b"\x00\x00\x00") return hdr + tdf_bytes def parse_fire2_frame(data: bytes): """-> (dict header, bytes payload). Raises if the buffer is short.""" if len(data) < FIRE2_HEADER_LEN: raise ValueError("short Fire2 frame") (ln, zero, comp, cmd, msgid, mtype, reserved) = struct.unpack( ">IHHHHB3s", data[:FIRE2_HEADER_LEN]) payload = data[FIRE2_HEADER_LEN:FIRE2_HEADER_LEN + ln] if len(payload) != ln: raise ValueError("truncated Fire2 payload: want %d have %d" % (ln, len(payload))) hdr = { "length": ln, "zero": zero, "component": comp, "command": cmd, "msgId": msgid, "msgType": mtype, "reserved": reserved, } return hdr, payload def decode_fire2(data: bytes): """-> (header dict, decoded TDF OrderedDict)""" hdr, payload = parse_fire2_frame(data) return hdr, decode_tdf(payload) def encode_fire2(hdr: dict, fields) -> bytes: """Inverse of decode_fire2 (uses hdr's component/command/msgType/msgId).""" return build_fire2_frame(hdr["component"], hdr["command"], hdr["msgType"], hdr["msgId"], encode_tdf(fields)) # ---------------------------------------------------------------- pretty def dump(fields, depth: int = 0) -> str: pad = " " * depth lines = [] for tag, (typ, val) in fields.items(): tn = TYPE_NAMES.get(typ, "0x%02x" % typ) if typ == STRUCT: lines.append("%s%s (struct) {" % (pad, tag)) lines.append(dump(val, depth + 1)) lines.append("%s}" % pad) elif typ == BLOB: lines.append("%s%s (blob[%d]) = %s" % (pad, tag, len(val), val.hex())) else: lines.append("%s%s (%s) = %r" % (pad, tag, tn, val)) return "\n".join(lines) # ---------------------------------------------------------------- self-test CAPTURE = ("/home/alex/Documents/OpenFUT/fifa17-recon/captures/blaze/" "blaze_fire2_37161.bin") def _selftest(path: str = CAPTURE) -> bool: ok = True # unit: tag packing for lbl in ("CDAT", "CINF", "FCCR", "LADD", "ENV", "LOC", "BSDK", "PTVR"): enc = encode_tag(lbl) assert decode_tag(enc) == lbl, (lbl, enc.hex()) assert encode_tag("CDAT") == bytes.fromhex("8e4874"), encode_tag("CDAT").hex() assert encode_tag("ENV") == bytes.fromhex("96ed80"), encode_tag("ENV").hex() assert encode_tag("LADD") == bytes.fromhex("b21924"), encode_tag("LADD").hex() # unit: varint assert encode_varint(0) == b"\x00" assert encode_varint(4) == b"\x04" assert encode_varint(0x3F) == b"\x3f" assert encode_varint(0x40) == bytes.fromhex("8001") assert encode_varint(0x656E5553) == bytes.fromhex("93d5f2d60c") for n in (0, 1, 63, 64, 127, 128, 8191, 0x656E5553, 2**40, 2**63 - 1): v, j = decode_varint(encode_varint(n), 0) assert v == n and j == len(encode_varint(n)), n # round trip the real capture original = open(path, "rb").read() hdr, payload = parse_fire2_frame(original) fields = decode_tdf(payload) re_payload = encode_tdf(fields) re_frame = encode_fire2(hdr, fields) print("header:", hdr) print(dump(fields)) print() print("payload %d -> %d bytes" % (len(payload), len(re_payload))) print("frame %d -> %d bytes" % (len(original), len(re_frame))) if re_frame == original: print("ROUND-TRIP: PASS (byte-identical, %d bytes)" % len(original)) else: ok = False print("ROUND-TRIP: FAIL") n = min(len(re_frame), len(original)) for k in range(n): if re_frame[k] != original[k]: print(" first diff at 0x%04x: got %02x want %02x" % (k, re_frame[k], original[k])) print(" got %s" % re_frame[max(0, k - 8):k + 16].hex()) print(" want %s" % original[max(0, k - 8):k + 16].hex()) break else: print(" length differs only: %d vs %d" % (len(re_frame), len(original))) return ok if __name__ == "__main__": import sys p = sys.argv[1] if len(sys.argv) > 1 else CAPTURE raise SystemExit(0 if _selftest(p) else 1)