diff --git a/src/answer.sh b/src/answer.sh index 63800e1d..3ac67e41 100644 --- a/src/answer.sh +++ b/src/answer.sh @@ -1643,100 +1643,6 @@ validateDomainName() { return 0 } -validateLegacyText() { - - local name="$1" - local value="$2" - local desc="${3:-}" - - local suffix="" - [ -n "$desc" ] && suffix=" for $desc" - - if [[ "$value" =~ [[:cntrl:]] ]]; then - error "The $name variable cannot contain control characters$suffix!" - return 1 - fi - - if [[ "$value" == *'"'* ]]; then - error "The $name variable cannot contain double quotes$suffix!" - return 1 - fi - - return 0 -} - -validateLegacyUsername() { - - local value="$1" - local desc="${2:-}" - - local suffix="" - [ -n "$desc" ] && suffix=" for $desc" - - if [ -z "$value" ]; then - error "The USERNAME variable cannot be empty$suffix!" - return 1 - fi - - if [ "${#value}" -gt 20 ]; then - error "The USERNAME variable cannot contain more than 20 characters$suffix!" - return 1 - fi - - if [[ "$value" =~ [[:cntrl:]] ]]; then - error "The USERNAME variable cannot contain control characters$suffix!" - return 1 - fi - - case "$value" in - *'"'* | *'/'* | *\\* | *'['* | *']'* | *':'* | *';'* | *'|'* | *'='* | \ - *','* | *'+'* | *'*'* | *'?'* | *'<'* | *'>'* | *'%'* ) - error "The USERNAME variable contains unsupported characters$suffix!" - return 1 ;; - esac - - if [[ "$value" == *"." ]]; then - error "The USERNAME variable cannot end with a period$suffix!" - return 1 - fi - - if [[ "$value" =~ ^[.[:space:]]+$ ]]; then - error "The USERNAME variable cannot consist only of spaces or periods$suffix!" - return 1 - fi - - case "${value^^}" in - - "NONE" ) - error "The USERNAME value \"NONE\" is reserved by Windows$suffix!" - return 1 ;; - - "ADMINISTRATOR" | "GUEST" | "DEFAULTACCOUNT" | "WDAGUTILITYACCOUNT" | "WSIACCOUNT" ) - error "The USERNAME value \"$value\" is reserved for a built-in Windows account$suffix!" - return 1 ;; - - esac - - return 0 -} - -validateLegacyEncoding() { - - local name="$1" - local value="$2" - local desc="${3:-}" - - local suffix="" - [ -n "$desc" ] && suffix=" for $desc" - - if LC_ALL=C grep -q '[^ -~]' <<< "$value"; then - error "The $name variable may only contain printable ASCII characters$suffix!" - return 1 - fi - - return 0 -} - prepareDomainAccount() { local domain="$1" @@ -2266,731 +2172,4 @@ finalizeSetupScript() { return 0 } -escapeSIFValue() { - - local s="$1" - - s=${s//%/%%} - s=${s//\"/\"\"} - - printf '%s' "$s" - return 0 -} - -escapeRegistryValue() { - - printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -} - -extractDrivers() { - - local drivers="$1" - - rm -rf "$drivers" || return 1 - mkdir -p "$drivers" || return 1 - - if ! bsdtar -xf /var/drivers.txz -C "$drivers"; then - error "Failed to extract drivers!" - return 1 - fi - - return 0 -} - -copyStorageDriver() { - - local dir="$1" - local target="$2" - local driver="$3" - local arch="$4" - local drivers="$5" - - local destination="$dir/\$OEM\$/\$1/Drivers/viostor" - - if [ ! -f "$drivers/viostor/$driver/$arch/viostor.sys" ]; then - error "Failed to locate required storage drivers!" - return 1 - fi - - cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$target" || return 1 - - mkdir -p "$destination" || return 1 - cp -L "$drivers/viostor/$driver/$arch/viostor.cat" "$destination" || return 1 - cp -L "$drivers/viostor/$driver/$arch/viostor.inf" "$destination" || return 1 - cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$destination" || return 1 - - return 0 -} - -addNetworkDriver() { - - local dir="$1" - local driver="$2" - local arch="$3" - local drivers="$4" - - local destination="$dir/\$OEM\$/\$1/Drivers/NetKVM" - - if [ ! -f "$drivers/NetKVM/$driver/$arch/netkvm.sys" ]; then - error "Failed to locate required network drivers!" - return 1 - fi - - mkdir -p "$destination" || return 1 - cp -L "$drivers/NetKVM/$driver/$arch/netkvm.cat" "$destination" || return 1 - cp -L "$drivers/NetKVM/$driver/$arch/netkvm.inf" "$destination" || return 1 - cp -L "$drivers/NetKVM/$driver/$arch/netkvm.sys" "$destination" || return 1 - - return 0 -} - -addSIFEntry() { - - local file="$1" - local section="$2" - local entry="$3" - - local header="[$section]" - local line ending="lf" - - line=$(grep -Fnx -m 1 "$header" "$file" | cut -d: -f1) || line="" - - if [ -z "$line" ]; then - line=$(grep -Fnx -m 1 "$header"$'\r' "$file" | cut -d: -f1) || line="" - ending="crlf" - fi - - if [ -z "$line" ]; then - error "Failed to locate section \"$header\" in \"$file\" !" - return 1 - fi - - local rc=0 - grep -Fqx -e "$entry" -e "$entry"$'\r' "$file" || rc=$? - - if (( rc == 0 )); then - return 0 - fi - - if (( rc != 1 )); then - error "Failed to inspect section \"$header\" in \"$file\" !" - return 1 - fi - - if [[ "$ending" == "crlf" ]]; then - printf '%s\r\n' "$entry" - else - printf '%s\n' "$entry" - fi | sed -i "${line}r /dev/stdin" "$file" || return 1 - - return 0 -} - -patchStorageDriver() { - - local file="$1" - local arch="$2" - - # Text-mode setup reads TXTSETUP.SIF before Plug and Play is available, so the - # VirtIO storage service and hardware IDs must be registered there explicitly. - addSIFEntry "$file" "SCSI.Load" 'viostor=viostor.sys,4' || return 1 - addSIFEntry "$file" "SourceDisksFiles.$arch" 'viostor.sys=1,,,,,,4_,4,1,,,1,4' || return 1 - addSIFEntry "$file" "SCSI" 'viostor="Red Hat VirtIO SCSI Disk Device"' || return 1 - addSIFEntry "$file" "HardwareIdsDatabase" 'PCI\VEN_1AF4&DEV_1001&SUBSYS_00000000="viostor"' || return 1 - addSIFEntry "$file" "HardwareIdsDatabase" 'PCI\VEN_1AF4&DEV_1001&SUBSYS_00020000="viostor"' || return 1 - addSIFEntry "$file" "HardwareIdsDatabase" 'PCI\VEN_1AF4&DEV_1001&SUBSYS_00021AF4="viostor"' || return 1 - - return 0 -} - -addSataDriver() { - - local dir="$1" - local target="$2" - local arch="$3" - local drivers="$4" - local file="$5" - - local destination="$dir/\$OEM\$/\$1/Drivers/sata" - - if [ ! -d "$drivers/sata/xp/$arch" ]; then - error "Failed to locate required SATA drivers!" - return 1 - fi - - mkdir -p "$destination" || return 1 - cp -Lr "$drivers/sata/xp/$arch/." "$destination" || return 1 - cp -Lr "$drivers/sata/xp/$arch/." "$target" || return 1 - - addSIFEntry "$file" "SCSI.Load" 'iaStor=iaStor.sys,4' || return 1 - addSIFEntry "$file" "FileFlags" 'iaStor.sys = 16' || return 1 - addSIFEntry "$file" "SourceDisksFiles.$arch" 'iaStor.cat = 1,,,,,,,1,0,0' || return 1 - addSIFEntry "$file" "SourceDisksFiles.$arch" 'iaStor.inf = 1,,,,,,,1,0,0' || return 1 - addSIFEntry "$file" "SourceDisksFiles.$arch" 'iaStor.sys = 1,,,,,,4_,4,1,,,1,4' || return 1 - addSIFEntry "$file" "SourceDisksFiles.$arch" 'iaStor.sys = 1,,,,,,,1,0,0' || return 1 - addSIFEntry "$file" "SourceDisksFiles.$arch" 'iaahci.cat = 1,,,,,,,1,0,0' || return 1 - addSIFEntry "$file" "SourceDisksFiles.$arch" 'iaAHCI.inf = 1,,,,,,,1,0,0' || return 1 - addSIFEntry "$file" "SCSI" 'iaStor="Intel(R) SATA RAID/AHCI Controller"' || return 1 - addSIFEntry "$file" "HardwareIdsDatabase" 'PCI\VEN_8086&DEV_2922&CC_0106="iaStor"' || return 1 - - return 0 -} - -addLegacyDrivers() { - - local dir="$1" - local target="$2" - local driver="$3" - local arch="$4" - local drivers="$5" - - local msg="Adding drivers to image..." - info "$msg" && html "$msg" - - extractDrivers "$drivers" || return 1 - copyStorageDriver "$dir" "$target" "$driver" "$arch" "$drivers" || return 1 - addNetworkDriver "$dir" "$driver" "$arch" "$drivers" || return 1 - - local file - file=$(find "$target" -maxdepth 1 -type f -iname TXTSETUP.SIF -print -quit) || return 1 - - if [ -z "$file" ]; then - error "The file TXTSETUP.SIF could not be found!" - return 1 - fi - - patchStorageDriver "$file" "$arch" || return 1 - addSataDriver "$dir" "$target" "$arch" "$drivers" "$file" || return 1 - - if ! rm -rf "$drivers"; then - warn "failed to clean temporary driver files!" - fi - - return 0 -} - -setLegacyKey() { - - local target="$1" - local driver="$2" - local arch="$3" - local desc="$4" - - local setup pid file block - setup=$(find "$target" -maxdepth 1 -type f -iname setupp.ini -print -quit) || return 1 - - [[ -n "$setup" ]] || return 0 - [[ -z "$KEY" ]] || return 0 - - pid=$(<"$setup") || return 1 - pid="${pid%$'\r'}" - - if [[ "$driver" == "2k" ]]; then - [ "${#pid}" -ge 3 ] || return 0 - echo "${pid::-3}270" > "$setup" || : - return 0 - fi - - if [[ "$pid" == *"270" ]]; then - warn "this version of $desc requires a volume license key (VLK), it will ask for one during installation." - return 0 - fi - - file=$(find "$target" -maxdepth 1 -type f -iname PID.INF -print -quit) || return 1 - - if [[ -n "$file" ]]; then - - local key="" - - # Prefer a staging or OEM key already shipped on the media before falling - # back to Microsoft's documented generic installation keys. - if [[ "$driver" == "2k3" ]]; then - - block=$(grep -i -A 2 "StagingKey" "$file") || block="" - - if [ -n "$block" ]; then - key=$(printf '%s\n' "$block" | tail -n 2 | head -n 1) || key="" - fi - - else - - key="${pid: -8:5}" - - if [[ "${pid^^}" == *"OEM" ]]; then - - block=$(grep -i -A 2 "$key" "$file") || block="" - - else - - block=$(grep -i -m 1 -A 2 "$key" "$file") || block="" - - fi - - if [ -n "$block" ]; then - key=$(printf '%s\n' "$block" | tail -n 2 | head -n 1) || key="" - fi - - key="${key#*= }" - - fi - - if [ -n "$key" ]; then - key="${key%$'\r'}" - [[ "${#key}" == "29" ]] && KEY="$key" - fi - - fi - - [[ -n "$KEY" ]] && return 0 - - # These are NOT pirated keys, they come from official MS documentation. - - case "${driver,,}" in - "xp" ) - - if [[ "${arch,,}" == "x86" ]]; then - # Windows XP Professional x86 generic trial key (no activation) - KEY="DR8GV-C8V6J-BYXHG-7PYJR-DB66Y" - else - # Windows XP Professional x64 generic trial key (no activation) - KEY="B2RBK-7KPT9-4JP6X-QQFWM-PJD6G" - fi ;; - - "2k3" ) - - if [[ "${arch,,}" == "x86" ]]; then - # Windows Server 2003 Standard x86 generic trial key (no activation) - KEY="QKDCQ-TP2JM-G4MDG-VR6F2-P9C48" - else - # Windows Server 2003 Standard x64 generic trial key (no activation) - KEY="P4WJG-WK3W7-3HM8W-RWHCK-8JTRY" - fi ;; - - esac - - if [ "${#pid}" -ge 3 ]; then - echo "${pid::-3}000" > "$setup" || : - fi - - return 0 -} - -writeCommand() { - - local install="$1" - - [ -f "$install" ] || return 0 - - if enabled "${LOG:-}"; then - printf '%s' "\"Script\"=\"cmd /C start \\\"Install\\\" cmd.exe /D /C \\\"\\\"C:\\\\OEM\\\\install.bat\\\" > \\\"C:\\\\OEM\\\\install.log\\\" 2>&1\\\"\"" - else - printf '%s' "\"Script\"=\"cmd /C start \\\"Install\\\" cmd.exe /D /C \\\"\\\"C:\\\\OEM\\\\install.bat\\\"\\\"\"" - fi - - return 0 -} - -writeSIF() { - - local target="$1" - local driver="$2" - local product="$3" - local sifHost="$4" - local sifUsername="$5" - local sifPassword="$6" - local sifOrganization="$7" - local sifWorkgroup="$8" - - find "$target" -maxdepth 1 -type f -iname winnt.sif -delete || return 1 - - { - printf '%s\n' \ - '[Data]' \ - ' AutoPartition=1' \ - ' MsDosInitiated="0"' \ - ' UnattendedInstall="Yes"' \ - ' AutomaticUpdates="Yes"' \ - '' \ - '[Unattended]' \ - ' UnattendSwitch=Yes' \ - ' UnattendMode=FullUnattended' \ - ' OemSkipEula=Yes' \ - ' OemPreinstall=Yes' \ - ' Repartition=Yes' \ - ' WaitForReboot="No"' \ - ' DriverSigningPolicy="Ignore"' \ - ' NonDriverSigningPolicy="Ignore"' \ - ' OemPnPDriversPath="Drivers\viostor;Drivers\NetKVM;Drivers\sata"' \ - ' NoWaitAfterTextMode=1' \ - ' NoWaitAfterGUIMode=1' \ - ' FileSystem=ConvertNTFS' \ - ' ExtendOemPartition=0' \ - ' Hibernation="No"' \ - '' \ - '[GuiUnattended]' \ - ' OEMSkipRegional=1' ' OemSkipWelcome=1' " AdminPassword=\"$sifPassword\"" ' TimeZone=0' - - if disabled "$AUTOLOGIN"; then - printf '%s\n' ' AutoLogon=No' - else - printf '%s\n' \ - ' AutoLogon=Yes' ' AutoLogonCount=65432' - fi - - printf '%s\n' \ - '' \ - '[UserData]' \ - " FullName=\"$sifUsername\"" \ - " ComputerName=\"$sifHost\"" \ - " OrgName=\"$sifOrganization\"" \ - " $product" \ - '' \ - '[Identification]' \ - " JoinWorkgroup = \"$sifWorkgroup\"" \ - '' \ - '[Display]' \ - ' BitsPerPel=32' \ - " XResolution=$WIDTH" \ - " YResolution=$HEIGHT" \ - '' \ - '[Networking]' \ - ' InstallDefaultComponents=Yes' \ - '' \ - '[Branding]' \ - ' BrandIEUsingUnattended=Yes' \ - '' \ - '[URL]' \ - ' Home_Page = http://www.google.com' \ - ' 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' '' - } | unix2dos >> "$target/WINNT.SIF" || return 1 - fi - - return 0 -} - -writeRegistry() { - - local dir="$1" - local shortcut="$2" - local oem="$3" - local regUsername="$4" - local regPassword="$5" - - { - printf '%s\n' \ - 'Windows Registry Editor Version 5.00' \ - '' \ - '[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Security]' \ - '"FirstRunDisabled"=dword:00000001' \ - '"UpdatesDisableNotify"=dword:00000001' \ - '"FirewallDisableNotify"=dword:00000001' \ - '"AntiVirusDisableNotify"=dword:00000001' \ - '' \ - '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\wscsvc]' \ - '"Start"=dword:00000004' \ - '' \ - '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile\GloballyOpenPorts\List]' \ - '"3389:TCP"="3389:TCP:*:Enabled:@xpsp2res.dll,-22009"' \ - '' \ - '[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Applets\Tour]' \ - '"RunCount"=dword:00000000' \ - '' \ - '[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced]' \ - '"HideFileExt"=dword:00000000' \ - '' \ - '[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer]' \ - '"NoWelcomeScreen"="1"' \ - '' \ - '[HKEY_CURRENT_USER\Software\Microsoft\Internet Connection Wizard]' \ - '"Completed"="1"' \ - '"Desktopchanged"="1"' '' '[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]' - - if disabled "$AUTOLOGIN"; then - printf '%s\n' '"AutoAdminLogon"="0"' - else - printf '%s\n' \ - '"AutoAdminLogon"="1"' "\"DefaultUserName\"=\"$regUsername\"" "\"DefaultPassword\"=\"$regPassword\"" - fi - - printf '%s\n' \ - '' \ - '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Video\{23A77BF7-ED96-40EC-AF06-9B1F4867732A}\0000]' \ - '"DefaultSettings.BitsPerPel"=dword:00000020' \ - "\"DefaultSettings.XResolution\"=dword:$XHEX" \ - "\"DefaultSettings.YResolution\"=dword:$YHEX" \ - '' \ - '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Hardware Profiles\Current\System\CurrentControlSet\Control\VIDEO\{23A77BF7-ED96-40EC-AF06-9B1F4867732A}\0000]' \ - '"DefaultSettings.BitsPerPel"=dword:00000020' \ - "\"DefaultSettings.XResolution\"=dword:$XHEX" \ - "\"DefaultSettings.YResolution\"=dword:$YHEX" \ - '' \ - '[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce]' \ - '"ScreenSaver"="reg add \"HKCU\\Control Panel\\Desktop\" /f /v \"SCRNSAVE.EXE\" /t REG_SZ /d \"off\""' \ - '"ScreenSaverOff"="reg add \"HKCU\\Control Panel\\Desktop\" /f /v \"ScreenSaveActive\" /t REG_SZ /d \"0\""' - - if enabled "$shortcut"; then - printf '%s\n' '"SharedDrive"="cmd /C net use Z: \\\\host.lan\\Data /persistent:yes"' - fi - - printf '%s\n' "$oem" '' - } | unix2dos > "$dir/\$OEM\$/install.reg" || return 1 - - return 0 -} - -appendRegistry() { - - local dir="$1" - local driver="$2" - - if [[ "$driver" == "2k" ]]; then - { - printf '%s\n' \ - '[HKEY_USERS\.DEFAULT\Software\Microsoft\Windows\CurrentVersion\Runonce]' '"^SetupICWDesktop"=-' '' - } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 - fi - - if [[ "$driver" == "2k3" ]]; then - { - printf '%s\n' \ - '[HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\srvWiz]' \ - '@=dword:00000000' \ - '' \ - '[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ServerOOBE\SecurityOOBE]' \ - '"DontLaunchSecurityOOBE"=dword:00000000' '' - } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 - fi - - return 0 -} - -writeVBS() { - - local dir="$1" - local username="$2" - local shortcut="$3" - - # Locate the built-in Administrator by its RID 500 SID rather than its - # localized display name, then rename that account to the requested username. - - { - printf '%s\n' \ - 'Set WshShell = WScript.CreateObject("WScript.Shell")' \ - 'Set WshNetwork = WScript.CreateObject("WScript.Network")' \ - 'Set Domain = GetObject("WinNT://" & WshNetwork.ComputerName)' \ - '' \ - 'Function DecodeSID(binSID)' \ - ' ReDim o(LenB(binSID))' \ - '' \ - ' For i = 1 To LenB(binSID)' \ - ' o(i-1) = AscB(MidB(binSID, i, 1))' \ - ' Next' \ - '' \ - ' sid = "S-" & CStr(o(0)) & "-" & OctetArrayToString _' \ - ' (Array(o(2), o(3), o(4), o(5), o(6), o(7)))' \ - ' For i = 8 To (4 * o(1) + 4) Step 4' \ - ' sid = sid & "-" & OctetArrayToString _' \ - ' (Array(o(i+3), o(i+2), o(i+1), o(i)))' \ - ' Next' \ - '' \ - ' DecodeSID = sid' \ - 'End Function' \ - '' \ - 'Function OctetArrayToString(arr)' \ - ' v = 0' \ - ' For i = 0 To UBound(arr)' \ - ' v = v * 256 + arr(i)' \ - ' Next' \ - '' \ - ' OctetArrayToString = CStr(v)' \ - 'End Function' \ - '' \ - 'For Each DomainItem in Domain' \ - ' If DomainItem.Class = "User" Then' \ - ' sid = DecodeSID(DomainItem.Get("objectSID"))' \ - ' If Left(sid, 9) = "S-1-5-21-" And Right(sid, 4) = "-500" Then' \ - ' LocalAdminADsPath = DomainItem.ADsPath' \ - ' Exit For' \ - ' End If' ' End If' 'Next' '' "Call Domain.MoveHere(LocalAdminADsPath, \"$username\")" '' - - if enabled "$shortcut"; then - printf '%s\n' \ - 'Set oLink = WshShell.CreateShortcut(WshShell.SpecialFolders("Desktop") & "\Shared.lnk")' \ - 'With oLink' ' .TargetPath = "\\host.lan\Data"' ' .Save' 'End With' 'Set oLink = Nothing' '' - fi - } | unix2dos > "$dir/\$OEM\$/install.vbs" || return 1 - - { - printf '%s\n' \ - '[COMMANDS]' '"REGEDIT /s install.reg"' '"Wscript install.vbs"' '' - } | unix2dos > "$dir/\$OEM\$/cmdlines.txt" || return 1 - - return 0 -} - -disableAutoReboot() { - - local target="$1" - - local file rc=0 - local pattern='^[[:space:]]*HKLM[[:space:]]*,[[:space:]]*"SYSTEM\\CurrentControlSet\\Control\\CrashControl"[[:space:]]*,[[:space:]]*"AutoReboot"[[:space:]]*,[[:space:]]*[^,]*,' - - file=$(find "$target" -maxdepth 1 -type f -iname HIVESYS.INF -print -quit) || return 1 - - if [ -z "$file" ]; then - error "The file HIVESYS.INF could not be found!" - return 1 - fi - - # Keep setup crashes visible instead of immediately rebooting into an - # opaque installation loop. - grep -Eqi "${pattern}[[:space:]]*[^,;[:space:]]+" "$file" || rc=$? - - case "$rc" in - 0 ) - sed -i -E "s|(${pattern})[[:space:]]*[^,;[:space:]]+|\\1 0|I" "$file" || : - ;; - 1 ) - printf '%s\n' \ - 'HKLM,"SYSTEM\CurrentControlSet\Control\CrashControl","AutoReboot",0x00010001,0' | - unix2dos >> "$file" || : - ;; - esac - - return 0 -} - -legacyInstall() { - - local dir="$2" - local desc="$3" - local driver="$4" - - local shortcut="Y" - local drivers="/tmp/drivers" - - 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 - - ETFS="[BOOT]/Boot-NoEmul.img" - - if [ ! -f "$dir/$ETFS" ] || [ ! -s "$dir/$ETFS" ]; then - error "Failed to locate file \"$ETFS\" in $desc ISO image!" - return 1 - fi - - # Legacy media uses directory names rather than metadata to identify the - # architecture and the text-mode setup source tree. - local arch="amd64" - [ ! -d "$dir/AMD64" ] && arch="x86" - - local target="$dir/AMD64" - [[ "${arch,,}" == "x86" ]] && target="$dir/I386" - - if [ ! -d "$target" ]; then - error "Failed to locate directory \"$target\" in $desc ISO image!" - return 1 - fi - - if [[ "${driver,,}" == "xp" || "${driver,,}" == "2k3" ]]; then - addLegacyDrivers "$dir" "$target" "$driver" "$arch" "$drivers" || return 1 - fi - - disableAutoReboot "$target" || return 1 - setLegacyKey "$target" "$driver" "$arch" "$desc" || return 1 - validateProductKey "$KEY" || return 1 - - local product="" - [ -n "$KEY" ] && product="ProductID=$KEY" - - mkdir -p "$dir/\$OEM\$" || return 1 - - if ! addFolder "$dir"; then - error "Failed to add OEM folder to image!" - return 1 - fi - - local oem="" - local install="" - local oem_dir="$dir/\$OEM\$/\$1/OEM" - - if [ -d "$oem_dir" ]; then - install=$(find \ - "$oem_dir" -maxdepth 1 -type f -iname install.bat -print -quit - ) || return 1 - fi - - oem=$(writeCommand "$install") || return 1 - - [ -z "$WIDTH" ] && WIDTH="1280" - [ -z "$HEIGHT" ] && HEIGHT="720" - - validateResolution "WIDTH" "$WIDTH" 320 || return 1 - validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 - validateMembership || return 1 - validateComputerName "$HOST" || return 1 - - validateLegacyText "APP" "$APP" "$desc" || return 1 - validateLegacyText "ENGINE" "$ENGINE" "$desc" || return 1 - - if [[ "$driver" == "2k" ]]; then - validateLegacyEncoding "APP" "$APP" "$desc" || return 1 - validateLegacyEncoding "ENGINE" "$ENGINE" "$desc" || return 1 - fi - - XHEX=$(printf '%08x\n' "$((10#$WIDTH))") || return 1 - YHEX=$(printf '%08x\n' "$((10#$HEIGHT))") || return 1 - - local username="${USERNAME:-Docker}" - local password="${PASSWORD:-admin}" - local workgroup="${WORKGROUP:-WORKGROUP}" - - local sifHost sifUsername sifPassword sifOrganization sifWorkgroup - local regUsername regPassword - - validateLegacyUsername "$username" "$desc" || return 1 - validatePassword "$password" "$desc" || return 1 - - if [[ "$driver" == "2k" ]]; then - validateLegacyEncoding "USERNAME" "$username" "$desc" || return 1 - validateLegacyEncoding "PASSWORD" "$password" "$desc" || return 1 - validateLegacyEncoding "WORKGROUP" "$workgroup" "$desc" || return 1 - fi - - # WINNT.SIF and .reg files use different escaping rules, so prepare their - # values independently before generating either file. - sifHost=$(escapeSIFValue "${HOST:-*}") || return 1 - sifUsername=$(escapeSIFValue "$username") || return 1 - sifPassword=$(escapeSIFValue "$password") || return 1 - sifOrganization=$(escapeSIFValue "$APP for $ENGINE") || return 1 - sifWorkgroup=$(escapeSIFValue "$workgroup") || return 1 - regUsername=$(escapeRegistryValue "$username") || return 1 - regPassword=$(escapeRegistryValue "$password") || return 1 - - writeSIF \ - "$target" \ - "$driver" \ - "$product" "$sifHost" "$sifUsername" "$sifPassword" "$sifOrganization" "$sifWorkgroup" || return 1 - - writeRegistry "$dir" "$shortcut" "$oem" "$regUsername" "$regPassword" || return 1 - - appendRegistry "$dir" "$driver" || return 1 - writeVBS "$dir" "$username" "$shortcut" || return 1 - - return 0 -} - return 0 diff --git a/src/entry.sh b/src/entry.sh index fb8a39be..532756d7 100644 --- a/src/entry.sh +++ b/src/entry.sh @@ -16,6 +16,7 @@ cd /run . define.sh # Define versions . mido.sh # Download Windows . answer.sh # Setup answer files +. legacy.sh # Legacy installation . image.sh # Detect image files . install.sh # Run installation . disk.sh # Initialize disks diff --git a/src/image.sh b/src/image.sh index b89b68d7..317c6ef6 100644 --- a/src/image.sh +++ b/src/image.sh @@ -353,6 +353,169 @@ getImageSize() { printf '%s\n' "$size" } +getArchiveSize() { + + local file="$1" + local result_name="$2" + local -n result="$result_name" + + local found=0 listing line value rc + + result=0 + + listing=$(7z l -slt "$file" 2>/dev/null) || { + rc=$? + error "Failed to read archive information: $file" + return "$rc" + } + + while IFS= read -r line; do + + [[ "$line" == "Size = "* ]] || continue + + value="${line#Size = }" + [[ "$value" =~ ^[0-9]+$ ]] || continue + + result=$(( result + value )) + found=1 + + done <<< "$listing" + + if (( ! found )); then + error "Failed to determine archive contents size: $file" + return 1 + fi + + return 0 +} + +extractImage() { + + local iso="$1" + local dir="$2" + local version="$3" + + local target="$dir" + local desc="local ISO" + local archive="${dir}.archive" + local file size required archiveSize rc + + if [ -z "$CUSTOM" ]; then + desc="downloaded ISO" + if [[ "$version" != "http"* ]]; then + desc=$(printVariant "$version" "$desc") + fi + fi + + if [[ "${iso,,}" == *".esd" ]]; then + extractESD "$iso" "$dir" "$version" "$desc" || return + return 0 + fi + + local msg="Extracting $desc image" + info "$msg..." && html "$msg..." + + enabled "${UNPACK:-}" && target="$archive" + + if ! rm -rf -- "$dir" "$archive"; then + error "Failed to remove extraction directories!" + return 1 + fi + + if ! makeDir "$target"; then + error "Failed to create directory \"$target\" !" + return 1 + fi + + if ! size=$(stat -c%s "$iso"); then + error "Failed to determine ISO file size: $iso" + return 1 + fi + + if (( size < 10000000 )); then + error "Invalid ISO file: Size of \"$iso\" is smaller than 10 MB" + return 1 + fi + + required="$size" + + if enabled "${UNPACK:-}"; then + getArchiveSize "$iso" archiveSize || return + required="$archiveSize" + fi + + checkFreeSpace "$target" "$required" || return 1 + + if ! rm -rf -- "$target"; then + error "Failed to remove directory \"$target\" !" + return 1 + fi + + /run/progress.sh "$target" "$size" "$msg ([P])..." & + + 7z x "$iso" -o"$target" > /dev/null || { + rc=$? + fKill "progress.sh" + error "Failed to extract ISO file: $iso" + return "$rc" + } + + fKill "progress.sh" + + if ! enabled "${UNPACK:-}"; then + + LABEL=$(isoinfo -d -i "$iso" | sed -n 's/Volume id: //p') || LABEL="" + + else + + # Locate the first root-level ISO in the downloaded archive + if ! file=$(find "$archive" -maxdepth 1 -type f -iname "*.iso" -print -quit); then + error "Failed to search for a nested ISO in the extracted archive!" + return 1 + fi + + if [ -z "$file" ]; then + error "Failed to find any nested ISO files in the archive!" + return 1 + fi + + if ! mv -f -- "$file" "$iso"; then + error "Failed to preserve extracted ISO file: $file" + return 1 + fi + + if ! rm -rf -- "$archive"; then + error "Failed to remove directory \"$archive\" !" + return 1 + fi + + if ! makeDir "$dir"; then + error "Failed to create directory \"$dir\" !" + return 1 + fi + + if ! size=$(stat -c%s "$iso"); then + error "Failed to determine nested ISO file size: $iso" + return 1 + fi + + checkFreeSpace "$dir" "$size" || return 1 + + 7z x "$iso" -o"$dir" > /dev/null || { + rc=$? + error "Failed to extract nested ISO file: $iso" + return "$rc" + } + + LABEL=$(isoinfo -d -i "$iso" | sed -n 's/Volume id: //p') || LABEL="" + + UNPACK="" + + fi + + return 0 +} + checkPlatform() { local xml="$1" @@ -607,140 +770,6 @@ createSetupImage() { return 0 } -detectLegacy() { - - local dir="$1" - - local marker - - [[ "${PLATFORM,,}" == "x64" ]] || return 1 - - # Legacy media is identified from setup marker files rather than WIM - # metadata. The order is intentional because several releases share markers. - marker=$(find "$dir" -maxdepth 1 -type d -iname 'ia64' -print -quit) || return 2 - - if [ -n "$marker" ]; then - error "Windows IA-64 (Itanium) images are not supported by this container!" - return 2 - fi - - marker=$(find "$dir" -maxdepth 1 -type d -iname WIN95 -print -quit) || return 2 - - if [ -n "$marker" ]; then - DETECTED="win95" - return 0 - fi - - marker=$(find "$dir" -maxdepth 1 -type d -iname WIN98 -print -quit) || return 2 - - if [ -n "$marker" ]; then - DETECTED="win98" - return 0 - fi - - marker=$(find "$dir" -maxdepth 1 -type d -iname WIN9X -print -quit) || return 2 - - if [ -n "$marker" ]; then - DETECTED="win9x" - return 0 - fi - - marker=$(find "$dir" -maxdepth 1 -type f \ - \( \ - -iname CDROM_W.40 -o \ - -iname CDROM_S.40 -o \ - -iname CDROM_TS.40 \ - \) \ - -print -quit) || return 2 - - if [ -n "$marker" ]; then - DETECTED="winnt4" - return 0 - fi - - marker=$(find "$dir" -maxdepth 1 -type f -iname CDROM_NT.5 -print -quit) || return 2 - - if [ -n "$marker" ]; then - - marker=$(find "$dir" -maxdepth 1 -type f \ - \( \ - -iname CDROM_IA.5 -o \ - -iname CDROM_ID.5 -o \ - -iname CDROM_IP.5 -o \ - -iname CDROM_IS.5 \ - \) \ - -print -quit) || return 2 - - if [ -n "$marker" ]; then - DETECTED="win2k" - return 0 - fi - - fi - - # WIN51 identifies the NT 5.1/5.2 media family; the companion marker then - # distinguishes XP x86, XP x64, and Server 2003. - marker=$(find "$dir" -maxdepth 1 -iname WIN51 -print -quit) || return 2 - [ -n "$marker" ] || return 1 - - marker=$(find "$dir" -maxdepth 1 -type f -iname WIN51AP -print -quit) || return 2 - - if [ -n "$marker" ]; then - DETECTED="winxpx64" - return 0 - fi - - marker=$(find "$dir" -maxdepth 1 -type f \ - \( \ - -iname WIN51IC -o \ - -iname WIN51IP -o \ - -iname setupxp.htm \ - \) \ - -print -quit) || return 2 - - if [ -n "$marker" ]; then - DETECTED="winxpx86" - return 0 - fi - - marker=$(find "$dir" -maxdepth 1 -type f \ - \( \ - -iname WIN51IS -o \ - -iname WIN51IA -o \ - -iname WIN51IB -o \ - -iname WIN51ID -o \ - -iname WIN51IL -o \ - -iname WIN51AA -o \ - -iname WIN51AD -o \ - -iname WIN51AS -o \ - -iname WIN51MA -o \ - -iname WIN51MD -o \ - -iname WIN51MP \ - \) \ - -print -quit) || return 2 - - if [ -n "$marker" ]; then - DETECTED="win2003r2" - return 0 - fi - - return 1 -} - -detectReactOS() { - - local dir="$1" - local marker - - marker=$(find "$dir" -maxdepth 2 -type f \ - \( -ipath '*/reactos/reactos.inf' -o -ipath '*/reactos/unattend.inf' \) -print -quit) || return 2 - - [ -n "$marker" ] || return 1 - - DETECTED="reactos" - return 0 -} - findIsoImage() { local iso="$1" @@ -1727,175 +1756,6 @@ extractESD() { return 0 } -normalizeBatch() { - - local file="$1" - local bom tmp encoding - - [ ! -s "$file" ] && return 0 - - bom=$(od -An -N2 -tx1 "$file" | tr -d ' \n') || return 1 - - # Convert only BOM-marked UTF-16 files; unmarked ANSI and UTF-8 batch files - # are deliberately left unchanged. - case "$bom" in - "fffe" ) encoding="UTF-16LE" ;; - "feff" ) encoding="UTF-16BE" ;; - * ) return 0 ;; - esac - - if ! tmp=$(mktemp "${file}.XXXXXX"); then - error "Failed to create temporary batch file!" - return 1 - fi - - if ! tail -c +3 "$file" | iconv -f "$encoding" -t UTF-8 > "$tmp"; then - rm -f "$tmp" - error "Failed to convert $file from $encoding to UTF-8!" - return 1 - fi - - if ! chmod --reference="$file" "$tmp" || ! mv -f "$tmp" "$file"; then - rm -f "$tmp" - error "Failed to replace batch file: $file" - return 1 - fi - - return 0 -} - -reportBatchMatches() { - - local file="$1" - local source="$2" - local pattern="$3" - local message="$4" - local suggestion="$5" - - local matches line - matches=$(grep -Pin "$pattern" "$file" || true) - - [ -n "$matches" ] || return 0 - - warn "$message in $source:" - - while IFS= read -r line; do - printf ' %s\n' "$line" >&2 - done <<< "$matches" - - printf ' %s\n\n' "$suggestion" >&2 - - return 0 -} - -checkBatch() { - - local file="$1" - - local tmp output - local matches line - local enabled_rules - - [ -s "$file" ] || return 0 - - if ! tmp=$(mktemp -d /tmp/blinter.XXXXXX); then - warn "failed to create temporary Blinter directory." - return 0 - fi - - local source="your install.bat file" - [ -n "${COMMAND:-}" ] && source="your COMMAND variable" - - # Only check rules that indicate likely execution or behavioural failures. - enabled_rules="E001,E002,E003,E004,E005,E006,E007,E008" - enabled_rules+=",E009,E010,E011,E012,E013,E014,E015,E016" - enabled_rules+=",E017,E018,E019,E020,E021,E022,E023,E024" - enabled_rules+=",E025,E027,E028,E029,E030,E031,E032,E033,E034" - enabled_rules+=",W004,W005,W013,W017,W021,W022,W034,W038,W040" - - if enabled "$DEBUG"; then - if LC_ALL=C grep -Pq '[^\x09\x0D\x20-\x7E]' "$file"; then - warn "non-ASCII characters were detected in $source and may not execute correctly in Windows Command Prompt." - fi - fi - - cat > "$tmp/blinter.ini" <&1 || true - ) - - # Remove header. - output=$( - awk ' - /^DETAILED ISSUES:/ { - found = 1 - next - } - - found && !started { - if (/^-+$/) { - started = 1 - } - next - } - - started { - print - } - ' <<< "$output" - ) - - output="${output#"${output%%[!$'\r\n ']*}"}" - output="${output%"${output##*[!$'\r\n ']}"}" - - if grep -Eq '^(ERROR|WARNING|SECURITY) LEVEL ISSUES:$' <<< "$output"; then - - warn "possible issues were detected in $source:" - printf '\n%s\n\n' "$output" >&2 - fi - - rm -rf "$tmp" || true - - reportBatchMatches \ - "$file" \ - "$source" \ - '(?/dev/null) || { - rc=$? - error "Failed to read archive information: $file" - return "$rc" - } - - while IFS= read -r line; do - - [[ "$line" == "Size = "* ]] || continue - - value="${line#Size = }" - [[ "$value" =~ ^[0-9]+$ ]] || continue - - result=$(( result + value )) - found=1 - - done <<< "$listing" - - if (( ! found )); then - error "Failed to determine archive contents size: $file" - return 1 - fi - - return 0 -} - -extractImage() { - - local iso="$1" - local dir="$2" - local version="$3" - - local target="$dir" - local desc="local ISO" - local archive="${dir}.archive" - local file size required archiveSize rc - - if [ -z "$CUSTOM" ]; then - desc="downloaded ISO" - if [[ "$version" != "http"* ]]; then - desc=$(printVariant "$version" "$desc") - fi - fi - - if [[ "${iso,,}" == *".esd" ]]; then - extractESD "$iso" "$dir" "$version" "$desc" || return - return 0 - fi - - local msg="Extracting $desc image" - info "$msg..." && html "$msg..." - - enabled "${UNPACK:-}" && target="$archive" - - if ! rm -rf -- "$dir" "$archive"; then - error "Failed to remove extraction directories!" - return 1 - fi - - if ! makeDir "$target"; then - error "Failed to create directory \"$target\" !" - return 1 - fi - - if ! size=$(stat -c%s "$iso"); then - error "Failed to determine ISO file size: $iso" - return 1 - fi - - if (( size < 10000000 )); then - error "Invalid ISO file: Size of \"$iso\" is smaller than 10 MB" - return 1 - fi - - required="$size" - - if enabled "${UNPACK:-}"; then - getArchiveSize "$iso" archiveSize || return - required="$archiveSize" - fi - - checkFreeSpace "$target" "$required" || return 1 - - if ! rm -rf -- "$target"; then - error "Failed to remove directory \"$target\" !" - return 1 - fi - - /run/progress.sh "$target" "$size" "$msg ([P])..." & - - 7z x "$iso" -o"$target" > /dev/null || { - rc=$? - fKill "progress.sh" - error "Failed to extract ISO file: $iso" - return "$rc" - } - - fKill "progress.sh" - - if ! enabled "${UNPACK:-}"; then - - LABEL=$(isoinfo -d -i "$iso" | sed -n 's/Volume id: //p') || LABEL="" - - else - - # Locate the first root-level ISO in the downloaded archive - if ! file=$(find "$archive" -maxdepth 1 -type f -iname "*.iso" -print -quit); then - error "Failed to search for a nested ISO in the extracted archive!" - return 1 - fi - - if [ -z "$file" ]; then - error "Failed to find any nested ISO files in the archive!" - return 1 - fi - - if ! mv -f -- "$file" "$iso"; then - error "Failed to preserve extracted ISO file: $file" - return 1 - fi - - if ! rm -rf -- "$archive"; then - error "Failed to remove directory \"$archive\" !" - return 1 - fi - - if ! makeDir "$dir"; then - error "Failed to create directory \"$dir\" !" - return 1 - fi - - if ! size=$(stat -c%s "$iso"); then - error "Failed to determine nested ISO file size: $iso" - return 1 - fi - - checkFreeSpace "$dir" "$size" || return 1 - - 7z x "$iso" -o"$dir" > /dev/null || { - rc=$? - error "Failed to extract nested ISO file: $iso" - return "$rc" - } - - LABEL=$(isoinfo -d -i "$iso" | sed -n 's/Volume id: //p') || LABEL="" - - UNPACK="" - - fi - - return 0 -} - detectImage() { local dir="$1" @@ -1306,6 +1143,175 @@ addDrivers() { return 0 } +normalizeBatch() { + + local file="$1" + local bom tmp encoding + + [ ! -s "$file" ] && return 0 + + bom=$(od -An -N2 -tx1 "$file" | tr -d ' \n') || return 1 + + # Convert only BOM-marked UTF-16 files; unmarked ANSI and UTF-8 batch files + # are deliberately left unchanged. + case "$bom" in + "fffe" ) encoding="UTF-16LE" ;; + "feff" ) encoding="UTF-16BE" ;; + * ) return 0 ;; + esac + + if ! tmp=$(mktemp "${file}.XXXXXX"); then + error "Failed to create temporary batch file!" + return 1 + fi + + if ! tail -c +3 "$file" | iconv -f "$encoding" -t UTF-8 > "$tmp"; then + rm -f "$tmp" + error "Failed to convert $file from $encoding to UTF-8!" + return 1 + fi + + if ! chmod --reference="$file" "$tmp" || ! mv -f "$tmp" "$file"; then + rm -f "$tmp" + error "Failed to replace batch file: $file" + return 1 + fi + + return 0 +} + +reportBatchMatches() { + + local file="$1" + local source="$2" + local pattern="$3" + local message="$4" + local suggestion="$5" + + local matches line + matches=$(grep -Pin "$pattern" "$file" || true) + + [ -n "$matches" ] || return 0 + + warn "$message in $source:" + + while IFS= read -r line; do + printf ' %s\n' "$line" >&2 + done <<< "$matches" + + printf ' %s\n\n' "$suggestion" >&2 + + return 0 +} + +checkBatch() { + + local file="$1" + + local tmp output + local matches line + local enabled_rules + + [ -s "$file" ] || return 0 + + if ! tmp=$(mktemp -d /tmp/blinter.XXXXXX); then + warn "failed to create temporary Blinter directory." + return 0 + fi + + local source="your install.bat file" + [ -n "${COMMAND:-}" ] && source="your COMMAND variable" + + # Only check rules that indicate likely execution or behavioural failures. + enabled_rules="E001,E002,E003,E004,E005,E006,E007,E008" + enabled_rules+=",E009,E010,E011,E012,E013,E014,E015,E016" + enabled_rules+=",E017,E018,E019,E020,E021,E022,E023,E024" + enabled_rules+=",E025,E027,E028,E029,E030,E031,E032,E033,E034" + enabled_rules+=",W004,W005,W013,W017,W021,W022,W034,W038,W040" + + if enabled "$DEBUG"; then + if LC_ALL=C grep -Pq '[^\x09\x0D\x20-\x7E]' "$file"; then + warn "non-ASCII characters were detected in $source and may not execute correctly in Windows Command Prompt." + fi + fi + + cat > "$tmp/blinter.ini" <&1 || true + ) + + # Remove header. + output=$( + awk ' + /^DETAILED ISSUES:/ { + found = 1 + next + } + + found && !started { + if (/^-+$/) { + started = 1 + } + next + } + + started { + print + } + ' <<< "$output" + ) + + output="${output#"${output%%[!$'\r\n ']*}"}" + output="${output%"${output##*[!$'\r\n ']}"}" + + if grep -Eq '^(ERROR|WARNING|SECURITY) LEVEL ISSUES:$' <<< "$output"; then + + warn "possible issues were detected in $source:" + printf '\n%s\n\n' "$output" >&2 + fi + + rm -rf "$tmp" || true + + reportBatchMatches \ + "$file" \ + "$source" \ + '(?2G. - - writeState "args" "-global q35-pcihost.x-pci-hole64-fix=false" || return 1 - - fi ;; - - esac - - return 0 -} - startWindows return 0 diff --git a/src/legacy.sh b/src/legacy.sh new file mode 100644 index 00000000..43445809 --- /dev/null +++ b/src/legacy.sh @@ -0,0 +1,1121 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +detectLegacy() { + + local dir="$1" + + local marker + + [[ "${PLATFORM,,}" == "x64" ]] || return 1 + + # Legacy media is identified from setup marker files rather than WIM + # metadata. The order is intentional because several releases share markers. + marker=$(find "$dir" -maxdepth 1 -type d -iname 'ia64' -print -quit) || return 2 + + if [ -n "$marker" ]; then + error "Windows IA-64 (Itanium) images are not supported by this container!" + return 2 + fi + + marker=$(find "$dir" -maxdepth 1 -type d -iname WIN95 -print -quit) || return 2 + + if [ -n "$marker" ]; then + DETECTED="win95" + return 0 + fi + + marker=$(find "$dir" -maxdepth 1 -type d -iname WIN98 -print -quit) || return 2 + + if [ -n "$marker" ]; then + DETECTED="win98" + return 0 + fi + + marker=$(find "$dir" -maxdepth 1 -type d -iname WIN9X -print -quit) || return 2 + + if [ -n "$marker" ]; then + DETECTED="win9x" + return 0 + fi + + marker=$(find "$dir" -maxdepth 1 -type f \ + \( \ + -iname CDROM_W.40 -o \ + -iname CDROM_S.40 -o \ + -iname CDROM_TS.40 \ + \) \ + -print -quit) || return 2 + + if [ -n "$marker" ]; then + DETECTED="winnt4" + return 0 + fi + + marker=$(find "$dir" -maxdepth 1 -type f -iname CDROM_NT.5 -print -quit) || return 2 + + if [ -n "$marker" ]; then + + marker=$(find "$dir" -maxdepth 1 -type f \ + \( \ + -iname CDROM_IA.5 -o \ + -iname CDROM_ID.5 -o \ + -iname CDROM_IP.5 -o \ + -iname CDROM_IS.5 \ + \) \ + -print -quit) || return 2 + + if [ -n "$marker" ]; then + DETECTED="win2k" + return 0 + fi + + fi + + # WIN51 identifies the NT 5.1/5.2 media family; the companion marker then + # distinguishes XP x86, XP x64, and Server 2003. + marker=$(find "$dir" -maxdepth 1 -iname WIN51 -print -quit) || return 2 + [ -n "$marker" ] || return 1 + + marker=$(find "$dir" -maxdepth 1 -type f -iname WIN51AP -print -quit) || return 2 + + if [ -n "$marker" ]; then + DETECTED="winxpx64" + return 0 + fi + + marker=$(find "$dir" -maxdepth 1 -type f \ + \( \ + -iname WIN51IC -o \ + -iname WIN51IP -o \ + -iname setupxp.htm \ + \) \ + -print -quit) || return 2 + + if [ -n "$marker" ]; then + DETECTED="winxpx86" + return 0 + fi + + marker=$(find "$dir" -maxdepth 1 -type f \ + \( \ + -iname WIN51IS -o \ + -iname WIN51IA -o \ + -iname WIN51IB -o \ + -iname WIN51ID -o \ + -iname WIN51IL -o \ + -iname WIN51AA -o \ + -iname WIN51AD -o \ + -iname WIN51AS -o \ + -iname WIN51MA -o \ + -iname WIN51MD -o \ + -iname WIN51MP \ + \) \ + -print -quit) || return 2 + + if [ -n "$marker" ]; then + DETECTED="win2003r2" + return 0 + fi + + return 1 +} + +detectReactOS() { + + local dir="$1" + local marker + + marker=$(find "$dir" -maxdepth 2 -type f \ + \( -ipath '*/reactos/reactos.inf' -o -ipath '*/reactos/unattend.inf' \) -print -quit) || return 2 + + [ -n "$marker" ] || return 1 + + DETECTED="reactos" + return 0 +} + +setMachine() { + + local id="$1" + 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 ! legacyInstall "$iso" "$dir" "$desc" "$version"; then + error "Failed to prepare $desc ISO!" + return 1 + fi + + fi + + if isLegacy "$id"; then + + writeState "mode" "windows_legacy" || return 1 + + case "${id,,}" in + "win9"* | "win2k"* | "reactos" ) + writeState "vga" "cirrus" || return 1 ;; + * ) + writeState "vga" "std" || return 1 ;; + esac + + fi + + restoreBootMode || return 1 + + case "${id,,}" in + + "win9"* | "winnt4" ) + + writeState "usb" "N" || return 1 + writeState "net" "pcnet" || return 1 + writeState "type" "auto" || return 1 + writeState "old" "pc-i440fx-2.4" || return 1 ;; + + "win2k"* ) + + writeState "old" "pc" || return 1 + writeState "type" "auto" || return 1 + writeState "net" "rtl8139" || return 1 + writeState "usb" "pci-ohci" || return 1 ;; + + "winxpx"* | "win2003"* ) + + writeState "type" "blk" || return 1 + writeState "net" "rtl8139" || return 1 + writeState "sound" "usb-audio" || return 1 ;; + + "reactos" ) + + writeState "old" "pc" || return 1 + writeState "type" "auto" || return 1 + writeState "net" "rtl8139" || return 1 + writeState "usb" "pci-ohci" || return 1 ;; + + esac + + if [[ "${id,,}" == "reactos" ]] && [ -z "$CUSTOM" ]; 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" + fi + + restoreMachine || return 1 + + case "${id,,}" in + + "win9"* | "winnt4" | "win2k"* | *"x86"* | "reactos" ) + + # Legacy 32-bit Windows may enter an incompatible PAE/DEP path when the + # NX flag is exposed, causing installation failures or repeated resets. + + writeState "flag" "nx=off" || return 1 ;; + + esac + + case "${id,,}" in + + "win9"* | "winnt4" | "win2k"* | "winxp"* | "win2003"* | \ + "winvistax86"* | "win7x86"* | "reactos" ) + + if isQ35 "$MACHINE"; then + + # pc-q35-2.11 began advertising a synthetic 64-bit PCI MMIO aperture. + # Older Windows ACPI implementations may reject that resource layout, + # so retain the pre-2.11 behavior for these guests to prevent a + # blue screen on XP and others if the 64 bit PCI hole size is >2G. + + writeState "args" "-global q35-pcihost.x-pci-hole64-fix=false" || return 1 + + fi ;; + + esac + + return 0 +} + +restoreMachine() { + + # Restore the saved machine only when q35 is still the default; + # an explicit user-selected machine must remain untouched. + [[ "${MACHINE,,}" != "q35" ]] && return 0 + [[ "${PLATFORM,,}" != "x64" ]] && return 0 + + MACHINE="" + restoreState "MACHINE" "old" || return 1 + [ -z "$MACHINE" ] && MACHINE="q35" + + return 0 +} + +restoreMachineState() { + + restoreState "VGA" "vga" || return 1 + restoreState "USB" "usb" || return 1 + restoreState "SOUND" "sound" || return 1 + restoreState "ADAPTER" "net" || return 1 + restoreState "CPU_MODEL" "cpu" || return 1 + restoreState "DISK_TYPE" "type" || return 1 + + mergeState "CPU_FLAGS" "flag" "," || return 1 + mergeState "ARGUMENTS" "args" " " || return 1 + + return 0 +} + +restoreBootMode() { + + local current="${BOOT_MODE:-}" + + local mode + mode=$(readState "mode") || return 1 + + [ -n "$mode" ] || return 0 + + # A saved legacy mode always wins. A saved modern mode only replaces the + # default mode and never an explicit user-selected boot configuration. + if [[ "${mode,,}" == "windows_legacy" ]]; then + BOOT_MODE="$mode" + return 0 + fi + + case "${current,,}" in + "" | "windows" | "windows_plain" ) + BOOT_MODE="$mode" ;; + esac + + return 0 +} + +extractDrivers() { + + local drivers="$1" + + rm -rf "$drivers" || return 1 + mkdir -p "$drivers" || return 1 + + if ! bsdtar -xf /var/drivers.txz -C "$drivers"; then + error "Failed to extract drivers!" + return 1 + fi + + return 0 +} + +copyStorageDriver() { + + local dir="$1" + local target="$2" + local driver="$3" + local arch="$4" + local drivers="$5" + + local destination="$dir/\$OEM\$/\$1/Drivers/viostor" + + if [ ! -f "$drivers/viostor/$driver/$arch/viostor.sys" ]; then + error "Failed to locate required storage drivers!" + return 1 + fi + + cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$target" || return 1 + + mkdir -p "$destination" || return 1 + cp -L "$drivers/viostor/$driver/$arch/viostor.cat" "$destination" || return 1 + cp -L "$drivers/viostor/$driver/$arch/viostor.inf" "$destination" || return 1 + cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$destination" || return 1 + + return 0 +} + +addNetworkDriver() { + + local dir="$1" + local driver="$2" + local arch="$3" + local drivers="$4" + + local destination="$dir/\$OEM\$/\$1/Drivers/NetKVM" + + if [ ! -f "$drivers/NetKVM/$driver/$arch/netkvm.sys" ]; then + error "Failed to locate required network drivers!" + return 1 + fi + + mkdir -p "$destination" || return 1 + cp -L "$drivers/NetKVM/$driver/$arch/netkvm.cat" "$destination" || return 1 + cp -L "$drivers/NetKVM/$driver/$arch/netkvm.inf" "$destination" || return 1 + cp -L "$drivers/NetKVM/$driver/$arch/netkvm.sys" "$destination" || return 1 + + return 0 +} + +addSIFEntry() { + + local file="$1" + local section="$2" + local entry="$3" + + local header="[$section]" + local line ending="lf" + + line=$(grep -Fnx -m 1 "$header" "$file" | cut -d: -f1) || line="" + + if [ -z "$line" ]; then + line=$(grep -Fnx -m 1 "$header"$'\r' "$file" | cut -d: -f1) || line="" + ending="crlf" + fi + + if [ -z "$line" ]; then + error "Failed to locate section \"$header\" in \"$file\" !" + return 1 + fi + + local rc=0 + grep -Fqx -e "$entry" -e "$entry"$'\r' "$file" || rc=$? + + if (( rc == 0 )); then + return 0 + fi + + if (( rc != 1 )); then + error "Failed to inspect section \"$header\" in \"$file\" !" + return 1 + fi + + if [[ "$ending" == "crlf" ]]; then + printf '%s\r\n' "$entry" + else + printf '%s\n' "$entry" + fi | sed -i "${line}r /dev/stdin" "$file" || return 1 + + return 0 +} + +patchStorageDriver() { + + local file="$1" + local arch="$2" + + # Text-mode setup reads TXTSETUP.SIF before Plug and Play is available, so the + # VirtIO storage service and hardware IDs must be registered there explicitly. + addSIFEntry "$file" "SCSI.Load" 'viostor=viostor.sys,4' || return 1 + addSIFEntry "$file" "SourceDisksFiles.$arch" 'viostor.sys=1,,,,,,4_,4,1,,,1,4' || return 1 + addSIFEntry "$file" "SCSI" 'viostor="Red Hat VirtIO SCSI Disk Device"' || return 1 + addSIFEntry "$file" "HardwareIdsDatabase" 'PCI\VEN_1AF4&DEV_1001&SUBSYS_00000000="viostor"' || return 1 + addSIFEntry "$file" "HardwareIdsDatabase" 'PCI\VEN_1AF4&DEV_1001&SUBSYS_00020000="viostor"' || return 1 + addSIFEntry "$file" "HardwareIdsDatabase" 'PCI\VEN_1AF4&DEV_1001&SUBSYS_00021AF4="viostor"' || return 1 + + return 0 +} + +addSataDriver() { + + local dir="$1" + local target="$2" + local arch="$3" + local drivers="$4" + local file="$5" + + local destination="$dir/\$OEM\$/\$1/Drivers/sata" + + if [ ! -d "$drivers/sata/xp/$arch" ]; then + error "Failed to locate required SATA drivers!" + return 1 + fi + + mkdir -p "$destination" || return 1 + cp -Lr "$drivers/sata/xp/$arch/." "$destination" || return 1 + cp -Lr "$drivers/sata/xp/$arch/." "$target" || return 1 + + addSIFEntry "$file" "SCSI.Load" 'iaStor=iaStor.sys,4' || return 1 + addSIFEntry "$file" "FileFlags" 'iaStor.sys = 16' || return 1 + addSIFEntry "$file" "SourceDisksFiles.$arch" 'iaStor.cat = 1,,,,,,,1,0,0' || return 1 + addSIFEntry "$file" "SourceDisksFiles.$arch" 'iaStor.inf = 1,,,,,,,1,0,0' || return 1 + addSIFEntry "$file" "SourceDisksFiles.$arch" 'iaStor.sys = 1,,,,,,4_,4,1,,,1,4' || return 1 + addSIFEntry "$file" "SourceDisksFiles.$arch" 'iaStor.sys = 1,,,,,,,1,0,0' || return 1 + addSIFEntry "$file" "SourceDisksFiles.$arch" 'iaahci.cat = 1,,,,,,,1,0,0' || return 1 + addSIFEntry "$file" "SourceDisksFiles.$arch" 'iaAHCI.inf = 1,,,,,,,1,0,0' || return 1 + addSIFEntry "$file" "SCSI" 'iaStor="Intel(R) SATA RAID/AHCI Controller"' || return 1 + addSIFEntry "$file" "HardwareIdsDatabase" 'PCI\VEN_8086&DEV_2922&CC_0106="iaStor"' || return 1 + + return 0 +} + +addLegacyDrivers() { + + local dir="$1" + local target="$2" + local driver="$3" + local arch="$4" + local drivers="$5" + + local msg="Adding drivers to image..." + info "$msg" && html "$msg" + + extractDrivers "$drivers" || return 1 + copyStorageDriver "$dir" "$target" "$driver" "$arch" "$drivers" || return 1 + addNetworkDriver "$dir" "$driver" "$arch" "$drivers" || return 1 + + local file + file=$(find "$target" -maxdepth 1 -type f -iname TXTSETUP.SIF -print -quit) || return 1 + + if [ -z "$file" ]; then + error "The file TXTSETUP.SIF could not be found!" + return 1 + fi + + patchStorageDriver "$file" "$arch" || return 1 + addSataDriver "$dir" "$target" "$arch" "$drivers" "$file" || return 1 + + if ! rm -rf "$drivers"; then + warn "failed to clean temporary driver files!" + fi + + return 0 +} + +setLegacyKey() { + + local target="$1" + local driver="$2" + local arch="$3" + local desc="$4" + + local setup pid file block + setup=$(find "$target" -maxdepth 1 -type f -iname setupp.ini -print -quit) || return 1 + + [[ -n "$setup" ]] || return 0 + [[ -z "$KEY" ]] || return 0 + + pid=$(<"$setup") || return 1 + pid="${pid%$'\r'}" + + if [[ "$driver" == "2k" ]]; then + [ "${#pid}" -ge 3 ] || return 0 + echo "${pid::-3}270" > "$setup" || : + return 0 + fi + + if [[ "$pid" == *"270" ]]; then + warn "this version of $desc requires a volume license key (VLK), it will ask for one during installation." + return 0 + fi + + file=$(find "$target" -maxdepth 1 -type f -iname PID.INF -print -quit) || return 1 + + if [[ -n "$file" ]]; then + + local key="" + + # Prefer a staging or OEM key already shipped on the media before falling + # back to Microsoft's documented generic installation keys. + if [[ "$driver" == "2k3" ]]; then + + block=$(grep -i -A 2 "StagingKey" "$file") || block="" + + if [ -n "$block" ]; then + key=$(printf '%s\n' "$block" | tail -n 2 | head -n 1) || key="" + fi + + else + + key="${pid: -8:5}" + + if [[ "${pid^^}" == *"OEM" ]]; then + + block=$(grep -i -A 2 "$key" "$file") || block="" + + else + + block=$(grep -i -m 1 -A 2 "$key" "$file") || block="" + + fi + + if [ -n "$block" ]; then + key=$(printf '%s\n' "$block" | tail -n 2 | head -n 1) || key="" + fi + + key="${key#*= }" + + fi + + if [ -n "$key" ]; then + key="${key%$'\r'}" + [[ "${#key}" == "29" ]] && KEY="$key" + fi + + fi + + [[ -n "$KEY" ]] && return 0 + + # These are NOT pirated keys, they come from official MS documentation. + + case "${driver,,}" in + "xp" ) + + if [[ "${arch,,}" == "x86" ]]; then + # Windows XP Professional x86 generic trial key (no activation) + KEY="DR8GV-C8V6J-BYXHG-7PYJR-DB66Y" + else + # Windows XP Professional x64 generic trial key (no activation) + KEY="B2RBK-7KPT9-4JP6X-QQFWM-PJD6G" + fi ;; + + "2k3" ) + + if [[ "${arch,,}" == "x86" ]]; then + # Windows Server 2003 Standard x86 generic trial key (no activation) + KEY="QKDCQ-TP2JM-G4MDG-VR6F2-P9C48" + else + # Windows Server 2003 Standard x64 generic trial key (no activation) + KEY="P4WJG-WK3W7-3HM8W-RWHCK-8JTRY" + fi ;; + + esac + + if [ "${#pid}" -ge 3 ]; then + echo "${pid::-3}000" > "$setup" || : + fi + + return 0 +} + +writeCommand() { + + local install="$1" + + [ -f "$install" ] || return 0 + + if enabled "${LOG:-}"; then + printf '%s' "\"Script\"=\"cmd /C start \\\"Install\\\" cmd.exe /D /C \\\"\\\"C:\\\\OEM\\\\install.bat\\\" > \\\"C:\\\\OEM\\\\install.log\\\" 2>&1\\\"\"" + else + printf '%s' "\"Script\"=\"cmd /C start \\\"Install\\\" cmd.exe /D /C \\\"\\\"C:\\\\OEM\\\\install.bat\\\"\\\"\"" + fi + + return 0 +} + +writeSIF() { + + local target="$1" + local driver="$2" + local product="$3" + local sifHost="$4" + local sifUsername="$5" + local sifPassword="$6" + local sifOrganization="$7" + local sifWorkgroup="$8" + + find "$target" -maxdepth 1 -type f -iname winnt.sif -delete || return 1 + + { + printf '%s\n' \ + '[Data]' \ + ' AutoPartition=1' \ + ' MsDosInitiated="0"' \ + ' UnattendedInstall="Yes"' \ + ' AutomaticUpdates="Yes"' \ + '' \ + '[Unattended]' \ + ' UnattendSwitch=Yes' \ + ' UnattendMode=FullUnattended' \ + ' OemSkipEula=Yes' \ + ' OemPreinstall=Yes' \ + ' Repartition=Yes' \ + ' WaitForReboot="No"' \ + ' DriverSigningPolicy="Ignore"' \ + ' NonDriverSigningPolicy="Ignore"' \ + ' OemPnPDriversPath="Drivers\viostor;Drivers\NetKVM;Drivers\sata"' \ + ' NoWaitAfterTextMode=1' \ + ' NoWaitAfterGUIMode=1' \ + ' FileSystem=ConvertNTFS' \ + ' ExtendOemPartition=0' \ + ' Hibernation="No"' \ + '' \ + '[GuiUnattended]' \ + ' OEMSkipRegional=1' ' OemSkipWelcome=1' " AdminPassword=\"$sifPassword\"" ' TimeZone=0' + + if disabled "$AUTOLOGIN"; then + printf '%s\n' ' AutoLogon=No' + else + printf '%s\n' \ + ' AutoLogon=Yes' ' AutoLogonCount=65432' + fi + + printf '%s\n' \ + '' \ + '[UserData]' \ + " FullName=\"$sifUsername\"" \ + " ComputerName=\"$sifHost\"" \ + " OrgName=\"$sifOrganization\"" \ + " $product" \ + '' \ + '[Identification]' \ + " JoinWorkgroup = \"$sifWorkgroup\"" \ + '' \ + '[Display]' \ + ' BitsPerPel=32' \ + " XResolution=$WIDTH" \ + " YResolution=$HEIGHT" \ + '' \ + '[Networking]' \ + ' InstallDefaultComponents=Yes' \ + '' \ + '[Branding]' \ + ' BrandIEUsingUnattended=Yes' \ + '' \ + '[URL]' \ + ' Home_Page = http://www.google.com' \ + ' 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' '' + } | unix2dos >> "$target/WINNT.SIF" || return 1 + fi + + return 0 +} + +writeRegistry() { + + local dir="$1" + local shortcut="$2" + local oem="$3" + local regUsername="$4" + local regPassword="$5" + + { + printf '%s\n' \ + 'Windows Registry Editor Version 5.00' \ + '' \ + '[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Security]' \ + '"FirstRunDisabled"=dword:00000001' \ + '"UpdatesDisableNotify"=dword:00000001' \ + '"FirewallDisableNotify"=dword:00000001' \ + '"AntiVirusDisableNotify"=dword:00000001' \ + '' \ + '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\wscsvc]' \ + '"Start"=dword:00000004' \ + '' \ + '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile\GloballyOpenPorts\List]' \ + '"3389:TCP"="3389:TCP:*:Enabled:@xpsp2res.dll,-22009"' \ + '' \ + '[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Applets\Tour]' \ + '"RunCount"=dword:00000000' \ + '' \ + '[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced]' \ + '"HideFileExt"=dword:00000000' \ + '' \ + '[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer]' \ + '"NoWelcomeScreen"="1"' \ + '' \ + '[HKEY_CURRENT_USER\Software\Microsoft\Internet Connection Wizard]' \ + '"Completed"="1"' \ + '"Desktopchanged"="1"' '' '[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]' + + if disabled "$AUTOLOGIN"; then + printf '%s\n' '"AutoAdminLogon"="0"' + else + printf '%s\n' \ + '"AutoAdminLogon"="1"' "\"DefaultUserName\"=\"$regUsername\"" "\"DefaultPassword\"=\"$regPassword\"" + fi + + printf '%s\n' \ + '' \ + '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Video\{23A77BF7-ED96-40EC-AF06-9B1F4867732A}\0000]' \ + '"DefaultSettings.BitsPerPel"=dword:00000020' \ + "\"DefaultSettings.XResolution\"=dword:$XHEX" \ + "\"DefaultSettings.YResolution\"=dword:$YHEX" \ + '' \ + '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Hardware Profiles\Current\System\CurrentControlSet\Control\VIDEO\{23A77BF7-ED96-40EC-AF06-9B1F4867732A}\0000]' \ + '"DefaultSettings.BitsPerPel"=dword:00000020' \ + "\"DefaultSettings.XResolution\"=dword:$XHEX" \ + "\"DefaultSettings.YResolution\"=dword:$YHEX" \ + '' \ + '[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce]' \ + '"ScreenSaver"="reg add \"HKCU\\Control Panel\\Desktop\" /f /v \"SCRNSAVE.EXE\" /t REG_SZ /d \"off\""' \ + '"ScreenSaverOff"="reg add \"HKCU\\Control Panel\\Desktop\" /f /v \"ScreenSaveActive\" /t REG_SZ /d \"0\""' + + if enabled "$shortcut"; then + printf '%s\n' '"SharedDrive"="cmd /C net use Z: \\\\host.lan\\Data /persistent:yes"' + fi + + printf '%s\n' "$oem" '' + } | unix2dos > "$dir/\$OEM\$/install.reg" || return 1 + + return 0 +} + +appendRegistry() { + + local dir="$1" + local driver="$2" + + if [[ "$driver" == "2k" ]]; then + { + printf '%s\n' \ + '[HKEY_USERS\.DEFAULT\Software\Microsoft\Windows\CurrentVersion\Runonce]' '"^SetupICWDesktop"=-' '' + } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 + fi + + if [[ "$driver" == "2k3" ]]; then + { + printf '%s\n' \ + '[HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\srvWiz]' \ + '@=dword:00000000' \ + '' \ + '[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ServerOOBE\SecurityOOBE]' \ + '"DontLaunchSecurityOOBE"=dword:00000000' '' + } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 + fi + + return 0 +} + +writeVBS() { + + local dir="$1" + local username="$2" + local shortcut="$3" + + # Locate the built-in Administrator by its RID 500 SID rather than its + # localized display name, then rename that account to the requested username. + + { + printf '%s\n' \ + 'Set WshShell = WScript.CreateObject("WScript.Shell")' \ + 'Set WshNetwork = WScript.CreateObject("WScript.Network")' \ + 'Set Domain = GetObject("WinNT://" & WshNetwork.ComputerName)' \ + '' \ + 'Function DecodeSID(binSID)' \ + ' ReDim o(LenB(binSID))' \ + '' \ + ' For i = 1 To LenB(binSID)' \ + ' o(i-1) = AscB(MidB(binSID, i, 1))' \ + ' Next' \ + '' \ + ' sid = "S-" & CStr(o(0)) & "-" & OctetArrayToString _' \ + ' (Array(o(2), o(3), o(4), o(5), o(6), o(7)))' \ + ' For i = 8 To (4 * o(1) + 4) Step 4' \ + ' sid = sid & "-" & OctetArrayToString _' \ + ' (Array(o(i+3), o(i+2), o(i+1), o(i)))' \ + ' Next' \ + '' \ + ' DecodeSID = sid' \ + 'End Function' \ + '' \ + 'Function OctetArrayToString(arr)' \ + ' v = 0' \ + ' For i = 0 To UBound(arr)' \ + ' v = v * 256 + arr(i)' \ + ' Next' \ + '' \ + ' OctetArrayToString = CStr(v)' \ + 'End Function' \ + '' \ + 'For Each DomainItem in Domain' \ + ' If DomainItem.Class = "User" Then' \ + ' sid = DecodeSID(DomainItem.Get("objectSID"))' \ + ' If Left(sid, 9) = "S-1-5-21-" And Right(sid, 4) = "-500" Then' \ + ' LocalAdminADsPath = DomainItem.ADsPath' \ + ' Exit For' \ + ' End If' ' End If' 'Next' '' "Call Domain.MoveHere(LocalAdminADsPath, \"$username\")" '' + + if enabled "$shortcut"; then + printf '%s\n' \ + 'Set oLink = WshShell.CreateShortcut(WshShell.SpecialFolders("Desktop") & "\Shared.lnk")' \ + 'With oLink' ' .TargetPath = "\\host.lan\Data"' ' .Save' 'End With' 'Set oLink = Nothing' '' + fi + } | unix2dos > "$dir/\$OEM\$/install.vbs" || return 1 + + { + printf '%s\n' \ + '[COMMANDS]' '"REGEDIT /s install.reg"' '"Wscript install.vbs"' '' + } | unix2dos > "$dir/\$OEM\$/cmdlines.txt" || return 1 + + return 0 +} + +disableAutoReboot() { + + local target="$1" + + local file rc=0 + local pattern='^[[:space:]]*HKLM[[:space:]]*,[[:space:]]*"SYSTEM\\CurrentControlSet\\Control\\CrashControl"[[:space:]]*,[[:space:]]*"AutoReboot"[[:space:]]*,[[:space:]]*[^,]*,' + + file=$(find "$target" -maxdepth 1 -type f -iname HIVESYS.INF -print -quit) || return 1 + + if [ -z "$file" ]; then + error "The file HIVESYS.INF could not be found!" + return 1 + fi + + # Keep setup crashes visible instead of immediately rebooting into an + # opaque installation loop. + grep -Eqi "${pattern}[[:space:]]*[^,;[:space:]]+" "$file" || rc=$? + + case "$rc" in + 0 ) + sed -i -E "s|(${pattern})[[:space:]]*[^,;[:space:]]+|\\1 0|I" "$file" || : + ;; + 1 ) + printf '%s\n' \ + 'HKLM,"SYSTEM\CurrentControlSet\Control\CrashControl","AutoReboot",0x00010001,0' | + unix2dos >> "$file" || : + ;; + esac + + return 0 +} + +legacyInstall() { + + local dir="$2" + local desc="$3" + local driver="$4" + + local shortcut="Y" + local drivers="/tmp/drivers" + + 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 + + ETFS="[BOOT]/Boot-NoEmul.img" + + if [ ! -f "$dir/$ETFS" ] || [ ! -s "$dir/$ETFS" ]; then + error "Failed to locate file \"$ETFS\" in $desc ISO image!" + return 1 + fi + + # Legacy media uses directory names rather than metadata to identify the + # architecture and the text-mode setup source tree. + local arch="amd64" + [ ! -d "$dir/AMD64" ] && arch="x86" + + local target="$dir/AMD64" + [[ "${arch,,}" == "x86" ]] && target="$dir/I386" + + if [ ! -d "$target" ]; then + error "Failed to locate directory \"$target\" in $desc ISO image!" + return 1 + fi + + if [[ "${driver,,}" == "xp" || "${driver,,}" == "2k3" ]]; then + addLegacyDrivers "$dir" "$target" "$driver" "$arch" "$drivers" || return 1 + fi + + disableAutoReboot "$target" || return 1 + setLegacyKey "$target" "$driver" "$arch" "$desc" || return 1 + validateProductKey "$KEY" || return 1 + + local product="" + [ -n "$KEY" ] && product="ProductID=$KEY" + + mkdir -p "$dir/\$OEM\$" || return 1 + + if ! addFolder "$dir"; then + error "Failed to add OEM folder to image!" + return 1 + fi + + local oem="" + local install="" + local oem_dir="$dir/\$OEM\$/\$1/OEM" + + if [ -d "$oem_dir" ]; then + install=$(find \ + "$oem_dir" -maxdepth 1 -type f -iname install.bat -print -quit + ) || return 1 + fi + + oem=$(writeCommand "$install") || return 1 + + [ -z "$WIDTH" ] && WIDTH="1280" + [ -z "$HEIGHT" ] && HEIGHT="720" + + validateResolution "WIDTH" "$WIDTH" 320 || return 1 + validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 + validateMembership || return 1 + validateComputerName "$HOST" || return 1 + + validateLegacyText "APP" "$APP" "$desc" || return 1 + validateLegacyText "ENGINE" "$ENGINE" "$desc" || return 1 + + if [[ "$driver" == "2k" ]]; then + validateLegacyEncoding "APP" "$APP" "$desc" || return 1 + validateLegacyEncoding "ENGINE" "$ENGINE" "$desc" || return 1 + fi + + XHEX=$(printf '%08x\n' "$((10#$WIDTH))") || return 1 + YHEX=$(printf '%08x\n' "$((10#$HEIGHT))") || return 1 + + local username="${USERNAME:-Docker}" + local password="${PASSWORD:-admin}" + local workgroup="${WORKGROUP:-WORKGROUP}" + + local sifHost sifUsername sifPassword sifOrganization sifWorkgroup + local regUsername regPassword + + validateLegacyUsername "$username" "$desc" || return 1 + validatePassword "$password" "$desc" || return 1 + + if [[ "$driver" == "2k" ]]; then + validateLegacyEncoding "USERNAME" "$username" "$desc" || return 1 + validateLegacyEncoding "PASSWORD" "$password" "$desc" || return 1 + validateLegacyEncoding "WORKGROUP" "$workgroup" "$desc" || return 1 + fi + + # WINNT.SIF and .reg files use different escaping rules, so prepare their + # values independently before generating either file. + sifHost=$(escapeSIFValue "${HOST:-*}") || return 1 + sifUsername=$(escapeSIFValue "$username") || return 1 + sifPassword=$(escapeSIFValue "$password") || return 1 + sifOrganization=$(escapeSIFValue "$APP for $ENGINE") || return 1 + sifWorkgroup=$(escapeSIFValue "$workgroup") || return 1 + regUsername=$(escapeRegistryValue "$username") || return 1 + regPassword=$(escapeRegistryValue "$password") || return 1 + + writeSIF \ + "$target" \ + "$driver" \ + "$product" "$sifHost" "$sifUsername" "$sifPassword" "$sifOrganization" "$sifWorkgroup" || return 1 + + writeRegistry "$dir" "$shortcut" "$oem" "$regUsername" "$regPassword" || return 1 + + appendRegistry "$dir" "$driver" || return 1 + writeVBS "$dir" "$username" "$shortcut" || return 1 + + return 0 +} + +escapeSIFValue() { + + local s="$1" + + s=${s//%/%%} + s=${s//\"/\"\"} + + printf '%s' "$s" + return 0 +} + +escapeRegistryValue() { + + printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' +} + +validateLegacyText() { + + local name="$1" + local value="$2" + local desc="${3:-}" + + local suffix="" + [ -n "$desc" ] && suffix=" for $desc" + + if [[ "$value" =~ [[:cntrl:]] ]]; then + error "The $name variable cannot contain control characters$suffix!" + return 1 + fi + + if [[ "$value" == *'"'* ]]; then + error "The $name variable cannot contain double quotes$suffix!" + return 1 + fi + + return 0 +} + +validateLegacyUsername() { + + local value="$1" + local desc="${2:-}" + + local suffix="" + [ -n "$desc" ] && suffix=" for $desc" + + if [ -z "$value" ]; then + error "The USERNAME variable cannot be empty$suffix!" + return 1 + fi + + if [ "${#value}" -gt 20 ]; then + error "The USERNAME variable cannot contain more than 20 characters$suffix!" + return 1 + fi + + if [[ "$value" =~ [[:cntrl:]] ]]; then + error "The USERNAME variable cannot contain control characters$suffix!" + return 1 + fi + + case "$value" in + *'"'* | *'/'* | *\\* | *'['* | *']'* | *':'* | *';'* | *'|'* | *'='* | \ + *','* | *'+'* | *'*'* | *'?'* | *'<'* | *'>'* | *'%'* ) + error "The USERNAME variable contains unsupported characters$suffix!" + return 1 ;; + esac + + if [[ "$value" == *"." ]]; then + error "The USERNAME variable cannot end with a period$suffix!" + return 1 + fi + + if [[ "$value" =~ ^[.[:space:]]+$ ]]; then + error "The USERNAME variable cannot consist only of spaces or periods$suffix!" + return 1 + fi + + case "${value^^}" in + + "NONE" ) + error "The USERNAME value \"NONE\" is reserved by Windows$suffix!" + return 1 ;; + + "ADMINISTRATOR" | "GUEST" | "DEFAULTACCOUNT" | "WDAGUTILITYACCOUNT" | "WSIACCOUNT" ) + error "The USERNAME value \"$value\" is reserved for a built-in Windows account$suffix!" + return 1 ;; + + esac + + return 0 +} + +validateLegacyEncoding() { + + local name="$1" + local value="$2" + local desc="${3:-}" + + local suffix="" + [ -n "$desc" ] && suffix=" for $desc" + + if LC_ALL=C grep -q '[^ -~]' <<< "$value"; then + error "The $name variable may only contain printable ASCII characters$suffix!" + return 1 + fi + + return 0 +} + +return 0