#!/usr/bin/env python3
"""
gen_cst.py -- craft a full RSDP->RSDT->[FACP->FACS+DSDT, MADT] chain carrying a
crafted per-processor _CST package, for the acpi_cstate_machdep.c pass-2
verification (DF-5619 / DF-5620).

Injection model (identical to the proven DF-5483/DF-5583 rig):
  - guest booted with loader kenv hint.acpi.0.rsdp=0x40000000
    (sys/platform/pc64/acpica/acpi_sdt.c -- hinted RSDP accepted with NO
    signature and NO checksum validation)
  - QEMU -device loader,file=<blob>,addr=0x40000000,force-raw=on plants the
    chain in guest RAM before firmware handoff.

The DSDT (compiled on the guest from the scenario dsdt_*.dsl with iasl)
defines \\_SB.CPUS.C000..C005, each carrying a _CST package.

Scenario 'control'    : one valid C1 (HALT) state                      -- boots,
                        hw.acpi.cpu.cst.cx_supported == "C1/1"
Scenario 'nullenter'  : one C2 state, GAS SystemIO/8/addr=0
                        -> acpi_bus_alloc_gas() rejects (Address==0, acpi.c:1280)
                        -> cx->res stays NULL
                        -> acpi_cst_md_cx_setup() *accepts* it (returns 0,
                           acpi_cstate_machdep.c:91-93) with enter still NULL
                        -> acpi_cst_idle(): cx_next->enter(cx_next) == call NULL
                           (acpi_cpu_cstate.c:970) -> boot-time kernel panic
Scenario 'zerostate'  : one C2 state, GAS FixedHW (no MWAIT under TCG)
                        -> mwait setup EOPNOTSUPP -> state skipped
                        -> acpi_cst_cx_probe_cst() returns 0 with ZERO states
                        -> idle reads the zeroed states[0] -> enter==NULL ->
                           same panic via the caller's missing zero-state guard
Scenario 'badhint'    : valid C1 + C2 FixedHW/native with an INVALID mwait
                        hint (0x99999971: cx_idx=(0x71>>4)+1=8 >= CPU_MWAIT_CX_MAX)
                        -> "C2: invalid mwait hint 0x99999971", state rejected,
                        BUT the done: block still runs cpu_mwait_cx_no_bmsts()
                        twice + no_bmarb -> machdep.mwait.CX.c3_preamble drops
                        2 -> 0 system-wide (DF-5620); boot survives (C1 valid)
"""
import struct
import sys

BASE     = 0x40000000
RSDP_OFF = 0x0000
RSDT_OFF = 0x0100
FADT_OFF = 0x0200
FACS_OFF = 0x0280
MADT_OFF = 0x0400
DSDT_OFF = 0x1000


def csum(b):
    return (-sum(b)) & 0xFF


def sdth(sig, length, rev):
    h = bytearray(36)
    h[0:4] = sig
    struct.pack_into('<I', h, 4, length)
    h[8] = rev
    h[9] = 0
    h[10:16] = b'DFBSD '
    h[17:21] = b'PASS'
    struct.pack_into('<I', h, 21, 1)
    h[25:29] = b'IACT'
    struct.pack_into('<I', h, 29, 1)
    return h


