axvm 0.6.1

Virtual Machine resource management crate for ArceOS's hypervisor variant.
//! QEMU table-loader serialization of the same logical x86 ACPI tables.

use acpi_tables::{facs::FACS, rsdp::Rsdp, xsdt::XSDT};
use axdevice::FwCfgAcpiBlobs;

use super::{
    aml::{build_dsdt, build_spcr, oem_id, oem_revision, oem_table_id},
    config::X86FirmwarePlan,
    tables::{build_fadt, build_madt, serialize},
};
use crate::boot::acpi::*;

const TABLE_FILE: &str = "etc/acpi/tables";
const RSDP_FILE: &str = "etc/acpi/rsdp";
const FADT_X_FIRMWARE_CTRL_OFFSET: usize = 132;
const FADT_X_DSDT_OFFSET: usize = 140;
const XSDT_HEADER_SIZE: usize = 36;

pub(crate) fn build_fw_cfg_blobs(plan: &X86FirmwarePlan) -> Result<FwCfgAcpiBlobs, AcpiBuildError> {
    let mut dsdt = build_dsdt(plan)?;
    let mut madt = build_madt(plan);
    let mut spcr = build_spcr(plan);
    let mut arena = AcpiTableArena::new(0, u64::from(u32::MAX) + 1)?;
    let xsdt_slot = arena.reserve("XSDT", XSDT_HEADER_SIZE + 3 * 8, 8)?;
    let fadt_slot = arena.reserve("FADT", acpi_tables::fadt::FADT::len(), 8)?;
    let facs_slot = arena.reserve("FACS", FACS::len(), 64)?;
    let dsdt_slot = arena.reserve("DSDT", dsdt.len(), 8)?;
    let madt_slot = arena.reserve("MADT", madt.len(), 8)?;
    let spcr_slot = arena.reserve("SPCR", spcr.len(), 8)?;

    let mut fadt = build_fadt(plan, dsdt_slot.gpa(), facs_slot.gpa());
    let mut xsdt = XSDT::new(oem_id(), oem_table_id(), oem_revision());
    xsdt.add_entry(fadt_slot.gpa());
    xsdt.add_entry(madt_slot.gpa());
    xsdt.add_entry(spcr_slot.gpa());
    let mut xsdt = serialize(&xsdt);
    let facs = serialize(&FACS::new());

    for (name, table) in [
        ("XSDT", xsdt.as_mut_slice()),
        ("FADT", fadt.as_mut_slice()),
        ("DSDT", dsdt.as_mut_slice()),
        ("MADT", madt.as_mut_slice()),
        ("SPCR", spcr.as_mut_slice()),
    ] {
        clear_checksum(table, 9, name)?;
    }

    arena.write(&xsdt_slot, &xsdt)?;
    arena.write(&fadt_slot, &fadt)?;
    arena.write(&facs_slot, &facs)?;
    arena.write(&dsdt_slot, &dsdt)?;
    arena.write(&madt_slot, &madt)?;
    arena.write(&spcr_slot, &spcr)?;

    let mut rsdp = serialize(&Rsdp::new(oem_id(), xsdt_slot.gpa()));
    clear_checksum(&mut rsdp, 8, "RSDP")?;
    clear_checksum(&mut rsdp, 32, "RSDP")?;
    let mut loader = AcpiLoaderPlan::new();
    loader.allocate(TABLE_FILE, 64, LoaderZone::High)?;
    loader.allocate(RSDP_FILE, 16, LoaderZone::Fseg)?;
    add_table_pointer(
        &mut loader,
        &fadt_slot,
        FADT_X_FIRMWARE_CTRL_OFFSET,
        TABLE_FILE,
        8,
    )?;
    add_table_pointer(&mut loader, &fadt_slot, FADT_X_DSDT_OFFSET, TABLE_FILE, 8)?;
    for index in 0..3 {
        add_table_pointer(
            &mut loader,
            &xsdt_slot,
            XSDT_HEADER_SIZE + index * 8,
            TABLE_FILE,
            8,
        )?;
    }
    loader.add_pointer(RSDP_FILE, TABLE_FILE, 24, 8)?;
    for (slot, length) in [
        (&xsdt_slot, xsdt.len()),
        (&fadt_slot, fadt.len()),
        (&dsdt_slot, dsdt.len()),
        (&madt_slot, madt.len()),
        (&spcr_slot, spcr.len()),
    ] {
        add_table_checksum(&mut loader, slot, length)?;
    }
    loader.add_checksum(RSDP_FILE, 8, 0, 20)?;
    loader.add_checksum(RSDP_FILE, 32, 0, u32_len(rsdp.len(), "RSDP length")?)?;

    Ok(FwCfgAcpiBlobs {
        tables: arena.into_bytes(),
        rsdp,
        loader: loader.serialize(),
    })
}

