diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index b26f8f8ee..cfe2d136b 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -231,7 +231,7 @@ jobs: set -Eeuo pipefail case "$CALLBACK" in - powershell | legacy) + powershell | legacy | basic) ;; *) echo "Unsupported guest script type: $CALLBACK" @@ -249,6 +249,7 @@ jobs: printf '%s\n' "$token" > "$RUNNER_TEMP/data/validation.token" printf '%s\n' "$token" > "$RUNNER_TEMP/oem/validation.token" + if [[ "$CALLBACK" != "basic" ]]; then cat > "$RUNNER_TEMP/oem/sync-log.bat" <<'BATCH' @echo off setlocal @@ -327,6 +328,7 @@ jobs: shell.Run "cmd.exe /C echo VALIDATION_CALLBACK_FAILED^>COM1", 0, True VBSCRIPT + fi case "$CALLBACK" in powershell) @@ -764,6 +766,34 @@ jobs: exit /B %result% EOF ;; + + basic) + cat > "$RUNNER_TEMP/oem/install.bat" <<'BATCH' + @echo off + + :retry + if not exist \\host.lan\Data\validation.token goto wait + + echo TOKEN> C:\OEM\validation.result + type C:\OEM\validation.token >> C:\OEM\validation.result + echo SHARE>> C:\OEM\validation.result + type \\host.lan\Data\validation.token >> C:\OEM\validation.result + echo VERSION>> C:\OEM\validation.result + ver >> C:\OEM\validation.result + + if exist \\host.lan\Data\validation.result del \\host.lan\Data\validation.result >nul + copy C:\OEM\validation.result \\host.lan\Data\validation.result >nul + if errorlevel 1 goto wait + + goto end + + :wait + ping 127.0.0.1 -n 6 >nul + goto retry + + :end + BATCH + ;; esac echo "token=$token" >> "$GITHUB_OUTPUT" @@ -890,6 +920,7 @@ jobs: - name: Install and validate Windows shell: bash env: + CALLBACK: ${{ inputs.callback }} KILL_ON_FAILURE: ${{ inputs.kill_on_failure }} INSTALL_TIMEOUT: ${{ inputs.install_timeout }} REBOOT_TIMEOUT: ${{ inputs.reboot_timeout }} @@ -1130,6 +1161,101 @@ jobs: exit 1 fi + basic_result="$RUNNER_TEMP/data/validation.result" + + if [[ "$CALLBACK" == "basic" && -s "$basic_result" ]]; then + basic_response="$(tr -d '\r' < "$basic_result")" + + oem_file="$( + awk '$0 == "TOKEN" { getline; print; exit }' <<< "$basic_response" + )" + + share="$( + awk '$0 == "SHARE" { getline; print; exit }' <<< "$basic_response" + )" + + version="$( + awk ' + $0 == "VERSION" { found = 1; next } + found && NF { print; exit } + ' <<< "$basic_response" + )" + + if [[ -z "$oem_file" || -z "$share" || -z "$version" ]] || + ! grep -Eq '[0-9]+\.[0-9]+\.[0-9]+' <<< "$version"; then + sleep 1 + continue + fi + + stop_logs + trap - EXIT + + echo + echo "------------------------------------------------------------" + echo "Received response:" + printf '%s\n' "$basic_response" + + if [[ "$oem_file" != "$EXPECTED_TOKEN" ]]; then + echo "Failed to read C:\\OEM\\validation.token." + echo "Expected contents:" + echo " $EXPECTED_TOKEN" + echo "Received:" + echo " ${oem_file:-empty}" + exit 1 + fi + + if [[ "$share" != "$EXPECTED_TOKEN" ]]; then + echo "Failed to read \\\\host.lan\\Data\\validation.token." + echo "Expected contents:" + echo " $EXPECTED_TOKEN" + echo "Received:" + echo " ${share:-empty}" + exit 1 + fi + + normalized_caption="${version//\(R\)/}" + normalized_expected_caption="${EXPECTED_CAPTION//\(R\)/}" + + if [[ "$normalized_caption" != *"$normalized_expected_caption"* ]]; then + echo "Expected caption containing:" + echo " $EXPECTED_CAPTION" + echo "Received:" + echo " $version" + exit 1 + fi + + version_number="$( + grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' <<< "$version" | + head -n 1 || true + )" + + if [[ ! "$version_number" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Unexpected Windows version: $version" + exit 1 + fi + + build="${version_number##*.}" + + if [[ ! "$build" =~ ^[0-9]+$ ]]; then + echo "Invalid Windows build number: $build" + exit 1 + fi + + if (( build < MINIMUM_BUILD )); then + echo "Expected build $MINIMUM_BUILD or newer." + echo "Received build: $build" + exit 1 + fi + + echo + echo "$DISPLAY_NAME installed successfully." + echo "Version: $version_number" + echo "Build: $build" + echo "OEM files: copied successfully" + echo "Shared folder: readable and writable" + exit 0 + fi + response="$( grep -F 'VALIDATION_RESULT=' <<< "$container_log" | tail -n 1 | diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e35cb66eb..0dcccec17 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -206,6 +206,39 @@ jobs: minimum_build: ${{ matrix.minimum_build }} platform: ${{ matrix.platform }} + win9x: + strategy: + fail-fast: false + matrix: + include: + - name: Windows 95 + version: "95" + expected_caption: Windows 95 + minimum_build: 950 + platform: x86 + + - name: Windows 98 + version: "98" + expected_caption: Windows 98 + minimum_build: 2222 + platform: x86 + + - name: Windows ME + version: "me" + expected_caption: Windows Millennium + minimum_build: 3000 + platform: x86 + + name: ${{ matrix.name }} + uses: ./.github/workflows/settings.yml + with: + name: ${{ matrix.name }} + version: ${{ matrix.version }} + callback: basic + expected_caption: ${{ matrix.expected_caption }} + minimum_build: ${{ matrix.minimum_build }} + platform: ${{ matrix.platform }} + tiny: strategy: fail-fast: false diff --git a/readme.md b/readme.md index 5417640f8..3d85f0ed5 100644 --- a/readme.md +++ b/readme.md @@ -125,6 +125,9 @@ For a complete graphical desktop experience, see [WinBoat](https://winboat.app), | `vu` | Windows Vista Ultimate | 3.0 GB | | `xp` | Windows XP Professional | 0.6 GB | | `2k` | Windows 2000 Professional | 0.4 GB | + | `me` | Windows ME | 0.5 GB | + | `98` | Windows 98 | 0.7 GB | + | `95` | Windows 95 | 0.6 GB | |||| | `2025` | Windows Server 2025 | 7.6 GB | | `2022` | Windows Server 2022 | 6.0 GB | diff --git a/src/define.sh b/src/define.sh index 00e535d46..d21f29e78 100644 --- a/src/define.sh +++ b/src/define.sh @@ -95,6 +95,12 @@ parseVersion() { VERSION="winxpx64" ;; "2k" | "2000" | "win2k" | "win2000" | "windows2k" | "windows2000" ) VERSION="win2kx86" ;; + "me" | "winme" | "win9x" | "windowsme" | "windows me" ) + VERSION="win9x" ;; + "98" | "98se" | "win98" | "win98se" | "windows98" | "windows98se" | "windows 98" | "windows 98 se" ) + VERSION="win98" ;; + "95" | "95c" | "win95" | "win95c" | "windows95" | "windows 95" ) + VERSION="win95" ;; "25" | "2025" | "win25" | "win2025" | "windows2025" | "windows 2025" ) VERSION="win2025-eval" ;; "22" | "2022" | "win22" | "win2022" | "windows2022" | "windows 2022" ) @@ -570,6 +576,14 @@ fromFile() { id="win11${arch}" ;; *"winxp"* | *"win_xp"* | *"windowsxp"* | *"windows_xp"* ) id="winxpx86" ;; + *"winme"* | *"win_me"* | *"win9x"* | *"windowsme"* | *"windows_me"* ) + id="win9x" ;; + *"win98"* | *"win_98"* | *"windows98"* | *"windows_98"* ) + id="win98" ;; + *"win95"* | *"win_95"* | *"windows95"* | *"windows_95"* ) + id="win95" ;; + *"winnt4"* | *"win_nt4"* | *"windowsnt4"* | *"windows_nt4"* | *"windows_nt_4"* ) + id="winnt4" ;; *"winvista"* | *"win_vista"* | *"windowsvista"* | *"windows_vista"* ) id="winvista${arch}" ;; "tiny11core"* | "tiny11_core"* | "tiny_11_core"* ) @@ -1038,7 +1052,7 @@ isLegacy() { local id="$1" case "${id,,}" in - "win9"* | "winnt4"* | "win2k"* | "winxp"* | "win2003"* | \ + "win9"* | "winnt4" | "win2k"* | "winxp"* | "win2003"* | \ "winvista"* | "win7"* | "win2008"* | "reactos" ) return 0 ;; esac @@ -1051,36 +1065,65 @@ supportsUnattended() { local id="$1" case "${id,,}" in - "win9"* | "winnt4"* | "reactos" ) + "winnt4" | "reactos" ) return 1 ;; esac return 0 } -supportsBootKey() { +supportsXML() { local id="$1" + supportsSIF "$id" && return 1 + supportsUnattended "$id" || return 1 + case "${id,,}" in - "win9"* | "winnt4" | "reactos" ) + "win9"* ) return 1 ;; - "win"* | "tiny"* | "core"* ) - return 0 ;; esac return 0 } -supportsXML() { +supportsSIF() { local id="$1" - supportsUnattended "$id" || return 1 - case "${id,,}" in "win2k"* | "winxp"* | "win2003"* ) + return 0 ;; + esac + + return 1 +} + +supportsACPI() { + + local id="$1" + + case "${id,,}" in + + "reactos" ) + + # If the ISO is a Live-CD it will ignore ACPI signals. + hasSystemImage && return 1 ;; + + esac + + return 0 +} + +supportsBootKey() { + + local id="$1" + + case "${id,,}" in + "win9"* | "winnt4" | "reactos" ) return 1 ;; + "win"* | "tiny"* | "core"* ) + return 0 ;; esac return 0 @@ -1337,6 +1380,21 @@ getLink1() { sum="a93251b31f92316411bb48458a695d9051b13cdeba714c46f105012fdda45bf3" url="2000/5.00.2195.6717_x86fre_client-professional_retail_en-us.7z" ;; + "win9x" ) + size=448957271 + sum="36e75592cf89e072e165da60100f91ca59b7667be90494f1aa4a78091011cfea" + url="9x/me/Windows%20Me_en.zip" + ;; + "win98" ) + size=558263127 + sum="980bfc9abc93c3104d1a00690c5db702efda7a377de7e06cda61889a972e2e08" + url="9x/98/se/Microsoft%20Windows%2098%20Second%20Edition%20%284.10.2222%29.rar" + ;; + "win95" ) + size=422127699 + sum="5456632a73a3c70f8e5ee566374876266a81f61968e623ef5500028dec8e2979" + url="9x/95/Microsoft%20Windows%2095C%20%284.03.1216.osr2.5%29.zip" + ;; esac case "${ret,,}" in @@ -1616,6 +1674,21 @@ getLink4() { sum="e3816f6e80b66ff686ead03eeafffe9daf020a5e4717b8bd4736b7c51733ba22" url="MicrosoftWindows2000BuildCollection/5.00.2195.6717_x86fre_client-professional_retail_en-us-ZRMPFPP_EN.iso" ;; + "win9x" ) + size=523665408 + sum="bff3e9e69e625d2fdffd01b326988c45be8b9285465a4dabbae96ac550de611e" + url="windows-me_202209/Windows%20ME.ISO" + ;; + "win98" ) + size=655591424 + sum="2adfb46df8a9c7bbd2f67bff07461cc2f9d9ec8e01f0e112cb044c9e3e62f607" + url="windows-98-se-isofile/Windows%2098%20Second%20Edition.iso" + ;; + "win95" ) + size=618229760 + sum="493212b2a3cd391269e55a8fde5ee0a35e29ddece4b5910ec49df69ab62f5e26" + url="win-95-osr-25_202103/Win95_OSR25.iso" + ;; "core11" ) size=3304132608 sum="c0e0252b24144b8defb6c7ded2bc09f9297daf1fb8369b16c5b85382331eb47f" diff --git a/src/legacy.sh b/src/legacy.sh index 810151b17..044f1781e 100644 --- a/src/legacy.sh +++ b/src/legacy.sh @@ -7,75 +7,68 @@ setMachine() { local iso="$2" local dir="$3" local desc="$4" - local version="" - case "${id,,}" in - "win2k"* ) version="2k" ;; - "winxp"* ) version="xp" ;; - "win2003"* ) version="2k3" ;; - esac - - if [ -n "$version" ]; then + if isLegacy "$id"; then - if ! legacyInstall "$iso" "$dir" "$desc" "$version"; then + if ! prepareLegacyInstall "$id" "$iso" "$dir" "$desc"; then error "Failed to prepare $desc ISO!" return 1 fi - fi - - if isLegacy "$id"; then - + writeState "vga" "std" || return 1 writeState "mode" "windows_legacy" || return 1 case "${id,,}" in - "win9"* | "win2k"* | "reactos" ) - writeState "vga" "cirrus" || return 1 ;; - * ) - writeState "vga" "std" || return 1 ;; + + "win9"* | "winnt4" | "win2k"* | "reactos" ) + + writeState "old" "pc" || return 1 + writeState "type" "auto" || return 1 + esac - fi + case "${id,,}" in - restoreBootMode || return 1 + "win95" | "winnt4" ) - case "${id,,}" in + writeState "usb" "N" || return 1 + writeState "port" "on" || return 1 + writeState "net" "pcnet" || return 1 + writeState "sound" "sb16" || return 1 ;; - "win9"* | "winnt4" ) + "win98" | "win9x" ) - writeState "old" "pc" || return 1 - writeState "net" "pcnet" || return 1 - writeState "type" "auto" || return 1 - writeState "usb" "pci-ohci" || return 1 ;; + writeState "port" "on" || return 1 + writeState "net" "pcnet" || return 1 + writeState "sound" "sb16" || return 1 + writeState "usb" "pci-ohci" || return 1 ;; - "win2k"* ) + "win2k"* ) - writeState "old" "pc" || return 1 - writeState "type" "auto" || return 1 - writeState "net" "rtl8139" || return 1 - writeState "usb" "pci-ohci" || return 1 ;; + writeState "net" "rtl8139" || return 1 + writeState "usb" "pci-ohci" || return 1 + writeState "sound" "usb-audio" || return 1 ;; - "winxpx"* | "win2003"* ) + "winxpx"* | "win2003"* ) - writeState "type" "blk" || return 1 - writeState "net" "rtl8139" || return 1 - writeState "sound" "usb-audio" || return 1 ;; + writeState "type" "blk" || return 1 + writeState "net" "rtl8139" || return 1 + writeState "sound" "usb-audio" || return 1 ;; - "reactos" ) + "reactos" ) - writeState "old" "pc" || return 1 - writeState "type" "auto" || return 1 - writeState "net" "rtl8139" || return 1 - writeState "usb" "pci-ohci" || return 1 ;; + writeState "sound" "AC97" || return 1 + writeState "net" "rtl8139" || return 1 + writeState "usb" "pci-ohci" || return 1 - esac + if isReactOSLiveCD "$iso"; then + SYSTEM="$iso" + fi ;; - if [[ "${id,,}" == "reactos" ]] && ! isCustomImage; then - # The ISO is a Live-CD so we need to disable the data disk - # as it will be always wiped during the next runs currently. - DISK_DISABLE="Y" + esac fi + restoreBootMode || return 1 restoreMachine || return 1 case "${id,,}" in @@ -112,10 +105,11 @@ setMachine() { restoreMachine() { + isPlatform "x64" || return 0 + # Restore the saved machine only when q35 is still the default; # an explicit user-selected machine must remain untouched. [[ "${MACHINE,,}" != "q35" ]] && return 0 - isPlatform "x64" || return 0 MACHINE="" restoreState "MACHINE" "old" || return 1 @@ -126,12 +120,6 @@ restoreMachine() { writeState "old" "$MACHINE" || return 1 fi - # Migrate existing WinXP installs to QEMU 10 - if [[ "${MACHINE,,}" == "pc-q35-2.10" ]]; then - MACHINE="" - removeState "old" || return 1 - fi - [ -z "$MACHINE" ] && MACHINE="q35" return 0 @@ -143,6 +131,7 @@ restoreMachineState() { restoreState "USB" "usb" || return 1 restoreState "SOUND" "sound" || return 1 restoreState "ADAPTER" "net" || return 1 + restoreState "VMPORT" "port" || return 1 restoreState "CPU_MODEL" "cpu" || return 1 restoreState "DISK_TYPE" "type" || return 1 @@ -238,6 +227,68 @@ addNetworkDriver() { return 0 } +addDisplayDriver() { + + local dir="$1" + local driver="$2" + local arch="$3" + local drivers="$4" + + local source="$drivers/qxl/$driver/$arch" + local destination="$dir/\$OEM\$/\$1/Drivers/QXL" + + # The legacy QXL package is named for XP but its x86 INF targets XP and + # later NT x86 releases, so it is also suitable to keep around for 2003 x86. + if [ ! -d "$source" ] && [[ "${arch,,}" == "x86" ]]; then + source="$drivers/qxl/xp/x86" + fi + + [ -d "$source" ] || return 0 + + local file + + for file in qxl.cat qxl.inf qxl.sys qxldd.dll; do + + if [ ! -f "$source/$file" ]; then + error "Failed to locate required QXL display driver file: $file" + return 1 + fi + + done + + mkdir -p "$destination" || return 1 + cp -Lr "$source/." "$destination" || return 1 + + return 0 +} + +addBalloonDriver() { + + local dir="$1" + local driver="$2" + local arch="$3" + local drivers="$4" + + local source="$drivers/Balloon/$driver/$arch" + local destination="$dir/\$OEM\$/\$1/Drivers/Balloon" + + local file + + for file in balloon.cat balloon.inf balloon.sys blnsvr.exe; do + + if [ ! -f "$source/$file" ]; then + error "Failed to locate required Balloon driver file: $file" + return 1 + fi + + done + + mkdir -p "$destination" || return 1 + cp -Lr "$source/." "$destination" || return 1 + + return 0 +} + addSIFEntry() { local file="$1" @@ -344,6 +395,8 @@ addLegacyDrivers() { extractDrivers "$drivers" || return 1 copyStorageDriver "$dir" "$target" "$driver" "$arch" "$drivers" || return 1 addNetworkDriver "$dir" "$driver" "$arch" "$drivers" || return 1 + addDisplayDriver "$dir" "$driver" "$arch" "$drivers" || return 1 + addBalloonDriver "$dir" "$driver" "$arch" "$drivers" || return 1 local file file=$(find "$target" -maxdepth 1 -type f -iname TXTSETUP.SIF -print -quit) || return 1 @@ -514,7 +567,7 @@ writeSIF() { ' WaitForReboot="No"' \ ' DriverSigningPolicy="Ignore"' \ ' NonDriverSigningPolicy="Ignore"' \ - ' OemPnPDriversPath="Drivers\viostor;Drivers\NetKVM;Drivers\sata"' \ + ' OemPnPDriversPath="Drivers\viostor;Drivers\NetKVM;Drivers\sata;Drivers\QXL;Drivers\Balloon"' \ ' NoWaitAfterTextMode=1' \ ' NoWaitAfterGUIMode=1' \ ' FileSystem=ConvertNTFS' \ @@ -522,13 +575,18 @@ writeSIF() { ' Hibernation="No"' \ '' \ '[GuiUnattended]' \ - ' OEMSkipRegional=1' ' OemSkipWelcome=1' " AdminPassword=\"$sifPassword\"" ' TimeZone=0' + ' OEMSkipRegional=1' \ + ' OemSkipWelcome=1' \ + " AdminPassword=\"$sifPassword\"" \ + ' TimeZone=4' if disabled "$AUTOLOGIN"; then - printf '%s\n' ' AutoLogon=No' + printf '%s\n' \ + ' AutoLogon=No' else printf '%s\n' \ - ' AutoLogon=Yes' ' AutoLogonCount=65432' + ' AutoLogon=Yes' \ + ' AutoLogonCount=65432' fi printf '%s\n' \ @@ -555,14 +613,23 @@ writeSIF() { '' \ '[URL]' \ ' Home_Page = http://www.google.com' \ - ' Search_Page = http://www.google.com' '' '[TerminalServices]' ' AllowConnections=1' '' + ' Search_Page = http://www.google.com' \ + '' \ + '[TerminalServices]' \ + ' AllowConnections=1' \ + '' } | unix2dos > "$target/WINNT.SIF" || return 1 if [[ "$driver" == "2k3" ]]; then { printf '%s\n' \ '[Components]' \ - ' TerminalServer=On' '' '[LicenseFilePrintData]' ' AutoMode=PerServer' ' AutoUsers=5' '' + ' TerminalServer=On' \ + '' \ + '[LicenseFilePrintData]' \ + ' AutoMode=PerServer' \ + ' AutoUsers=5' \ + '' } | unix2dos >> "$target/WINNT.SIF" || return 1 fi @@ -644,10 +711,41 @@ appendRegistry() { local dir="$1" local driver="$2" + if [[ "$driver" == "2k" || "$driver" == "xp" || "$driver" == "2k3" ]]; then + { + printf '%s\n' \ + '[HKEY_CURRENT_USER\Control Panel\Desktop]' \ + '"MenuShowDelay"="100"' \ + '' \ + '[HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics]' \ + '"MinAnimate"="0"' \ + '' \ + '[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer]' \ + '"link"=hex:00,00,00,00' '' + } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 + fi + + if [[ "$driver" == "xp" || "$driver" == "2k3" ]]; then + { + printf '%s\n' \ + '[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce]' \ + '"PowerPolicy"="Wscript.exe C:\\OEM\\NT5POWER.VBS"' '' + } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 + fi + if [[ "$driver" == "2k" ]]; then { printf '%s\n' \ - '[HKEY_USERS\.DEFAULT\Software\Microsoft\Windows\CurrentVersion\Runonce]' '"^SetupICWDesktop"=-' '' + '[HKEY_USERS\.DEFAULT\Software\Microsoft\Windows\CurrentVersion\Runonce]' \ + '"^SetupICWDesktop"=-' '' + } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 + fi + + if [[ "$driver" == "2k" || "$driver" == "2k3" ]]; then + { + printf '%s\n' \ + '[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer]' \ + '"NoActiveDesktop"=dword:00000001' '' } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 fi @@ -658,7 +756,10 @@ appendRegistry() { '@=dword:00000000' \ '' \ '[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ServerOOBE\SecurityOOBE]' \ - '"DontLaunchSecurityOOBE"=dword:00000000' '' + '"DontLaunchSecurityOOBE"=dword:00000000' \ + '' \ + '[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\R2Setup]' \ + '"cd2chain"=dword:00000000' '' } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 fi @@ -670,6 +771,10 @@ writeVBS() { local dir="$1" local username="$2" local shortcut="$3" + local driver="$4" + local balloon="$dir/\$OEM\$/balloon.cmd" + local balloonExe="$dir/\$OEM\$/\$1/Drivers/Balloon/blnsvr.exe" + local power="$dir/\$OEM\$/\$1/OEM/NT5POWER.VBS" # Locate the built-in Administrator by its RID 500 SID rather than its # localized display name, then rename that account to the requested username. @@ -721,9 +826,55 @@ writeVBS() { fi } | unix2dos > "$dir/\$OEM\$/install.vbs" || return 1 + if [[ "$driver" == "xp" || "$driver" == "2k3" ]]; then + mkdir -p "$(dirname "$power")" || return 1 + + { + printf '%s\n' \ + 'On Error Resume Next' \ + 'Set Shell = WScript.CreateObject("WScript.Shell")' \ + 'Set FSO = WScript.CreateObject("Scripting.FileSystemObject")' \ + 'PowerCfg = Shell.ExpandEnvironmentStrings("%SystemRoot%\System32\POWERCFG.EXE")' \ + '' \ + 'If FSO.FileExists(PowerCfg) Then' \ + ' Err.Clear' \ + ' Policy = Shell.RegRead("HKCU\Control Panel\PowerCfg\CurrentPowerPolicy")' \ + ' If Err.Number = 0 Then' \ + ' Cmd = Chr(34) & PowerCfg & Chr(34) & " /CHANGE " & Policy & " /NUMERICAL "' \ + ' For Each Setting In Array("/monitor-timeout-ac", "/monitor-timeout-dc", "/disk-timeout-ac", "/disk-timeout-dc", "/standby-timeout-ac", "/standby-timeout-dc")' \ + ' Shell.Run Cmd & Setting & " 0", 0, True' \ + ' Next' \ + ' End If' \ + 'End If' \ + '' + } | unix2dos > "$power" || return 1 + else + rm -f -- "$power" || return 1 + fi + + if [ -f "$balloonExe" ]; then + { + printf '%s\n' \ + '@echo off' \ + 'sc.exe query BalloonService >nul 2>&1' \ + 'if errorlevel 1 "C:\Drivers\Balloon\blnsvr.exe" -i' \ + '' + } | unix2dos > "$balloon" || return 1 + else + rm -f -- "$balloon" || return 1 + fi + { printf '%s\n' \ - '[COMMANDS]' '"REGEDIT /s install.reg"' '"Wscript install.vbs"' '' + '[COMMANDS]' \ + '"REGEDIT /s install.reg"' \ + '"Wscript install.vbs"' + + if [ -f "$balloon" ]; then + printf '%s\n' '"cmd /C balloon.cmd"' + fi + + printf '%s\n' '' } | unix2dos > "$dir/\$OEM\$/cmdlines.txt" || return 1 return 0 @@ -761,7 +912,28 @@ disableAutoReboot() { return 0 } -legacyInstall() { +prepareLegacyInstall() { + + local id="$1" + local iso="$2" + local dir="$3" + local desc="$4" + + case "${id,,}" in + "win9"* ) + prepareWin9xInstall "$id" "$iso" "$dir" "$desc" || return 1 ;; + "win2k"* ) + prepareSIFInstall "$iso" "$dir" "$desc" "2k" || return 1 ;; + "winxp"* ) + prepareSIFInstall "$iso" "$dir" "$desc" "xp" || return 1 ;; + "win2003"* ) + prepareSIFInstall "$iso" "$dir" "$desc" "2k3" || return 1 ;; + esac + + return 0 +} + +prepareSIFInstall() { local dir="$2" local desc="$3" @@ -882,7 +1054,7 @@ legacyInstall() { writeRegistry "$dir" "$shortcut" "$oem" "$regUsername" "$regPassword" || return 1 appendRegistry "$dir" "$driver" || return 1 - writeVBS "$dir" "$username" "$shortcut" || return 1 + writeVBS "$dir" "$username" "$shortcut" "$driver" || return 1 return 0 } @@ -997,6 +1169,1439 @@ validateLegacyEncoding() { return 0 } +createWin9xSystemImage() { + + local dir="$1" + local image="$2" + local desc="$3" + local source="$4" + local options="$5" + + local temp="$TMP/win9x-image" + local config="$temp/mtools.conf" + local autoexec="$temp/AUTOEXEC.BAT" + local msdos="$temp/MSDOS.SYS" + local tmp="${image}.tmp" + local size=$((261 * 255 * 63 * 512)) + local start=63 + local sectors=$((size / 512 - start)) + local offset=$((start * 512)) + local entry find_pid setup_dir + local setup="SETUP" + local fs attributes + local entries=() + + local msg="Creating system image..." + info "$msg" && html "$msg" + + rm -rf -- "$temp" || return 1 + mkdir -p "$temp" || return 1 + rm -f -- "$tmp" || return 1 + + # mtools can create a perfectly valid FAT32 filesystem, including its BPB, + # FSInfo sector and backup boot area, but its generic boot code cannot start + # Windows 9x. Microsoft FORMAT.COM carries the matching DOS FAT32 bootstrap + # as a three-sector template, so use the copy supplied by the installation + # media instead of embedding Microsoft's boot code in this script. + # + # Do not assume where FORMAT.COM lives. Windows 95 OSR2, Windows 98 and + # Windows Me media use different setup layouts, and customized media may move + # the file again. Search all FORMAT.COM copies and select the first one that + # contains a structurally valid FAT32 boot template. + local format="" + local template="" + local template_base=0 + local format_size sig boot_sig fsinfo_lead fsinfo_struct + local -a formats=() signatures=() + + mapfile -d '' formats < <( + find "$dir" -type f -iname 'FORMAT.COM' -print0 + ) + + find_pid=$! + + if ! wait "$find_pid"; then + error "Failed to enumerate FORMAT.COM files in $desc ISO image!" + return 1 + fi + + if (( ${#formats[@]} == 0 )); then + error "Failed to locate FORMAT.COM in $desc ISO image!" + return 1 + fi + + for format in "${formats[@]}"; do + + format_size=$(stat -c %s -- "$format") || return 1 + signatures=() + + # FAT32 places the filesystem type string at offset 0x52 in the extended + # BPB. Searching for only this standard marker intentionally avoids tying + # the code to the exact Windows 98 machine-code bytes that follow it; a + # Windows 95 OSR2 or Windows Me FORMAT.COM may contain a different loader. + mapfile -t signatures < <( + LC_ALL=C grep -aobF 'FAT32 ' "$format" | cut -d: -f1 + ) + + for sig in "${signatures[@]}"; do + + [[ "$sig" =~ ^[0-9]+$ ]] || continue + (( sig >= 0x52 )) || continue + + local candidate=$((sig - 0x52)) + + # The Microsoft Win9x FAT32 bootstrap occupies three 512-byte sectors. + # Reject truncated matches before probing the fixed signatures below. + (( candidate + 0x600 <= format_size )) || continue + + # Validate the candidate by FAT32 structure rather than by one particular + # release's loader bytes: + # + # +0x1fe boot-sector signature (55 aa) + # +0x200 FSInfo lead signature (RRaA) + # +0x3e4 FSInfo structure signature (rrAa) + # +0x3fe FSInfo-sector signature (55 aa) + # +0x5fe third boot-sector signature (55 aa) + # + # Requiring all five makes an accidental "FAT32 " string elsewhere in + # FORMAT.COM extremely unlikely to be mistaken for the boot template. + boot_sig=$(dd if="$format" bs=1 skip="$((candidate + 0x1fe))" count=2 status=none | xxd -p) || return 1 + [[ "$boot_sig" == "55aa" ]] || continue + + fsinfo_lead=$(dd if="$format" bs=1 skip="$((candidate + 0x200))" count=4 status=none | xxd -p) || return 1 + [[ "$fsinfo_lead" == "52526141" ]] || continue + + fsinfo_struct=$(dd if="$format" bs=1 skip="$((candidate + 0x3e4))" count=4 status=none | xxd -p) || return 1 + [[ "$fsinfo_struct" == "72724161" ]] || continue + + boot_sig=$(dd if="$format" bs=1 skip="$((candidate + 0x3fe))" count=2 status=none | xxd -p) || return 1 + [[ "$boot_sig" == "55aa" ]] || continue + + boot_sig=$(dd if="$format" bs=1 skip="$((candidate + 0x5fe))" count=2 status=none | xxd -p) || return 1 + [[ "$boot_sig" == "55aa" ]] || continue + + template="$format" + template_base="$candidate" + break 2 + + done + + done + + if [ -z "$template" ]; then + error "Failed to locate a Windows 9x FAT32 boot template in FORMAT.COM!" + return 1 + fi + + checkFreeSpace "$(dirname "$image")" "$size" || return 1 + + fs=$(stat -f -c %T "$(dirname "$tmp")") || return 1 + + if [[ "${fs,,}" == "btrfs" ]]; then + touch "$tmp" || return 1 + { chattr +C "$tmp"; } || : + attributes=$(lsattr "$tmp") || return 1 + if [[ "$attributes" != *"C"* ]]; then + error "Failed to disable COW for $desc system image $tmp on ${fs^^} filesystem!" + fi + fi + + truncate -s "$size" "$tmp" || return 1 + + printf 'drive w: file="%s" partition=1 fat_bits=32 cylinders=261 heads=255 sectors=63 mformat_only\n' \ + "$tmp" > "$config" || return 1 + + # Let mtools create all volume-specific FAT32 data. In particular, the BPB + # contains the geometry, FAT size, root cluster, FSInfo sector and backup boot + # sector locations for this image. Those values must survive the Microsoft + # boot-code transplant below, so FORMAT.COM is deliberately not passed to + # mformat as a whole-sector template. + if ! MTOOLSRC="$config" mpartition -I -c -a -T 0x0c -b "$start" -l "$sectors" w: || + ! MTOOLSRC="$config" mformat -F -m 0xf8 -H "$start" w:; then + + rm -f -- "$tmp" + error "Failed to create the $desc FAT32 system image!" + return 1 + fi + + # BIOS loads the MBR at physical address 0000:7c00, which is also the address + # where a partition boot sector (VBR) expects to run. We therefore cannot read + # the VBR directly on top of the MBR and then continue executing the MBR: + # INT 13h completes the disk transfer before returning, so the instructions + # following that interrupt would already have been replaced by VBR bytes. + # + # Relocate the complete 512-byte MBR from 0000:7c00 to 0000:0600 first. The + # far jump below continues at offset 0x1e in that relocated copy (0000:061e). + # Only then do we read CHS 0/1/1 -- LBA 63 with this 63-sector geometry -- back + # into 0000:7c00 and transfer control to the Microsoft FAT32 boot sector. + # + # DL is deliberately left untouched: BIOS supplies the boot drive number in + # DL, and the INT 13h read must use that same hard disk. + printf '%b' \ + '\xfa\x31\xc0\x8e\xd0\xbc\x00\x7c\x8e\xd8\x8e\xc0\xfb\xfc' \ + '\xbe\x00\x7c\xbf\x00\x06\xb9\x00\x01\xf3\xa5' \ + '\xea\x1e\x06\x00\x00' \ + '\x31\xc0\x8e\xc0\xbb\x00\x7c\xb8\x01\x02\xb9\x01\x00\xb6\x01' \ + '\xcd\x13\x72\x05\xea\x00\x7c\x00\x00\xcd\x18\xeb\xfe' | + dd of="$tmp" bs=1 seek=0 conv=notrunc status=none || return 1 + + # FAT32 stores the location of its backup boot sector in BPB_BkBootSec at + # offset 0x32. Read both the reserved-sector count and that pointer from the + # BPB generated by mformat rather than assuming Microsoft's usual sector 6. + # If a backup exists, patch it together with the primary boot area so recovery + # never falls back to mtools' non-Windows bootstrap. + local reserved_bytes backup_bytes reserved backup_sector backup_offset + local reserved_lo reserved_hi backup_lo backup_hi + local -a boot_targets=("$offset") + + reserved_bytes=$(dd if="$tmp" bs=1 skip="$((offset + 0x0e))" count=2 status=none | od -An -tu1) || return 1 + read -r reserved_lo reserved_hi <<< "$reserved_bytes" + [[ "$reserved_lo" =~ ^[0-9]+$ && "$reserved_hi" =~ ^[0-9]+$ ]] || return 1 + reserved=$((reserved_lo | reserved_hi << 8)) + + backup_bytes=$(dd if="$tmp" bs=1 skip="$((offset + 0x32))" count=2 status=none | od -An -tu1) || return 1 + read -r backup_lo backup_hi <<< "$backup_bytes" + [[ "$backup_lo" =~ ^[0-9]+$ && "$backup_hi" =~ ^[0-9]+$ ]] || return 1 + backup_sector=$((backup_lo | backup_hi << 8)) + + if (( backup_sector != 0 && backup_sector != 0xffff )); then + + # We copy three sectors of boot/FSInfo data starting at the backup pointer, + # therefore all three must fit inside the FAT32 reserved area. + if (( backup_sector + 2 >= reserved )); then + rm -f -- "$tmp" + error "Invalid FAT32 backup boot sector generated by mtools!" + return 1 + fi + + backup_offset=$((offset + backup_sector * 512)) + boot_targets+=("$backup_offset") + + fi + + # ms-sys' Windows 95/98/Me FAT32 writer does not overwrite the complete + # 1536-byte template. It deliberately preserves the volume-specific fields + # created by the formatter and replaces only these three ranges: + # + # 0x000..0x00a jump instruction + OEM identifier (11 bytes) + # 0x052..0x3e7 boot code + static FSInfo data (918 bytes) + # 0x3f0..0x5ff FSInfo trailer + second-stage boot code (528 bytes) + # + # Two regions are intentionally left untouched: + # + # 0x00b..0x051 FAT32 BPB/extended BPB generated by mformat + # 0x3e8..0x3ef current free-cluster and next-free hints from mformat + # + # The source offsets are relative to the FAT32 template we located inside + # FORMAT.COM, so neither the ISO layout nor FORMAT.COM's internal layout is + # hardcoded. Apply the same transplant to the backup boot area when present. + local target_base + + for target_base in "${boot_targets[@]}"; do + + dd if="$template" of="$tmp" bs=1 \ + skip="$template_base" seek="$target_base" count=11 \ + conv=notrunc status=none || return 1 + + dd if="$template" of="$tmp" bs=1 \ + skip="$((template_base + 0x52))" seek="$((target_base + 0x52))" count=918 \ + conv=notrunc status=none || return 1 + + dd if="$template" of="$tmp" bs=1 \ + skip="$((template_base + 0x3f0))" seek="$((target_base + 0x3f0))" count=528 \ + conv=notrunc status=none || return 1 + + # The Microsoft template intentionally leaves the FAT32 extended BPB alone. + # Set its BIOS drive number explicitly because QEMU exposes the system image as + # hard disk 0x80, regardless of what default mformat happened to write there. + printf '%b' '\x80' | + dd of="$tmp" bs=1 seek="$((target_base + 0x40))" conv=notrunc status=none || return 1 + + done + + # Verify both the partition table and the FAT32 structures after patching. + # The checks cover all three Microsoft boot sectors, not only sector zero, so + # a wrong FORMAT.COM match or an off-by-one copy cannot silently produce an + # image that merely looks bootable from its first 55 aa signature. + local active mbrsig vbrsig stage2sig fstype target_fsinfo_lead target_fsinfo_struct drive + active=$(dd if="$tmp" bs=1 skip=446 count=1 status=none | od -An -tu1 | tr -d ' ') || return 1 + mbrsig=$(dd if="$tmp" bs=1 skip=510 count=2 status=none | xxd -p) || return 1 + vbrsig=$(dd if="$tmp" bs=1 skip="$((offset + 0x1fe))" count=2 status=none | xxd -p) || return 1 + stage2sig=$(dd if="$tmp" bs=1 skip="$((offset + 0x5fe))" count=2 status=none | xxd -p) || return 1 + fstype=$(dd if="$tmp" bs=1 skip="$((offset + 0x52))" count=8 status=none) || return 1 + target_fsinfo_lead=$(dd if="$tmp" bs=1 skip="$((offset + 0x200))" count=4 status=none | xxd -p) || return 1 + target_fsinfo_struct=$(dd if="$tmp" bs=1 skip="$((offset + 0x3e4))" count=4 status=none | xxd -p) || return 1 + drive=$(dd if="$tmp" bs=1 skip="$((offset + 0x40))" count=1 status=none | od -An -tu1 | tr -d ' ') || return 1 + + if [[ "$active" != "128" || "$mbrsig" != "55aa" || "$vbrsig" != "55aa" || + "$stage2sig" != "55aa" || "$fstype" != "FAT32 " || + "$target_fsinfo_lead" != "52526141" || "$target_fsinfo_struct" != "72724161" || + "$drive" != "128" ]]; then + rm -f -- "$tmp" + error "Failed to make the $desc FAT32 system image bootable!" + return 1 + fi + + if (( backup_sector != 0 && backup_sector != 0xffff )); then + + vbrsig=$(dd if="$tmp" bs=1 skip="$((backup_offset + 0x1fe))" count=2 status=none | xxd -p) || return 1 + stage2sig=$(dd if="$tmp" bs=1 skip="$((backup_offset + 0x5fe))" count=2 status=none | xxd -p) || return 1 + fstype=$(dd if="$tmp" bs=1 skip="$((backup_offset + 0x52))" count=8 status=none) || return 1 + drive=$(dd if="$tmp" bs=1 skip="$((backup_offset + 0x40))" count=1 status=none | od -An -tu1 | tr -d ' ') || return 1 + + if [[ "$vbrsig" != "55aa" || "$stage2sig" != "55aa" || + "$fstype" != "FAT32 " || "$drive" != "128" ]]; then + rm -f -- "$tmp" + error "Failed to update the $desc FAT32 backup boot record!" + return 1 + fi + + fi + + # The CD boot image is not part of the Windows 9x installation contract. + # Retail, OEM and repacked discs may use a differently named El Torito image + # or omit an extracted floppy image entirely. The setup cabinets are a much + # better source for the DOS kernel files because Setup itself ships them. + # + # The exact cabinet containing COMMAND.COM and WINBOOT.SYS varies between + # Windows 9x releases, so do not hardcode a cabinet number. Search every + # PRECOPY cabinet first and only fall back to the remaining CABs for unusual + # OEM/repacked media. + # + # WINBOOT.SYS is the setup-media form of IO.SYS. Copy it as IO.SYS on the + # generated boot volume; COMMAND.COM keeps its original name. cabextract can + # follow multi-part cabinet sets automatically, which also covers a file that + # happens to span into the next cabinet. + local cab source_name target_name extracted + local cab_temp="$temp/cab" + local -a precopy_cabs=() other_cabs=() + + mapfile -d '' precopy_cabs < <( + find "$dir" -type f -iname 'PRECOPY*.CAB' -print0 + ) + + find_pid=$! + + if ! wait "$find_pid"; then + rm -f -- "$tmp" + error "Failed to enumerate Windows 9x setup cabinets!" + return 1 + fi + + mapfile -d '' other_cabs < <( + find "$dir" -type f -iname '*.CAB' ! -iname 'PRECOPY*.CAB' -print0 + ) + + find_pid=$! + + if ! wait "$find_pid"; then + rm -f -- "$tmp" + error "Failed to enumerate Windows 9x setup cabinets!" + return 1 + fi + + if (( ${#precopy_cabs[@]} == 0 && ${#other_cabs[@]} == 0 )); then + rm -f -- "$tmp" + error "Failed to locate Windows 9x setup cabinets in $desc ISO image!" + return 1 + fi + + for source_name in COMMAND.COM WINBOOT.SYS; do + + target_name="$source_name" + [[ "$source_name" == "WINBOOT.SYS" ]] && target_name="IO.SYS" + extracted="" + + for cab in "${precopy_cabs[@]}" "${other_cabs[@]}"; do + + rm -rf -- "$cab_temp" || return 1 + mkdir -p "$cab_temp" || return 1 + + # -F avoids unpacking a complete cabinet just to obtain one small system + # file. -L normalizes the extracted name so case differences in localized + # or repacked media do not matter; find below remains case-insensitive as + # an additional safeguard. A cabinet that simply lacks the requested file + # is not an error here: continue with the next candidate. + if ! cabextract -q -L -F "$source_name" -d "$cab_temp" "$cab" >/dev/null 2>&1; then + continue + fi + + extracted=$(find "$cab_temp" -type f -iname "$source_name" -print -quit) || return 1 + + if [ -n "$extracted" ] && [ -s "$extracted" ]; then + cp -f -- "$extracted" "$temp/$target_name" || return 1 + break + fi + + done + + if [ ! -s "$temp/$target_name" ]; then + rm -f -- "$tmp" + error "Failed to extract $source_name from the Windows 9x setup cabinets!" + return 1 + fi + + done + + rm -rf -- "$cab_temp" || : + + { + printf '%s\n' \ + '[Options]' \ + 'BootGUI=0' \ + 'BootDelay=0' \ + 'Logo=0' \ + '' + } | unix2dos > "$msdos" || return 1 + + # Copy the DOS system files before anything else. Older DOS boot sectors + # expect IO.SYS and MSDOS.SYS at the start of a freshly formatted volume. + MTOOLSRC="$config" mcopy "$temp/IO.SYS" w:/IO.SYS || return 1 + MTOOLSRC="$config" mcopy "$msdos" w:/MSDOS.SYS || return 1 + MTOOLSRC="$config" mcopy "$temp/COMMAND.COM" w:/COMMAND.COM || return 1 + + MTOOLSRC="$config" mattrib +h +s +r w:/IO.SYS w:/MSDOS.SYS || return 1 + + # Windows is started explicitly from AUTOEXEC.BAT so the DOS environment stays + # available for the mouse TSR. + + # Setup only needs its release-specific source directory. Keeping the rest + # of the optical-media root off C: avoids exposing unrelated CD contents in + # the installed system. Rename the retained source to C:\SETUP so it cannot + # be confused with the installed C:\WINDOWS directory. + setup_dir=$(find "$dir" -mindepth 1 -maxdepth 1 -type d -iname "$source" -print -quit) || return 1 + + if [ -z "$setup_dir" ]; then + rm -f -- "$tmp" + error "Failed to locate the $source Setup folder in $desc ISO image!" + return 1 + fi + + entries=("$setup_dir") + [ -d "$dir/OEM" ] && entries+=("$dir/OEM") + + for entry in "${entries[@]}"; do + + if ! MTOOLSRC="$config" mcopy -Q -s "$entry" w:/; then + rm -f -- "$tmp" + error "Failed to copy $desc file: $entry" + return 1 + fi + + done + + if ! MTOOLSRC="$config" mren "w:/$source" "w:/$setup" || + ! MTOOLSRC="$config" mattrib +h +s "w:/$setup"; then + rm -f -- "$tmp" + error "Failed to rename or hide the $desc setup folder in the system image!" + return 1 + fi + + { + printf '%s\n' \ + '@ECHO OFF' \ + 'IF EXIST C:\WINDOWS\WIN.COM GOTO WINDOWS' \ + 'ECHO.' \ + 'ECHO Starting Windows Setup, please wait...' \ + 'ECHO.' \ + "C:\\${setup}\\VBMOUSE.EXE >NUL" \ + "IF NOT EXIST C:\\${setup}\\XMSMMGR.EXE GOTO SETUP" \ + "IF NOT EXIST C:\\${setup}\\SMARTDRV.EXE GOTO SETUP" \ + "C:\\${setup}\\XMSMMGR.EXE >NUL" \ + "C:\\${setup}\\SMARTDRV.EXE C+ /Q 16384 16384 >NUL" \ + ':SETUP' \ + "C:\\${setup}\\SETUP.EXE $options" \ + 'GOTO END' \ + ':WINDOWS' \ + 'C:\WINDOWS\SYSTEM\VBMOUSE.EXE >NUL' \ + 'C:\WINDOWS\WIN.COM' \ + ':END' \ + '' + } | unix2dos > "$autoexec" || return 1 + + MTOOLSRC="$config" mcopy -o "$autoexec" w:/AUTOEXEC.BAT || return 1 + + if ! MTOOLSRC="$config" mdir "w:/$setup/SETUP.EXE" >/dev/null; then + rm -f -- "$tmp" + error "Failed to verify the $desc system image!" + return 1 + fi + + if [[ "${fs,,}" == "btrfs" ]]; then + attributes=$(lsattr "$tmp") || return 1 + if [[ "$attributes" != *"C"* ]]; then + warn "COW (copy on write) is not disabled for $desc system image $tmp on ${fs^^} filesystem!" + fi + fi + + if ! mv -f -- "$tmp" "$image"; then + rm -f -- "$tmp" + error "Failed to save $desc system image: $image" + return 1 + fi + + if ! setOwner "$image"; then + warn "Failed to set the owner for \"$image\" !" + fi + + rm -rf -- "$temp" || : + + return 0 +} + +stageWin9xDisplayDriver() { + + local target="$1" + local source="$2" + local desc="$3" + + local dest="$target/BOXV9X" + local file + + for file in boxv9x.inf boxvmini.drv boxvmini.vxd; do + + if [ ! -f "$source/$file" ]; then + error "Failed to locate required Windows 9x display driver file: $file" + return 1 + fi + + done + + rm -rf -- "$dest" || return 1 + mkdir -p "$dest" || return 1 + + if ! cp -f -- \ + "$source/boxv9x.inf" \ + "$source/boxvmini.drv" \ + "$source/boxvmini.vxd" \ + "$dest/"; then + + error "Failed to add the display driver to $desc setup files!" + return 1 + fi + + # The virtual display driver's DDC flag makes Win9x enumerate a Plug and + # Play monitor after the display driver starts. The unattended setup already + # selects the monitor and display mode, and BOXV9X carries a fixed mode list, + # so disable DDC in our staged copy to avoid a second monitor PnP pass. + if ! grep -Eq '^[[:space:]]*HKR,DEFAULT,DDC,,1[[:space:]]*$' "$dest/boxv9x.inf"; then + error "Failed to locate the DDC setting in the Windows 9x display driver!" + return 1 + fi + + if ! sed -i 's/HKR,DEFAULT,DDC,,1/HKR,DEFAULT,DDC,,0/' "$dest/boxv9x.inf" || + ! grep -Eq '^[[:space:]]*HKR,DEFAULT,DDC,,0[[:space:]]*$' "$dest/boxv9x.inf"; then + error "Failed to disable DDC in the Windows 9x display driver!" + return 1 + fi + + # BOXV9X is the source-media tag named by the upstream INF. Provide that tag + # beside the driver files so Setup never asks for a separate driver disk while + # installing the exact QEMU PCI match. + : > "$dest/BOXV9X" || return 1 + + return 0 +} + +integrateWin9xSetupMouse() { + + local dir="$1" + local desc="$2" + local driver="$3" + + local cab + cab=$(find "$dir" -maxdepth 1 -type f -iname 'MINI.CAB' -print -quit) || return 1 + + if [ -z "$cab" ]; then + error "Failed to locate MINI.CAB in $desc ISO image!" + return 1 + fi + + if [ ! -f "$driver" ]; then + error "Failed to locate the mouse driver for $desc MINI.CAB!" + return 1 + fi + + local temp="$TMP/win9x-mini" + local files="$temp/files" + local rebuilt="$temp/MINI.CAB" + local system + local -a entries=() + + rm -rf -- "$temp" || return 1 + mkdir -p "$files" || return 1 + + if ! cabextract -q -d "$files" "$cab"; then + rm -rf -- "$temp" || : + error "Failed to extract MINI.CAB from $desc ISO image!" + return 1 + fi + + system=$(find "$files" -type f -iname 'SYSTEM.INI' -print -quit) || return 1 + + if [ -z "$system" ]; then + rm -rf -- "$temp" || : + error "Failed to locate SYSTEM.INI in $desc MINI.CAB!" + return 1 + fi + + if ! cp -f -- "$driver" "$files/VBMOUSE.DRV"; then + rm -rf -- "$temp" || : + error "Failed to add the mouse driver to $desc MINI.CAB!" + return 1 + fi + + if ! grep -Eqi '^[[:space:]]*mouse\.drv[[:space:]]*=' "$system"; then + rm -rf -- "$temp" || : + error "Failed to locate the mouse driver setting in $desc MINI.CAB!" + return 1 + fi + + if ! dos2unix "$system" >/dev/null 2>&1 || + ! sed -i -E 's/^([[:space:]]*mouse\.drv[[:space:]]*=[[:space:]]*).*$/\1vbmouse.drv/I' "$system" || + ! unix2dos "$system" >/dev/null 2>&1; then + rm -rf -- "$temp" || : + error "Failed to configure the mouse driver in $desc mini-Windows setup!" + return 1 + fi + + if ! grep -Eqi '^[[:space:]]*mouse\.drv[[:space:]]*=[[:space:]]*vbmouse\.drv[[:space:]]*$' "$system"; then + rm -rf -- "$temp" || : + error "Failed to verify the mouse driver setting in $desc MINI.CAB!" + return 1 + fi + + mapfile -d '' entries < <(find "$files" -type f -print0) + + if (( ${#entries[@]} == 0 )); then + rm -rf -- "$temp" || : + error "Failed to enumerate files from $desc MINI.CAB!" + return 1 + fi + + if ! gcab -c -z -n "$rebuilt" "${entries[@]}" >/dev/null; then + rm -rf -- "$temp" || : + error "Failed to rebuild $desc MINI.CAB!" + return 1 + fi + + if ! cabextract -q -l "$rebuilt" >/dev/null 2>&1; then + rm -rf -- "$temp" || : + error "Failed to verify the rebuilt $desc MINI.CAB!" + return 1 + fi + + if ! mv -f -- "$rebuilt" "$cab"; then + rm -rf -- "$temp" || : + error "Failed to replace $desc MINI.CAB!" + return 1 + fi + + rm -rf -- "$temp" || : + + return 0 +} + +stageWin9xPasswordList() { + + local dir="$1" + local desc="$2" + local target="$dir/DOCKER.PWL" + + if ! base64 -d <<'EOF' | gzip -dc > "$target" +H4sIAAAAAAACA3vc1DqNkWEUMPwf2SCIifIghKSj0rXqtq9ePF4Yukz50lmmIyebzcx+z7vIcKxZ +6S7PpN8sV44Jzlq1dhnfPs2iBxq536xPbXn9o/X+LEe7Ra6/5kfHNX57zqt/9cKFnAV5Eoc1bvTP +Uts5yVhW6XnWRtOdrdK2swELaJAzsAIAAA== +EOF + then + error "Failed to create DOCKER.PWL in $desc setup files!" + return 1 + fi + + if [ "$(wc -c < "$target")" -ne 688 ]; then + error "Failed to verify DOCKER.PWL in $desc setup files!" + return 1 + fi + + return 0 +} + +stageWin9xQuiet() { + + local dir="$1" + local desc="$2" + local target="$dir/QUIET.EXE" + + if ! base64 -d <<'EOF' | gzip -dc > "$target" +H4sIAAAAAAACA/ONYiAbNDBQDgJcGRh8GBlRxB4wMDFyg8SYkAQFkDCDA4QGyrNApWE0gwIDXB8T +TKMAMg2nIH4AcjQYqA92CKDagw70SlIrSkAMRga4X1D8CwQJYERb8F90h4ADQ+exBjulUkG3BjuG +Egcgs4TZ7fVnt9dCIL5Bg51CCUeDHSdI8B2IwwrklIJUgKQFsxjC/ovuARqSBTTsABJNFFiBFEgn +BCDhhg1cA4q/AOJPAtj53q5Bfq4+xkZ6Lj4+IL57aolzfm5uYl6KT2ZeqiNQJDwzz7UiNRnIcq3I +LAkoyk9OLS5mGOkAAN77EFsABAAA +EOF + then + error "Failed to create QUIET.EXE in $desc setup files!" + return 1 + fi + + if [ "$(wc -c < "$target")" -ne 1024 ]; then + error "Failed to verify QUIET.EXE in $desc setup files!" + return 1 + fi + + return 0 +} + +stageWin98DMA() { + + local dir="$1" + local desc="$2" + local target="$dir/WIN98DMA.EXE" + + # Keep the Win98-only DMA helper embedded alongside QUIET.EXE so the driver + # archive stays unchanged. It only updates enumerated ESDI DiskDrive devices. + if ! base64 -d <<'EOF' | gzip -dc > "$target" +H4sIAAAAAAACA+1VTUgUcRR/s2610eKs4EKE4CgLUcTy//fpoWC2nUFEzcltFTKzzZ11dtsPm52N +FTosuEsaBGIdCjp4COpYERJ1UEiQKA28dPCQBB7Cuniw4/RmZ79U/LhWPni87/d+///+52371TQw +AGCF9cTDzpRGrq5/Vw1vDs41vGXa5hquKOEkN6gmBtRAjOsPxOMJjbspc2oqzoXjnNDh42KJoOx2 +AUgiQBtjhdd9mUix3xJYmENMNYClApCjwEVUhm4ph0u4pbJhKRY6KmVFG6QTHMD57Q7H7xDfgkaw +L7dN3K3JaQ3lVwZKZ9l4+Vh/w60GA1oAoJExHfm8/Zsg8m4zDSaMPLLlUabdqhxN9BfOxBf62Tbl +XYQ9+q/I7+vuGv5Z+0BwWaWILQJKI8eDgo8jozsfo5qbTh1VMpioAOEhArrmcujORxi5UIVPLnUE +HVbdOcblg84RlMMztdf7eno/0LXR5SnjWdLFlTP6aZeZNbocQnKz9dn8fG+5uW9Db+0ZDv/mz6My +4hiuLYGaVYy1pWSKoOyVheP018o5dNrMkZXzxHKdf0MZm+3WdR17L+Jt2KRIVcTMM2EV5k7NCq78 +HkFpN75LzLVLSobyhnZYwhqGy9fU6c6n+Ro2dxv7Dt91ORg2FzJVO07rQZVOh0Lr7oOdHDN63X95 +AGuzC6zwpWmVFeezC9o+vmlVW8p9ZLONWBhhFDDu3ABZUxo4gdqPKgx/3i0x3mggmQQhnLwlqOE7 +MgjtHm9KVeW4Fh3yJ+UgiPFU7JroE1pAqVhstgZz1w1W+JzoG9ti+b0o+CdRziAvIH9HXuN2F8df +Lh3WJDXRLyNcgE55wBtNJOVWeci0DJhoeEyrY1COoyWmC/bllKwOdQWiKdl0GT6frJU9rWLnJbHt +1El3MBoFj9DlkVoKxj9ODvM/oYbUkePkLGkmHSRG7pFx8py8IlPkE1kiv4mF1tNjtJn6aC99SJ/Q +93SGztO97fn30x8aoy/WAAoAAA== +EOF + then + error "Failed to create WIN98DMA.EXE in $desc setup files!" + return 1 + fi + + if [ "$(wc -c < "$target")" -ne 2560 ]; then + error "Failed to verify WIN98DMA.EXE in $desc setup files!" + return 1 + fi + + return 0 +} + +stageWin9xPostSetup() { + + local dir="$1" + local desc="$2" + local install="$3" + local target="$dir/POST9X.BAT" + local marker="$dir/POST9X.NEW" + + { + printf '%s\n' \ + '@ECHO OFF' \ + 'IF NOT EXIST C:\WINDOWS\POST9X.RDY GOTO END' \ + 'C:\WINDOWS\RUNDLL.EXE SETUPX.DLL,InstallHinfSection Win9x.PostDesktop 4 C:\WINDOWS\MSBATCH.INF' \ + 'DEL C:\WINDOWS\POST9X.RDY >NUL' + + if enabled "${LOG:-}"; then + printf '%s\n' 'CALL C:\OEM\install.bat > C:\OEM\install.log' + else + printf '%s\n' 'CALL C:\OEM\install.bat' + fi + + printf '%s\n' ':END' + + } | unix2dos > "$target" || { + error "Failed to create post-desktop setup script for $desc!" + return 1 + } + + # Stage a real marker file for Setup to copy into the Windows directory. + # Win9x.FirstLogon adds a WININIT.INI rename so the following reboot promotes + # POST9X.NEW to POST9X.RDY before the final desktop starts. + if ! printf 'Ready\r\n' > "$marker"; then + error "Failed to create post-desktop marker for $desc!" + return 1 + fi + + return 0 +} + +stageWin9xMouseFiles() { + + local dir="$1" + local desc="$2" + local source="$3" + local file + + for file in VBMOUSE.EXE VBMOUSE.DRV; do + + if [ ! -f "$source/$file" ]; then + error "Failed to locate $file!" + return 1 + fi + + if ! cp -f -- "$source/$file" "$dir/$file" || + ! cmp -s -- "$source/$file" "$dir/$file"; then + error "Failed to stage $file in $desc setup files!" + return 1 + fi + + done + + return 0 +} + +writeWin9xUserRegistry() { + + printf '%s\n' \ + '[Win9x.User]' \ + 'HKCU,"Control Panel\Desktop","SCRNSAVE.EXE",,""' \ + 'HKCU,"Control Panel\Desktop","ScreenSaveActive",,"0"' \ + 'HKCU,"Control Panel\Desktop","DragFullWindows",,"1"' \ + 'HKCU,"Control Panel\Desktop","MenuShowDelay",,"100"' \ + 'HKCU,"Control Panel\Desktop","FontSmoothing",,"1"' \ + 'HKCU,"Control Panel\Desktop","SmoothScroll",0x00010001,0' \ + 'HKCU,"Control Panel\Desktop\WindowMetrics","MinAnimate",,"0"' \ + 'HKCU,"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced","HideFileExt",0x00010001,0' \ + 'HKCU,"Software\Microsoft\Windows\CurrentVersion\Explorer\CabinetState","Settings",1,0c,00,02,00,0a,01,00,00,60,00,00,00' \ + 'HKCU,"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer","NoActiveDesktop",0x00010001,1' \ + 'HKCU,"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer","ClassicShell",0x00010001,1' \ + 'HKCU,"Software\Microsoft\Windows\CurrentVersion\Explorer","link",1,00,00,00,00' + + if ! disabled "$AUTOLOGIN"; then + printf '%s\n' \ + 'HKCU,"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer","NoLogOff",0x00010001,1' + fi +} + +writeWin9xMachineRegistry() { + + printf '%s\n' \ + '[Win9x.Machine]' \ + 'HKU,".DEFAULT\Control Panel\Desktop","FontSmoothing",,"1"' \ + 'HKLM,"Software\Microsoft\Windows\CurrentVersion\FS Templates",,,"Server"' \ + 'HKLM,"Software\Microsoft\Windows\CurrentVersion\FS Templates\Server","NameCache",1,a9,0a,00,00' \ + 'HKLM,"Software\Microsoft\Windows\CurrentVersion\FS Templates\Server","PathCache",1,40,00,00,00' \ + 'HKLM,"System\CurrentControlSet\Control\FileSystem","NameCache",1,a9,0a,00,00' \ + 'HKLM,"System\CurrentControlSet\Control\FileSystem","PathCache",1,40,00,00,00' \ + 'HKLM,"System\CurrentControlSet\Control\FileSystem","ReadAheadThreshold",1,00,00,01,00' \ + 'HKLM,"System\CurrentControlSet\Control\FileSystem\CDFS","CacheSize",1,ac,09,00,00' \ + 'HKLM,"System\CurrentControlSet\Control\FileSystem\CDFS","Prefetch",1,e4,00,00,00' +} + +writeWin98Registry() { + + printf '%s\n' \ + '[Win98.Power]' \ + 'HKLM,"Software\Microsoft\Windows\CurrentVersion\Controls Folder\PowerCfg\PowerPolicies\3","Policies",0x00000001,01,00,00,00,02,00,00,00,02,00,00,00,02,00,00,00,02,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,32,32,00,00,02,00,00,00,04,00,00,c0,00,00,00,00,02,00,00,00,04,00,00,c0,00,00,00,00' \ + 'HKU,".DEFAULT\Control Panel\PowerCfg","CurrentPowerPolicy",,"3"' \ + 'HKU,".DEFAULT\Control Panel\PowerCfg\PowerPolicies\3","Policies",0x00000001,01,00,00,00,02,00,00,00,01,00,00,00,00,00,00,00,02,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,32,32,00,00,04,00,00,00,05,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,01,64,64,64,64,00,00' \ + '' \ + '[Win98.ActiveSetup]' \ + 'HKLM,"SOFTWARE\Microsoft\Active Setup\Installed Components\>BatchSetupx",,,">Batch 98 - General Settings"' \ + 'HKLM,"SOFTWARE\Microsoft\Active Setup\Installed Components\>BatchSetupx","IsInstalled",0x00000001,01,00,00,00' \ + 'HKLM,"SOFTWARE\Microsoft\Active Setup\Installed Components\>BatchSetupx","Version",,"3,0,0,0"' \ + 'HKLM,"SOFTWARE\Microsoft\Active Setup\Installed Components\>BatchSetupx","StubPath",,"%25%\rundll.exe setupx.dll,InstallHinfSection Win98.Browser 4 %10%\msbatch.inf"' \ + 'HKLM,"SOFTWARE\Microsoft\Active Setup\Installed Components\>BatchStoragex",,,">Batch 98 - Storage Settings"' \ + 'HKLM,"SOFTWARE\Microsoft\Active Setup\Installed Components\>BatchStoragex","IsInstalled",0x00000001,01,00,00,00' \ + 'HKLM,"SOFTWARE\Microsoft\Active Setup\Installed Components\>BatchStoragex","Version",,"3,0,0,0"' \ + 'HKLM,"SOFTWARE\Microsoft\Active Setup\Installed Components\>BatchStoragex","StubPath",,"%10%\WIN98DMA.EXE"' \ + '' \ + '[Win98.Browser]' \ + 'AddReg=Win98.User,Win98.PowerUser' \ + 'DelReg=Win98.MSN,Win98.ICWDesktop' \ + 'DelFiles=Win98.Connect,Win98.ConnectAll' \ + '' \ + '[Win98.PowerUser]' \ + 'HKCU,"Control Panel\PowerCfg","CurrentPowerPolicy",,"3"' \ + 'HKCU,"Control Panel\PowerCfg\PowerPolicies\3","Policies",0x00000001,01,00,00,00,02,00,00,00,01,00,00,00,00,00,00,00,02,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,32,32,00,00,04,00,00,00,05,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,01,64,64,64,64,00,00' \ + '' \ + '[Win98.User]' \ + 'HKCU,"Software\Microsoft\Internet Connection Wizard","Completed",0x00010001,1' \ + 'HKCU,"Software\Microsoft\Internet Explorer\Main","Start Page",,"http://www.google.com"' \ + 'HKCU,"Software\Microsoft\Internet Explorer\Main","First Home Page",,"http://www.google.com"' \ + 'HKCU,"Software\Microsoft\Internet Explorer\Main","Default_Page_URL",,"http://www.google.com"' \ + 'HKCU,"Software\Microsoft\Internet Explorer\Main","Search Page",,"http://www.google.com"' \ + 'HKCU,"Software\Microsoft\Internet Explorer\Main","Search Bar",,"http://www.google.com"' \ + '' \ + '[Win98.Welcome]' \ + 'HKLM,"Software\Microsoft\Windows\CurrentVersion\Run","Welcome",,,' \ + '' \ + '[Win98.MSN]' \ + 'HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\{4B876A40-4EE8-11D1-811E-00C04FB98EEC}",,,' \ + 'HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\{88667D10-10F0-11D0-8150-00AA00BF8457}",,,' \ + '' \ + '[Win98.ICWDesktop]' \ + 'HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce","^SetupICWDesktop",,,' \ + 'HKCU,"SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce","^SetupICWDesktop",,,' \ + 'HKU,".DEFAULT\Software\Microsoft\Windows\CurrentVersion\RunOnce","^SetupICWDesktop",,,' \ + '' \ + '[Win98.Connect]' \ + 'connec~1.lnk' \ + '"Connect to the Internet.lnk"' \ + '' \ + '[Win98.ConnectAll]' \ + 'connec~1.lnk' \ + '"Connect to the Internet.lnk"' \ + '' \ + '[Win98.OnlineServices]' \ + 'americ~1.lnk' \ + 'at&two~1.lnk' \ + 'compus~1.lnk' \ + 'prodig~1.lnk' \ + 'themic~1.lnk' \ + 'aboutt~1.lnk' \ + 'abouto~1.txt' \ + 'services.txt' \ + '' \ + '[Win98.OnlineServicesFolder]' \ + '%10%\wininit.ini,DIRNUL,,"C:\WINDOWS\Desktop\Online~1=1"' +} + +writeWin9xAnswerFile() { + + local target="$1" + local id="$2" + local setup="$3" + local monitor="$4" + local batchHost="$5" + local batchUsername="$6" + local batchOrganization="$7" + local batchWorkgroup="$8" + local batchKey="$9" + local shortcut="${10}" + local install="${11}" + + local desktop="%10%\Desktop" + local addReg="OPKInstall,Win9x.Machine" + local copyFiles="Win9x.Mouse" + local firstLogonAddReg="Win9x.User" + local firstLogonDelReg="" + local firstLogonDelFiles="" + local firstLogonUpdateInis="" + local post="" + local quiet="" + + if ! disabled "$AUTOLOGIN"; then + copyFiles+=",Win9x.Password" + fi + + if [ -n "$install" ]; then + post="Y" + copyFiles+=",Win9x.Post" + fi + + if enabled "$shortcut"; then + quiet="Y" + copyFiles+=",Win9x.Quiet" + fi + + if [[ "${id,,}" == "win95"* || "${id,,}" == "win98"* || "${id,,}" == "win9x"* ]]; then + addReg+=",OEMDrivers" + fi + + if [[ "${id,,}" == "win98"* ]]; then + # Seed the machine/default Always On policy before user initialization. + # Active Setup reapplies the user policy after IE/Setup finish resetting + # per-user defaults, while the separate DMA helper runs after enumeration. + addReg+=",Win98.Power,Win98.ActiveSetup" + copyFiles+=",Win98.DMA" + fi + + if [[ "${id,,}" == "win98"* ]]; then + firstLogonAddReg+=",Win98.User,Win98.PowerUser" + [ -n "$firstLogonDelReg" ] && firstLogonDelReg+="," + firstLogonDelReg+="Win98.Welcome" + firstLogonDelFiles="Win98.OnlineServices" + firstLogonUpdateInis="Win98.OnlineServicesFolder" + fi + + # Cap the memory Win9x itself can address at 4 GiB without changing QEMU's + # RAM_SIZE. Setup may use SYSTEM.CB for its minimal protected-mode/fallback + # startup, so keep the limit in both SYSTEM.INI and SYSTEM.CB. + { + printf '%s\n' \ + '[BatchSetup]' \ + 'Version=3.0 (32-bit)' \ + '' \ + '[Version]' \ + "Signature=\"\$CHICAGO\$\"" \ + 'AdvancedINF=2.5' \ + 'LayoutFile=layout.inf' \ + '' \ + '[SourceDisksNames]' \ + '22="Windows Setup",,0' \ + '' \ + '[SourceDisksFiles]' \ + 'VBMOUSE.EXE=22' \ + 'VBMOUSE.DRV=22' + + if [[ "${id,,}" == "win98"* ]]; then + printf '%s\n' 'WIN98DMA.EXE=22' + fi + + if ! disabled "$AUTOLOGIN"; then + printf '%s\n' 'DOCKER.PWL=22' + fi + + if enabled "$quiet"; then + printf '%s\n' 'QUIET.EXE=22' + fi + + if enabled "$post"; then + printf '%s\n' \ + 'POST9X.BAT=22' \ + 'POST9X.NEW=22' + fi + + printf '%s\n' \ + '' \ + '[Install]' \ + "CopyFiles=$copyFiles" \ + 'UpdateInis=Win9x.SystemIni,Win9x.SystemCb' \ + "AddReg=$addReg" \ + '' \ + '[OPKInstall]' \ + 'HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion","ProductId",,"12345-OEM-1234567-12345"' \ + 'HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion","ProductKey",,"CDKey"' \ + "HKLM,\"SOFTWARE\Microsoft\Windows\CurrentVersion\",\"RegisteredOwner\",,\"$batchUsername\"" \ + "HKLM,\"SOFTWARE\Microsoft\Windows\CurrentVersion\",\"RegisteredOrganization\",,\"$batchOrganization\"" \ + "HKLM,\"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce\",\"Win9xSetup\",,\"%25%\\rundll.exe setupx.dll,InstallHinfSection Win9x.FirstLogon 4 %10%\\msbatch.inf\"" + + if enabled "$shortcut"; then + printf '%s\n' \ + 'HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\Run","SharedDrive",,"C:\WINDOWS\QUIET.EXE C:\WINDOWS\NET.EXE USE Z: \\host.lan\Data"' + fi + + if enabled "$post"; then + printf '%s\n' \ + 'HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\Run","PostSetup",,"C:\WINDOWS\COMMAND.COM /C C:\WINDOWS\POST9X.BAT"' + fi + + if ! disabled "$AUTOLOGIN"; then + printf '%s\n' \ + 'HKLM,"Network\Logon","UserProfiles",0x00010001,0' \ + 'HKLM,"Network\Logon","username",,"Docker"' + fi + + printf '%s\n' '' + writeWin9xMachineRegistry + + if [[ "${id,,}" == "win95"* || "${id,,}" == "win98"* || "${id,,}" == "win9x"* ]]; then + printf '%s\n' \ + '' \ + '[OEMDrivers]' \ + "HKLM,\"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\",\"OtherDevicePath\",,\"C:\\WINDOWS\\INF\\OTHER;C:\\$setup\"" + fi + + printf '%s\n' \ + '' \ + '[Win9x.FirstLogon]' \ + "AddReg=$firstLogonAddReg" + + if [ -n "$firstLogonDelReg" ]; then + printf '%s\n' "DelReg=$firstLogonDelReg" + fi + + if [ -n "$firstLogonDelFiles" ]; then + printf '%s\n' "DelFiles=$firstLogonDelFiles" + fi + + if enabled "$post"; then + [ -n "$firstLogonUpdateInis" ] && firstLogonUpdateInis+="," + firstLogonUpdateInis+="Win9x.PostMarker" + fi + + if enabled "$shortcut"; then + [ -n "$firstLogonUpdateInis" ] && firstLogonUpdateInis+="," + firstLogonUpdateInis+="Win9x.Shortcut" + fi + + if [ -n "$firstLogonUpdateInis" ]; then + printf '%s\n' "UpdateInis=$firstLogonUpdateInis" + fi + + printf '%s\n' '' + writeWin9xUserRegistry + + if [[ "${id,,}" == "win98"* ]]; then + printf '%s\n' '' + writeWin98Registry + fi + + if enabled "$post"; then + printf '%s\n' \ + '' \ + '[Win9x.PostDesktop]' \ + 'DelReg=Win9x.PostDesktopRun' \ + '' \ + '[Win9x.PostDesktopRun]' \ + 'HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\Run","PostSetup"' + fi + + printf '%s\n' \ + '' \ + '[Win9x.Mouse]' \ + 'VBMOUSE.EXE' \ + 'VBMOUSE.DRV' + + if ! disabled "$AUTOLOGIN"; then + printf '%s\n' \ + '' \ + '[Win9x.Password]' \ + 'DOCKER.PWL' + fi + + if enabled "$quiet"; then + printf '%s\n' \ + '' \ + '[Win9x.Quiet]' \ + 'QUIET.EXE' + fi + + if enabled "$post"; then + printf '%s\n' \ + '' \ + '[Win9x.Post]' \ + 'POST9X.BAT' \ + 'POST9X.NEW' \ + '' \ + '[Win9x.PostMarker]' \ + '%10%\wininit.ini,Rename,,"C:\WINDOWS\POST9X.RDY=C:\WINDOWS\POST9X.NEW"' + fi + + if [[ "${id,,}" == "win98"* ]]; then + printf '%s\n' \ + '' \ + '[Win98.DMA]' \ + 'WIN98DMA.EXE' + fi + + printf '%s\n' \ + '' \ + '[DestinationDirs]' \ + 'Win9x.Mouse=11' + + if enabled "$quiet"; then + printf '%s\n' 'Win9x.Quiet=10' + fi + + if ! disabled "$AUTOLOGIN"; then + printf '%s\n' 'Win9x.Password=10' + fi + + if enabled "$post"; then + printf '%s\n' 'Win9x.Post=10' + fi + + if [[ "${id,,}" == "win98"* ]]; then + printf '%s\n' 'Win98.DMA=10' + printf '%s\n' \ + 'Win98.Connect=10,Desktop' \ + 'Win98.ConnectAll=10,alluse~1\desktop' \ + 'Win98.OnlineServices=10,Desktop\Online~1' + fi + + if enabled "$shortcut"; then + printf '%s\n' \ + '' \ + '[Win9x.Shortcut]' + printf 'setup.ini, progman.groups,, "group1=""%s"""\n' "$desktop" + printf '%s\n' \ + 'setup.ini, group1,,"""Shared"",""\\host.lan\Data"",""%11%\SHELL32.DLL"",3,,,""Shared folder"""' + fi + + printf '%s\n' \ + '' \ + '[Win9x.SystemIni]' \ + '%10%\system.ini,boot,"mouse.drv=mouse.drv","mouse.drv=vbmouse.drv"' \ + '%10%\system.ini,386Enh,,"MaxPhysPage=100000"' \ + '%10%\system.ini,vcache,,"MaxFileCache=65536"' + + if ! disabled "$AUTOLOGIN"; then + printf '%s\n' '%10%\system.ini,"Password Lists",,"DOCKER=C:\WINDOWS\DOCKER.PWL"' + fi + + printf '%s\n' \ + '' \ + '[Win9x.SystemCb]' \ + '%10%\system.cb,386Enh,,"MaxPhysPage=100000"' \ + '' \ + '[Setup]' \ + 'Express=1' \ + 'InstallDir="C:\WINDOWS"' \ + 'InstallType=3' + + # When KEY is supplied, pass it through so retail media does not stop later + # for the product-key dialog. Leave it absent otherwise because some OEM + # media handles its product identification differently. + if [ -n "$batchKey" ]; then + printf 'ProductKey="%s"\n' "$batchKey" + fi + + printf '%s\n' \ + 'EBD=0' \ + 'ShowEula=0' \ + 'Network=1' \ + 'DevicePath=1' \ + 'NoPrompt2Boot=1' \ + 'TimeZone=Pacific' \ + '' \ + '[OptionalComponents]' + + if [[ "${id,,}" == "win98"* ]]; then + printf '%s\n' \ + '"America Online"=0' \ + '"AT&T WorldNet Service"=0' \ + '"CompuServe"=0' \ + '"Prodigy Internet"=0' + fi + + printf '%s\n' \ + '"The Microsoft Network"=0' \ + '"Online Services"=0' \ + '' \ + '[NameAndOrg]' \ + "Name=\"$batchUsername\"" \ + "Org=\"$batchOrganization\"" \ + 'Display=0' \ + '' \ + '[System]' \ + "DisplChar=16,$WIDTH,$HEIGHT" \ + "Monitor=\"$monitor\"" \ + '' \ + '[Network]' \ + "ComputerName=\"$batchHost\"" \ + "Workgroup=\"$batchWorkgroup\"" \ + 'PrimaryLogon=Windows' \ + 'Display=0' \ + '' + } | unix2dos > "$target/MSBATCH.INF" || return 1 + + return 0 +} + +patchWin9xSetupFiles() { + + local id="$1" + local target="$2" + local desc="$3" + local patcher="$4" + local mouse="$5" + local display="$6" + + chmod 755 "$patcher" || { + error "Failed to make Patcher9x executable!" + return 1 + } + + local msg="Patching Windows setup..." + info "$msg" && html "$msg" + + local patch_output + + if ! patch_output=$("$patcher" -auto -unselect creg "$target" 2>&1); then + [ -z "$patch_output" ] || printf '%s\n' "$patch_output" >&2 + error "Failed to patch $desc setup files!" + return 1 + fi + + if [[ "${id,,}" == "win95"* || "${id,,}" == "win98"* || "${id,,}" == "win9x"* ]]; then + stageWin9xDisplayDriver "$target" "$display" "$desc" || return 1 + + if ! mv -f -- \ + "$target/BOXV9X/boxv9x.inf" \ + "$target/BOXV9X/boxvmini.drv" \ + "$target/BOXV9X/boxvmini.vxd" \ + "$target/"; then + error "Failed to stage the Windows 9x display driver in the setup source!" + return 1 + fi + + rm -rf -- "$target/BOXV9X" || return 1 + : > "$target/BOXV9X" || return 1 + fi + + if [[ "${id,,}" == "win98"* ]]; then + integrateWin9xSetupMouse "$target" "$desc" "$mouse/VBMOUSE.DRV" || return 1 + fi + + return 0 +} + +prepareWin9xInstall() { + + local id="$1" + local dir="$3" + local desc="$4" + + local folder monitor="Plug and Play Monitor" options="/IS /IQ /IT" target + local setup="SETUP" + local shortcut="Y" + + if disabled "$SHORTCUT" || disabled "${SAMBA:-Y}"; then + shortcut="N" + fi + + if [ -n "$DOMAIN" ]; then + error "The DOMAIN variable is not supported for $desc!" + return 1 + fi + + case "${id,,}" in + "win95"* ) + folder="WIN95" + monitor="Plug and Play Monitor (VESA DDC)" ;; + "win98"* ) + folder="WIN98" + monitor="Plug and Play Monitor (VESA DDC)" + options="/P J /IE /NF $options" ;; + "win9x"* ) + folder="WIN9X" + options="/IE /NF $options" ;; + * ) + return 0 ;; + esac + + target=$(find "$dir" -maxdepth 1 -type d -iname "$folder" -print -quit) || return 1 + + if [ -z "$target" ]; then + error "Failed to locate the $folder Setup folder in $desc ISO image!" + return 1 + fi + + [ -z "$WIDTH" ] && WIDTH="1280" + [ -z "$HEIGHT" ] && HEIGHT="720" + + validateResolution "WIDTH" "$WIDTH" 320 || return 1 + validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 + + # Express setup still needs concrete identity values. If NameAndOrg is absent, + # Windows 9x stops during GUI setup for the user's name/company; if Network is + # missing the identity fields, it later stops for the computer name/workgroup. + # Win9x has no useful "*" computer-name generator, so provide a valid default + # when HOST was not configured. + local host="${HOST:-Docker}" + local username="${USERNAME:-Docker}" + local workgroup="${WORKGROUP:-WORKGROUP}" + + if ! disabled "$AUTOLOGIN"; then + username="Docker" + fi + + validateComputerName "$host" || return 1 + + # Reuse the normal OEM-folder preparation so /OEM and COMMAND behave like the + # other Windows paths, but place the user payload directly at C:\OEM in the + # generated Win9x system image. + local win9x_oem="$dir/OEM" + local install="" + + if ! addFolder "$dir" "win9x"; then + error "Failed to add OEM folder to image!" + return 1 + fi + + if [ -d "$win9x_oem" ]; then + install=$(find "$win9x_oem" -maxdepth 1 -type f -iname install.bat -print -quit) || return 1 + fi + + # MSBATCH.INF and WINNT.SIF both use quoted INF-style values. Reuse the SIF + # escaping helper so quotes and percent signs cannot alter the answer file. + local batchHost batchUsername batchOrganization batchWorkgroup batchKey="" + batchHost=$(escapeSIFValue "$host") || return 1 + batchUsername=$(escapeSIFValue "$username") || return 1 + batchOrganization=$(escapeSIFValue "$APP for $ENGINE") || return 1 + batchWorkgroup=$(escapeSIFValue "$workgroup") || return 1 + + if [ -n "$KEY" ]; then + batchKey=$(escapeSIFValue "$KEY") || return 1 + fi + + if ! disabled "$AUTOLOGIN"; then + stageWin9xPasswordList "$target" "$desc" || return 1 + fi + + if enabled "$shortcut"; then + stageWin9xQuiet "$target" "$desc" || return 1 + fi + + if [ -n "$install" ]; then + stageWin9xPostSetup "$target" "$desc" "$install" || return 1 + fi + + if [[ "${id,,}" == "win98"* ]]; then + stageWin98DMA "$target" "$desc" || return 1 + fi + + writeWin9xAnswerFile \ + "$target" "$id" "$setup" "$monitor" \ + "$batchHost" "$batchUsername" "$batchOrganization" "$batchWorkgroup" \ + "$batchKey" "$shortcut" "$install" || return 1 + + # Reuse the same driver archive extraction used by the XP/2003 path. All + # Windows 9x support files live together under win9x/ in that archive. + local drivers="/tmp/drivers" + local win9x="$drivers/win9x" + local patcher="$win9x/patcher9x/patcher9x" + local mouse="$win9x/mouse" + local display="$win9x/boxv9x" + + extractDrivers "$drivers" || return 1 + + if [ ! -f "$patcher" ]; then + rm -rf "$drivers" || : + error "Failed to locate Patcher9x!" + return 1 + fi + + if [ ! -f "$mouse/VBMOUSE.EXE" ] || [ ! -f "$mouse/VBMOUSE.DRV" ]; then + rm -rf "$drivers" || : + error "Failed to locate required Windows 9x mouse drivers!" + return 1 + fi + + if ! patchWin9xSetupFiles "$id" "$target" "$desc" "$patcher" "$mouse" "$display"; then + rm -rf "$drivers" || : + return 1 + fi + + if ! stageWin9xMouseFiles "$target" "$desc" "$mouse"; then + rm -rf "$drivers" || : + return 1 + fi + + # Do not copy optical-media AutoRun metadata onto the system disk; Explorer + # otherwise treats C: like the installation CD and runs its default action. + find "$dir" -maxdepth 1 -type f -iname 'AUTORUN.INF' -delete || return 1 + + # Build the hard-disk system image from the setup files themselves. Do not rely + # on a particular El Torito floppy-image filename: some perfectly valid Win9x + # discs expose a differently named boot image, while the required DOS system + # files are already present in the installation cabinets. + if ! createWin9xSystemImage "$dir" "$TMP/windows.img" "$desc" "$folder" "$options"; then + rm -rf "$drivers" || : + return 1 + fi + + rm -rf "$drivers" || : + SYSTEM="$TMP/windows.img" + + return 0 +} + +isReactOSLiveCD() { + + local iso="$1" + local files + + files=$(bsdtar -tf "$iso" 2>/dev/null) || return 1 + + ! grep -Ei '(^|/)reactos/txtsetup\.sif(;1)?$' <<< "$files" >/dev/null +} + detectReactOS() { local dir="$1"