#!/bin/sh
set -eu

# win11-qemu-install WINDOWS_ISO VM_DISK [VIRTIO_WIN_ISO]
#
# Mostly-unattended install of Windows 11 onto VM_DISK, a disk image created
# beforehand with qemu-img. The answer file presets the language, bypasses the
# TPM, Secure Boot, and RAM checks, and automates OOBE. Setup's remaining pages
# (EULA, edition, target disk) are clicked once over VNC on 127.0.0.1:5900.

[ "$#" -eq 2 ] || [ "$#" -eq 3 ] || { echo "usage: $0 WINDOWS_ISO VM_DISK [VIRTIO_WIN_ISO]" >&2; exit 64; }

[ "$(id -u)" -ne 0 ] || { echo "error: refusing to run QEMU as root" >&2; exit 1; }

WIN_ISO=$1
VM_DISK=$2
VIRTIO_ISO=${3:-}
[ -r "$WIN_ISO" ] || { echo "error: WINDOWS_ISO not readable: $WIN_ISO" >&2; exit 1; }
[ -r "$VM_DISK" ] && [ -w "$VM_DISK" ] || { echo "error: VM_DISK must be readable and writable: $VM_DISK" >&2; exit 1; }
[ -z "$VIRTIO_ISO" ] || [ -r "$VIRTIO_ISO" ] || { echo "error: VIRTIO_WIN_ISO not readable: $VIRTIO_ISO" >&2; exit 1; }

RAM_MB=${RAM_MB:-8192}
SMP=${SMP:-8}
IMAGE_FORMAT=${IMAGE_FORMAT:-qcow2}
LOCAL_USER=${LOCAL_USER:-user}
LOCAL_PASS=${LOCAL_PASS:-123456}

validate_positive_integer() {
    case $2 in
        ''|*[!0-9]*|0) echo "error: $1 must be a positive integer: $2" >&2; exit 1 ;;
    esac
}

reject_comma() {
    case $2 in
        *,*) echo "error: $1 must not contain a comma: $2" >&2; exit 1 ;;
    esac
}

validate_positive_integer RAM_MB "$RAM_MB"
validate_positive_integer SMP "$SMP"
case $IMAGE_FORMAT in
    qcow2|raw) ;;
    *) echo "error: IMAGE_FORMAT must be qcow2 or raw: $IMAGE_FORMAT" >&2; exit 1 ;;
esac
reject_comma WINDOWS_ISO "$WIN_ISO"
reject_comma VM_DISK "$VM_DISK"
reject_comma VIRTIO_WIN_ISO "$VIRTIO_ISO"
reject_comma TMPDIR "${TMPDIR:-/tmp}"

command -v qemu-system-x86_64 >/dev/null || { echo "error: need qemu-system-x86_64" >&2; exit 1; }

xml_escape() {
    sed \
        -e 's/&/\&amp;/g' \
        -e 's/</\&lt;/g' \
        -e 's/>/\&gt;/g' \
        -e 's/"/\&quot;/g' \
        -e "s/'/\&apos;/g"
}

LOCAL_USER_XML=$(printf '%s' "$LOCAL_USER" | xml_escape)
LOCAL_PASS_XML=$(printf '%s' "$LOCAL_PASS" | xml_escape)

# The answer file carries LOCAL_PASS in plain text; mktemp gives a 0700 dir.
WORK=$(mktemp -d "${TMPDIR:-/tmp}/win11-qemu-install.XXXXXX")
QEMU_PID=
trap 'rm -rf "$WORK"' EXIT

# QEMU runs in the background so a trapped signal is handled while it is
# still alive; in the foreground the trap only runs once QEMU has exited.
# QEMU shuts down on TERM, so that is what gets sent on whatever arrived.
stop() {
    trap - HUP INT TERM
    if [ -n "$QEMU_PID" ]; then
        kill "$QEMU_PID" 2>/dev/null || :
        wait "$QEMU_PID" 2>/dev/null || :
    fi
    exit "$1"
}
trap 'stop 129' HUP
trap 'stop 130' INT
trap 'stop 143' TERM