fn add_table_pointer(
    loader: &mut AcpiLoaderPlan,
    slot: &AcpiAllocation,
    field_offset: usize,
    pointee_file: &str,
    size: u8,
) -> Result<(), AcpiBuildError> {
    let offset =
        slot.offset()
            .checked_add(field_offset)
            .ok_or_else(|| AcpiBuildError::AddressOverflow {
                object: "ACPI table-loader pointer".into(),
            })?;
    loader.add_pointer(
        TABLE_FILE,
        pointee_file,
        u32_len(offset, "pointer offset")?,
        size,
    )
}

fn add_table_checksum(
    loader: &mut AcpiLoaderPlan,
    slot: &AcpiAllocation,
    length: usize,
) -> Result<(), AcpiBuildError> {
    let start = u32_len(slot.offset(), "table offset")?;
    let checksum = slot
        .offset()
        .checked_add(9)
        .ok_or_else(|| AcpiBuildError::AddressOverflow {
            object: "ACPI checksum byte".into(),
        })?;
    loader.add_checksum(
        TABLE_FILE,
        u32_len(checksum, "checksum offset")?,
        start,
        u32_len(length, "table length")?,
    )
}

fn u32_len(value: usize, field: &'static str) -> Result<u32, AcpiBuildError> {
    u32::try_from(value).map_err(|_| AcpiBuildError::InvalidValue {
        field,
        value: std::format!("{value:#x}"),
    })
}

