#!/usr/bin/env python3
#
# Copyright (c) 2017 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0

"""Generate Interrupt Descriptor Table for x86 CPUs.

This script generates the interrupt descriptor table (IDT) for x86.
Please consult the IA Architecture SW Developer Manual, volume 3,
for more details on this data structure.

This script accepts as input the zephyr_prebuilt.elf binary,
which is a link of the Zephyr kernel without various build-time
generated data structures (such as the IDT) inserted into it.
This kernel image has been properly padded such that inserting
these data structures will not disturb the memory addresses of
other symbols. From the kernel binary we read a special section
"intList" which contains the desired interrupt routing configuration
for the kernel, populated by instances of the IRQ_CONNECT() macro.

This script outputs three binary tables:

1. The interrupt descriptor table itself.
2. A bitfield indicating which vectors in the IDT are free for
   installation of dynamic interrupts at runtime.
3. An array which maps configured IRQ lines to their associated
   vector entries in the IDT, used to program the APIC at runtime.
"""

import argparse
import os
import struct
import sys

import elftools
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import SymbolTableSection
from packaging import version

if version.parse(elftools.__version__) < version.parse('0.24'):
    sys.exit("pyelftools is out of date, need version 0.24 or later")

# This will never change, first selector in the GDT after the null selector
KERNEL_CODE_SEG = 0x08

# These exception vectors push an error code onto the stack.
ERR_CODE_VECTORS = [8, 10, 11, 12, 13, 14, 17, 21]


def debug(text):
    if not args.verbose:
        return
    sys.stdout.write(os.path.basename(sys.argv[0]) + ": " + text + "\n")


def error(text):
    sys.exit(os.path.basename(sys.argv[0]) + ": " + text)


# See Section 6.11 of the Intel Architecture Software Developer's Manual
gate_desc_format = "<HHBBH"


def create_irq_gate(handler, dpl):
    present = 1
    gate_type = 0xE  # 32-bit interrupt gate
    type_attr = gate_type | (dpl << 5) | (present << 7)

    offset_hi = handler >> 16
    offset_lo = handler & 0xFFFF

    data = struct.pack(gate_desc_format, offset_lo, KERNEL_CODE_SEG, 0, type_attr, offset_hi)
    return data


def create_task_gate(tss, dpl):
    present = 1
    gate_type = 0x5  # 32-bit task gate
    type_attr = gate_type | (dpl << 5) | (present << 7)

    data = struct.pack(gate_desc_format, 0, tss, 0, type_attr, 0)
    return data


def create_idt_binary(idt_config, filename):
    with open(filename, "wb") as fp:
        for handler, tss, dpl in idt_config:
            if handler and tss:
                error("entry specifies both handler function and tss")

            if not handler and not tss:
                error("entry does not specify either handler or tss")

            if handler:
                data = create_irq_gate(handler, dpl)
            else:
                data = create_task_gate(tss, dpl)

            fp.write(data)


map_fmt = "<B"


def create_irq_vec_map_binary(irq_vec_map, filename):
    with open(filename, "wb") as fp:
        for i in irq_vec_map:
            fp.write(struct.pack(map_fmt, i))


def priority_range(prio):
    # Priority levels are represented as groups of 16 vectors within the IDT
    base = 32 + (prio * 16)
    return range(base, base + 16)


def update_irq_vec_map(irq_vec_map, irq, vector, max_irq):
    # No IRQ associated; exception or software interrupt
    if irq == -1:
        return

    if irq >= max_irq:
        error(f"irq {irq:d} specified, but CONFIG_MAX_IRQ_LINES is {max_irq:d}")

    # This table will never have values less than 32 since those are for
    # exceptions; 0 means unconfigured
    if irq_vec_map[irq] != 0:
        error(f"multiple vector assignments for interrupt line {irq:d}")

    debug(f"assign IRQ {irq:d} to vector {vector:d}")
    irq_vec_map[irq] = vector


def setup_idt(spur_code, spur_nocode, intlist, max_vec, max_irq):
    irq_vec_map = [0 for i in range(max_irq)]
    vectors = [None for i in range(max_vec)]

    # Pass 1: sanity check and set up hard-coded interrupt vectors
    for handler, irq, prio, vec, dpl, tss in intlist:
        if vec == -1:
            if prio == -1:
                error("entry does not specify vector or priority level")
            continue

        if vec >= max_vec:
            error(f"Vector {vec:d} specified, but size of IDT is only {max_vec:d} vectors")

        if vectors[vec] is not None:
            error(f"Multiple assignments for vector {vec:d}")

        vectors[vec] = (handler, tss, dpl)
        update_irq_vec_map(irq_vec_map, irq, vec, max_irq)

    # Pass 2: set up priority-based interrupt vectors
    for handler, irq, prio, vec, dpl, tss in intlist:
        if vec != -1:
            continue

        for vi in priority_range(prio):
            if vi >= max_vec:
                break
            if vectors[vi] is None:
                vec = vi
                break

        if vec == -1:
            error(f"can't find a free vector in priority level {prio:d}")

        vectors[vec] = (handler, tss, dpl)
        update_irq_vec_map(irq_vec_map, irq, vec, max_irq)

    # Pass 3: fill in unused vectors with spurious handler at dpl=0
    for i in range(max_vec):
        if vectors[i] is not None:
            continue

        if i in ERR_CODE_VECTORS:
            handler = spur_code
        else:
            handler = spur_nocode

        vectors[i] = (handler, 0, 0)

    return vectors, irq_vec_map