def build(dsdt, ncpu=6):
    # MADT: ncpu LAPIC entries (BSP apic id 0 first, all enabled) + the real
    # QEMU IOAPIC so that PCI interrupts keep working (as DF-5483's control).
    ent = b''
    for i in range(ncpu):
        ent += struct.pack('<BBBBI', 0, 8, i, i, 1)          # LAPIC
    ent += struct.pack('<BBBBII', 1, 12, 0, 0, 0xFEC00000, 0)  # IOAPIC
    madt = sdth(b'APIC', 44 + len(ent), 3) + bytearray(8)
    struct.pack_into('<I', madt, 36, 0xFEE00000)
    struct.pack_into('<I', madt, 40, 1)
    madt += ent
    madt[9] = csum(madt)

    # FACS (64 bytes, everything zeroed)
    facs = bytearray(64)
    facs[0:4] = b'FACS'
    struct.pack_into('<I', facs, 4, 64)

    # FADT rev1 (128 bytes): PM registers cloned from the guest's real
    # SeaBIOS/QEMU FADT so AcpiEnableSubsystem() finds working PM1/GPE blocks;
    # DSDT pointer ours.  P_LVL2/P_LVL3 latency 0xfff (>100/1000 => FADT
    # C2/C3 never offered; scenarios are all _CST-mode anyway).
    fadt = sdth(b'FACP', 128, 1)
    fadt += bytearray(128 - 36)
    struct.pack_into('<I', fadt, 36, BASE + FACS_OFF)   # FIRMWARE_CTRL
    struct.pack_into('<I', fadt, 40, BASE + DSDT_OFF)   # DSDT
    fadt[44] = 1                                        # INT_MODEL (QEMU)
    fadt[45] = 0                                        # profile
    struct.pack_into('<H', fadt, 46, 9)                 # SCI_INT
    struct.pack_into('<I', fadt, 48, 0xb2)              # SMI_CMD (QEMU)
    fadt[52] = 0xf1                                     # ACPI_ENABLE
    fadt[53] = 0xf0                                     # ACPI_DISABLE
    struct.pack_into('<I', fadt, 56, 0x600)             # PM1a_EVT_BLK
    struct.pack_into('<I', fadt, 64, 0x604)             # PM1a_CNT_BLK
    struct.pack_into('<I', fadt, 76, 0x608)             # PM_TMR_BLK
    struct.pack_into('<I', fadt, 80, 0xafe0)            # GPE0_BLK
    fadt[88] = 4                                        # PM1_EVT_LEN
    fadt[89] = 2                                        # PM1_CNT_LEN
    fadt[91] = 4                                        # PM_TMR_LEN
    fadt[92] = 4                                        # GPE0_BLK_LEN
    struct.pack_into('<I', fadt, 96, 0x0fff)            # P_LVL2_LAT
    struct.pack_into('<I', fadt, 100, 0x0fff)           # P_LVL3_LAT
    fadt[9] = csum(fadt)

    # RSDT: FACP + APIC
    rsdt = sdth(b'RSDT', 36 + 8, 1) + bytearray(8)
    struct.pack_into('<I', rsdt, 36, BASE + FADT_OFF)
    struct.pack_into('<I', rsdt, 40, BASE + MADT_OFF)
    rsdt[9] = csum(rsdt)

    # RSDP v1 (20 bytes)
    rsdp = bytearray(20)
    rsdp[0:8] = b'RSD PTR '
    rsdp[8] = 0
    rsdp[9:15] = b'DFBSD '
    rsdp[15] = 0
    struct.pack_into('<I', rsdp, 16, BASE + RSDT_OFF)

    blob = bytearray(DSDT_OFF + len(dsdt))
    blob[RSDP_OFF:RSDP_OFF + 20] = rsdp
    blob[RSDT_OFF:RSDT_OFF + len(rsdt)] = rsdt
    blob[FADT_OFF:FADT_OFF + len(fadt)] = fadt
    blob[FACS_OFF:FACS_OFF + len(facs)] = facs
    blob[MADT_OFF:MADT_OFF + len(madt)] = madt
    blob[DSDT_OFF:DSDT_OFF + len(dsdt)] = dsdt
    return bytes(blob)


if __name__ == '__main__':
    aml_file, out = sys.argv[1], sys.argv[2]
    with open(aml_file, 'rb') as f:
        dsdt = f.read()
    assert dsdt[:4] == b'DSDT', 'not a DSDT'
    with open(out, 'wb') as f:
        f.write(build(dsdt))
    sys.stderr.write('blob: %d bytes (DSDT %d)\n' % (DSDT_OFF + len(dsdt), len(dsdt)))