fn clear_checksum(
    bytes: &mut [u8],
    offset: usize,
    object: &'static str,
) -> Result<(), AcpiBuildError> {
    let expected = offset + 1;
    let actual = bytes.len();
    let checksum = bytes
        .get_mut(offset)
        .ok_or_else(|| AcpiBuildError::LengthMismatch {
            object: object.into(),
            expected,
            actual,
        })?;
    *checksum = 0;
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::ops::Range;

    use super::*;

    const LOADER_ENTRY_SIZE: usize = 128;

    #[derive(Debug, Eq, PartialEq)]
    enum DecodedLoaderCommand {
        Allocate {
            file: String,
            alignment: u32,
            zone: u8,
        },
        Pointer {
            pointer_file: String,
            pointee_file: String,
            pointer_offset: u32,
            pointer_size: u8,
        },
        Checksum {
            file: String,
            checksum_offset: u32,
            start: u32,
            length: u32,
        },
    }

    #[test]
    fn fw_cfg_blobs_describe_the_complete_x86_acpi_loader_plan() {
        let blobs = build_fw_cfg_blobs(&super::super::config::test_plan(2)).unwrap();
        let commands = decode_loader(&blobs.loader);

        assert_eq!(blobs.loader.len(), 15 * LOADER_ENTRY_SIZE);
        assert_eq!(commands.len(), 15);
        assert_eq!(
            commands[0],
            DecodedLoaderCommand::Allocate {
                file: TABLE_FILE.into(),
                alignment: 64,
                zone: LoaderZone::High as u8,
            }
        );
        assert_eq!(
            commands[1],
            DecodedLoaderCommand::Allocate {
                file: RSDP_FILE.into(),
                alignment: 16,
                zone: LoaderZone::Fseg as u8,
            }
        );

        let xsdt = acpi_table_range(&blobs.tables, b"XSDT");
        let fadt = acpi_table_range(&blobs.tables, b"FACP");
        let facs = acpi_table_range(&blobs.tables, b"FACS");
        let dsdt = acpi_table_range(&blobs.tables, b"DSDT");
        let madt = acpi_table_range(&blobs.tables, b"APIC");
        let spcr = acpi_table_range(&blobs.tables, b"SPCR");

        for (command, pointer_file, offset, target) in [
            (&commands[2], TABLE_FILE, fadt.start + 132, &facs),
            (&commands[3], TABLE_FILE, fadt.start + 140, &dsdt),
            (&commands[4], TABLE_FILE, xsdt.start + 36, &fadt),
            (&commands[5], TABLE_FILE, xsdt.start + 44, &madt),
            (&commands[6], TABLE_FILE, xsdt.start + 52, &spcr),
            (&commands[7], RSDP_FILE, 24, &xsdt),
        ] {
            assert_pointer_command(
                command,
                pointer_file,
                offset,
                target,
                &blobs.tables,
                &blobs.rsdp,
            );
        }

        for (command, table) in commands[8..13]
            .iter()
            .zip([&xsdt, &fadt, &dsdt, &madt, &spcr])
        {
            assert_checksum_command(command, TABLE_FILE, table, &blobs.tables);
        }
        assert_eq!(
            commands[13],
            DecodedLoaderCommand::Checksum {
                file: RSDP_FILE.into(),
                checksum_offset: 8,
                start: 0,
                length: 20,
            }
        );
        assert_eq!(blobs.rsdp[8], 0);
        assert_eq!(
            commands[14],
            DecodedLoaderCommand::Checksum {
                file: RSDP_FILE.into(),
                checksum_offset: 32,
                start: 0,
                length: blobs.rsdp.len() as u32,
            }
        );
        assert_eq!(blobs.rsdp[32], 0);
    }

    fn decode_loader(bytes: &[u8]) -> Vec<DecodedLoaderCommand> {
        assert_eq!(bytes.len() % LOADER_ENTRY_SIZE, 0);
        bytes
            .as_chunks::<LOADER_ENTRY_SIZE>()
            .0
            .iter()
            .map(|entry| match read_u32(entry, 0) {
                1 => DecodedLoaderCommand::Allocate {
                    file: read_file(&entry[4..60]),
                    alignment: read_u32(entry, 60),
                    zone: entry[64],
                },
                2 => DecodedLoaderCommand::Pointer {
                    pointer_file: read_file(&entry[4..60]),
                    pointee_file: read_file(&entry[60..116]),
                    pointer_offset: read_u32(entry, 116),
                    pointer_size: entry[120],
                },
                3 => DecodedLoaderCommand::Checksum {
                    file: read_file(&entry[4..60]),
                    checksum_offset: read_u32(entry, 60),
                    start: read_u32(entry, 64),
                    length: read_u32(entry, 68),
                },
                command => panic!("unexpected table-loader command {command}"),
            })
            .collect()
    }

    fn read_file(field: &[u8]) -> String {
        let length = field
            .iter()
            .position(|byte| *byte == 0)
            .unwrap_or(field.len());
        String::from_utf8(field[..length].to_vec()).unwrap()
    }

    fn read_u32(bytes: &[u8], offset: usize) -> u32 {
        u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap())
    }

    fn read_u64(bytes: &[u8], offset: usize) -> u64 {
        u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap())
    }

    fn acpi_table_range(tables: &[u8], signature: &[u8; 4]) -> Range<usize> {
        let start = tables
            .windows(signature.len())
            .position(|window| window == signature)
            .unwrap_or_else(|| panic!("missing ACPI table signature {signature:?}"));
        let length = read_u32(tables, start + 4) as usize;
        let end = start.checked_add(length).unwrap();
        assert!(end <= tables.len(), "ACPI table extends past blob end");
        start..end
    }

    fn assert_pointer_command(
        command: &DecodedLoaderCommand,
        pointer_file: &str,
        expected_offset: usize,
        target: &Range<usize>,
        tables: &[u8],
        rsdp: &[u8],
    ) {
        let DecodedLoaderCommand::Pointer {
            pointer_file: actual_pointer_file,
            pointee_file,
            pointer_offset,
            pointer_size,
        } = command
        else {
            panic!("expected pointer command, got {command:?}");
        };
        assert_eq!(actual_pointer_file, pointer_file);
        assert_eq!(pointee_file, TABLE_FILE);
        assert_eq!(*pointer_offset as usize, expected_offset);
        assert_eq!(*pointer_size, 8);

        let source = if pointer_file == TABLE_FILE {
            tables
        } else {
            assert_eq!(pointer_file, RSDP_FILE);
            rsdp
        };
        let pointer_end = expected_offset.checked_add(8).unwrap();
        assert!(pointer_end <= source.len());
        assert_eq!(read_u64(source, expected_offset) as usize, target.start);
        assert!(target.end <= tables.len());
    }

    fn assert_checksum_command(
        command: &DecodedLoaderCommand,
        expected_file: &str,
        table: &Range<usize>,
        data: &[u8],
    ) {
        let DecodedLoaderCommand::Checksum {
            file,
            checksum_offset,
            start,
            length,
        } = command
        else {
            panic!("expected checksum command, got {command:?}");
        };
        assert_eq!(file, expected_file);
        assert_eq!(*checksum_offset as usize, table.start + 9);
        assert_eq!(*start as usize, table.start);
        assert_eq!(*length as usize, table.len());
        assert!(table.end <= data.len());
        assert_eq!(data[*checksum_offset as usize], 0);
    }
}