def get_symbols(obj):
    for section in obj.iter_sections():
        if isinstance(section, SymbolTableSection):
            return {sym.name: sym.entry.st_value for sym in section.iter_symbols()}

    raise LookupError("Could not find symbol table")


# struct genidt_header_s {
# uint32_t spurious_addr;
# uint32_t spurious_no_error_addr;
# int32_t num_entries;
# };


intlist_header_fmt = "<II"

# struct genidt_entry_s {
# uint32_t isr;
# int32_t irq;
# int32_t priority;
# int32_t vector_id;
# int32_t dpl;
# int32_t tss;
# };

intlist_entry_fmt = "<Iiiiii"


def get_intlist(elf):
    intdata = elf.get_section_by_name("intList").data()

    header_sz = struct.calcsize(intlist_header_fmt)
    header = struct.unpack_from(intlist_header_fmt, intdata, 0)
    intdata = intdata[header_sz:]

    spurious_code = header[0]
    spurious_nocode = header[1]

    debug(f"spurious handler (code)    : {hex(header[0]):s}")
    debug(f"spurious handler (no code) : {hex(header[1]):s}")

    intlist = [i for i in struct.iter_unpack(intlist_entry_fmt, intdata)]

    debug("Configured interrupt routing")
    debug("handler    irq pri vec dpl")
    debug("--------------------------")

    for irq in intlist:
        debug(
            f"{hex(irq[0]):<10} "
            f"{'-' if irq[1] == -1 else irq[1]:<3} "
            f"{'-' if irq[2] == -1 else irq[2]:<3} "
            f"{'-' if irq[3] == -1 else irq[3]:<3} "
            f"{irq[4]:<2}"
        )

    return (spurious_code, spurious_nocode, intlist)


def parse_args():
    global args
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        allow_abbrev=False,
    )

    parser.add_argument(
        "-m", "--vector-map", required=True, help="Output file mapping IRQ lines to IDT vectors"
    )
    parser.add_argument(
        "-o", "--output-idt", required=True, help="Output file containing IDT binary"
    )
    parser.add_argument(
        "-a",
        "--output-vectors-alloc",
        required=False,
        help="Output file indicating allocated vectors",
    )
    parser.add_argument("-k", "--kernel", required=True, help="Zephyr kernel image")
    parser.add_argument(
        "-v", "--verbose", action="store_true", help="Print extra debugging information"
    )
    args = parser.parse_args()
    if "VERBOSE" in os.environ:
        args.verbose = 1


def create_irq_vectors_allocated(vectors, spur_code, spur_nocode, filename):
    # Construct a bitfield over all the IDT vectors, where if bit n is 1,
    # that vector is free. those vectors have either of the two spurious
    # interrupt handlers installed, they are free for runtime installation
    # of interrupts
    num_chars = (len(vectors) + 7) // 8
    vbits = num_chars * [0]
    for i, (handler, _, _) in enumerate(vectors):
        if handler not in (spur_code, spur_nocode):
            continue

        vbit_index = i // 8
        vbit_val = 1 << (i % 8)
        vbits[vbit_index] = vbits[vbit_index] | vbit_val

    with open(filename, "wb") as fp:
        for char in vbits:
            fp.write(struct.pack("<B", char))


def main():
    parse_args()

    with open(args.kernel, "rb") as fp:
        kernel = ELFFile(fp)

        syms = get_symbols(kernel)
        spur_code, spur_nocode, intlist = get_intlist(kernel)

    max_irq = syms["CONFIG_MAX_IRQ_LINES"]
    max_vec = syms["CONFIG_IDT_NUM_VECTORS"]

    vectors, irq_vec_map = setup_idt(spur_code, spur_nocode, intlist, max_vec, max_irq)

    create_idt_binary(vectors, args.output_idt)
    create_irq_vec_map_binary(irq_vec_map, args.vector_map)
    if args.output_vectors_alloc:
        create_irq_vectors_allocated(vectors, spur_code, spur_nocode, args.output_vectors_alloc)


if __name__ == "__main__":
    main()