# The windowsPE pass stays minimal on purpose: LabConfig bypasses and
# language only. Automating the EULA, edition, and disk pages with UserData,
# DiskConfiguration, and DriverPaths makes the Windows 11 24H2 setup engine
# abort right after boot (extend code 0x40031), so those pages are clicked
# once over VNC instead.
cat <<EOF >"$WORK/Autounattend.xml"
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend"
          xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
  <settings pass="windowsPE">
    <component name="Microsoft-Windows-International-Core-WinPE"
               processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35"
               language="neutral"
               versionScope="nonSxS">
      <SetupUILanguage>
        <UILanguage>en-US</UILanguage>
      </SetupUILanguage>
      <InputLocale>0409:00000409</InputLocale>
      <SystemLocale>en-US</SystemLocale>
      <UILanguage>en-US</UILanguage>
      <UserLocale>en-US</UserLocale>
    </component>

    <component name="Microsoft-Windows-Setup"
               processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35"
               language="neutral"
               versionScope="nonSxS">
      <RunSynchronous>
        <RunSynchronousCommand wcm:action="add">
          <Order>1</Order>
          <Description>Create LabConfig</Description>
          <Path>cmd /c reg add HKLM\\SYSTEM\\Setup\\LabConfig /f</Path>
        </RunSynchronousCommand>
        <RunSynchronousCommand wcm:action="add">
          <Order>2</Order>
          <Description>Bypass TPM</Description>
          <Path>cmd /c reg add HKLM\\SYSTEM\\Setup\\LabConfig /v BypassTPMCheck /t REG_DWORD /d 1 /f</Path>
        </RunSynchronousCommand>
        <RunSynchronousCommand wcm:action="add">
          <Order>3</Order>
          <Description>Bypass Secure Boot</Description>
          <Path>cmd /c reg add HKLM\\SYSTEM\\Setup\\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 1 /f</Path>
        </RunSynchronousCommand>
        <RunSynchronousCommand wcm:action="add">
          <Order>4</Order>
          <Description>Bypass RAM check</Description>
          <Path>cmd /c reg add HKLM\\SYSTEM\\Setup\\LabConfig /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
        </RunSynchronousCommand>
      </RunSynchronous>
    </component>
  </settings>

  <settings pass="oobeSystem">
    <component name="Microsoft-Windows-International-Core"
               processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35"
               language="neutral"
               versionScope="nonSxS">
      <InputLocale>en-US</InputLocale>
      <SystemLocale>en-US</SystemLocale>
      <UILanguage>en-US</UILanguage>
      <UserLocale>en-US</UserLocale>
    </component>

    <component name="Microsoft-Windows-Shell-Setup"
               processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35"
               language="neutral"
               versionScope="nonSxS">
      <OOBE>
        <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
        <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
        <HideEULAPage>true</HideEULAPage>
        <ProtectYourPC>3</ProtectYourPC>
      </OOBE>

      <UserAccounts>
        <LocalAccounts>
          <LocalAccount wcm:action="add">
            <Name>${LOCAL_USER_XML}</Name>
            <DisplayName>${LOCAL_USER_XML}</DisplayName>
            <Group>Administrators</Group>
            <Password>
              <Value>${LOCAL_PASS_XML}</Value>
              <PlainText>true</PlainText>
            </Password>
          </LocalAccount>
        </LocalAccounts>
      </UserAccounts>

      <AutoLogon>
        <Enabled>true</Enabled>
        <Username>${LOCAL_USER_XML}</Username>
        <LogonCount>1</LogonCount>
        <Password>
          <Value>${LOCAL_PASS_XML}</Value>
          <PlainText>true</PlainText>
        </Password>
      </AutoLogon>
    </component>
  </settings>
</unattend>
EOF

echo 'use: remote-viewer vnc://127.0.0.1:5900'

# WinPE scans floppy drives for Autounattend.xml on its own; a vvfat floppy
# holding the single file is the simplest way to hand it over. usb-tablet
# gives the VNC pointer absolute positioning. No NIC: with no way to reach
# Microsoft, OOBE settles for a local account instead of forcing an online
# one.
set -- qemu-system-x86_64 \
    -enable-kvm \
    -machine q35 \
    -cpu host \
    -smp "$SMP" \
    -m "$RAM_MB" \
    -boot order=d \
    -sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny \
    -drive file="$WIN_ISO",format=raw,media=cdrom,if=ide,index=1 \
    -drive if=floppy,file="fat:floppy:$WORK",format=raw,readonly=on \
    -vnc 127.0.0.1:0 \
    -monitor none \
    -serial none \
    -parallel none \
    -usb \
    -device usb-tablet \
    -nic none

if [ -n "$VIRTIO_ISO" ]; then
    echo "virtio mode: enabled; when Setup's disk list is empty:"
    echo "  Load driver -> virtio CD -> viostor -> w11 -> amd64"
    set -- "$@" \
        -drive file="$VM_DISK",format="$IMAGE_FORMAT",if=virtio \
        -drive file="$VIRTIO_ISO",format=raw,media=cdrom,if=ide,index=2
else
    echo "virtio mode: disabled; disk attached over SATA/AHCI, no extra drivers"
    set -- "$@" \
        -device ich9-ahci,id=sata \
        -drive file="$VM_DISK",format="$IMAGE_FORMAT",if=none,id=systemdisk \
        -device ide-hd,drive=systemdisk,bus=sata.0
fi

"$@" &
QEMU_PID=$!
wait "$QEMU_PID"
