Files
dockur-windows/src/mido.sh
T
2026-08-03 13:17:13 +02:00

1309 lines
32 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
handleCurlError() {
local code="$1"
local server="$2"
local reason="${3:-}"
local signal
if [ -n "$reason" ] && (( code <= 125 )); then
error "Request to $server servers failed: ${reason%.}."
return 1
fi
case "$code" in
126) error "The curl command could not be executed." ;;
127) error "The curl command was not found." ;;
*)
if (( code < 129 )); then
error "Request to $server servers failed with curl exit status $code."
return 1
fi
signal=$(kill -l "$((code - 128))" 2>/dev/null || true)
case "$signal" in
INT) error "Curl was interrupted." ;;
SEGV | ABRT) error "Curl crashed with signal $signal." ;;
"") error "Curl terminated with exit status $code." ;;
*) error "Curl terminated due to signal $signal." ;;
esac
;;
esac
return 1
}
curlRequest() {
local output="$1"
local server="$2"
local agent="$3"
shift 3
local log reason response
if ! log=$(mktemp -p "$QEMU_DIR"); then
error "Failed to create a temporary curl log."
return 1
fi
{
response=$(LC_ALL=C curl \
--silent \
--show-error \
--max-time 30 \
--user-agent "$agent" \
--fail \
--proto =https \
--tlsv1.2 \
--http1.1 \
"$@" 2>"$log")
local rc=$?
} || :
if (( rc != 0 )); then
reason=$(sed -nE 's/^curl: \([0-9]+\) //p' "$log" | tail -n 1)
rm -f "$log"
handleCurlError "$rc" "$server" "$reason"
return 1
fi
rm -f "$log"
if [ -n "$output" ]; then
printf -v "$output" '%s' "$response"
fi
return 0
}
downloadWindowsLink() {
local productId="$1"
local url="$2"
local agent="$3"
local language="$4"
local lang="$5"
local desc="$6"
local type="$7"
local skuId skuJson
local linkJson link
local ovData ovTime session
local ovToken="" ovTicks=""
local profile="606624d44113"
# uuidgen: For MacOS (installed by default) and other systems (e.g. with no /proc) that don't have a kernel interface for generating random UUIDs
if ! session=$(cat /proc/sys/kernel/random/uuid 2> /dev/null || uuidgen --random); then
error "Failed to generate session ID!"
return 1
fi
session="${session//[![:print:]]/}"
if [ -z "$session" ]; then
error "Failed to generate session ID!"
return 1
fi
# Microsoft download "protection" requires the sessionId to be whitelisted through vlscppe.microsoft.com/tags
local orgId="y6jn8c31"
local vlsUrl="https://vlscppe.microsoft.com/tags?org_id=$orgId&session_id=$session"
enabled "$DEBUG" && echo "Getting Session ID: $session"
# Permit Session ID
curlRequest "" "Microsoft" "$agent" \
--output /dev/null \
--header "Accept:" \
--max-filesize 100K \
-- "$vlsUrl" || return 1
# Microsoft download "protection" also requires an ov-df.microsoft.com request/reply
# 1) Request mdt.js to get w and rticks. InstanceId is (currently) constant.
local instance="560dc9f3-1aa5-4a2f-b63c-9e18f8d0e175"
local ovUrl="https://ov-df.microsoft.com/mdt.js?instanceId=$instance&PageId=si&session_id=$session"
enabled "$DEBUG" && echo -n "Getting OV data: "
curlRequest ovData "Microsoft" "$agent" \
--header "Accept:" \
--max-filesize 1M \
-- "$ovUrl" || return 1
if [[ $ovData =~ [\?\&]w=([A-Fa-f0-9]+) ]]; then
ovToken="${BASH_REMATCH[1]}"
fi
if [[ $ovData =~ rticks=\"\+?([0-9]+) ]]; then
ovTicks="${BASH_REMATCH[1]}"
fi
if [[ -z $ovToken || -z $ovTicks ]]; then
error "Could not extract ov-df data from Microsoft server!"
return 1
fi
enabled "$DEBUG" && echo "$ovToken"
sleep 0.2
# 2) Send a reply with session ID, current epoch and previously retrieved w and rticks
ovTime=$(date +%s%3N)
ovUrl="https://ov-df.microsoft.com/?session_id=$session&CustomerId=$instance&PageId=si&w=$ovToken&mdt=$ovTime&rticks=$ovTicks"
enabled "$DEBUG" && echo "Sending OV reply: $instance"
curlRequest "" "Microsoft" "$agent" \
--output /dev/null \
--header "Accept:" \
--max-filesize 100K \
-- "$ovUrl" || return 1
enabled "$DEBUG" && echo -n "Getting language SKU ID: "
local skuUrl="https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?profile=$profile&ProductEditionId=$productId&SKU=undefined&friendlyFileName=undefined&Locale=en-US&sessionID=$session"
curlRequest skuJson "Microsoft" "$agent" \
--referer "$url" \
--header "Accept:" \
--max-filesize 100K \
-- "$skuUrl" || return 1
{ skuId=$(printf '%s\n' "$skuJson" | jq --arg LANG "$language" -r 'first(.Skus[]? | select(.Language == $LANG) | .Id) // empty') 2>/dev/null; local rc=$?; } || :
if [ -z "$skuId" ] || [[ "${skuId,,}" == "null" ]] || (( rc != 0 )); then
language=$(getLanguage "$lang" "desc")
error "No download in the $language language available for $desc!"
return 1
fi
enabled "$DEBUG" && echo "$skuId"
enabled "$DEBUG" && echo "Getting ISO download link..."
# Get ISO download link
# If any request is going to be blocked by Microsoft it's always this last one (the previous requests always seem to succeed)
local linkUrl="https://www.microsoft.com/software-download-connector/api/GetProductDownloadLinksBySku?profile=$profile&ProductEditionId=undefined&SKU=$skuId&friendlyFileName=undefined&Locale=en-US&sessionID=$session"
curlRequest linkJson "Microsoft" "$agent" \
--referer "$url" \
--header "Accept:" \
--max-filesize 100K \
-- "$linkUrl" || return 1
if ! [ "$linkJson" ]; then
# This should only happen if there's been some change to how this API works
error "Microsoft servers gave us an empty response to our request for an automated download."
return 1
fi
if grep -Fq "Sentinel marked this request as rejected." <<< "$linkJson"; then
error "Microsoft blocked the automated download request based on your IP address."
return 1
fi
if grep -Fq "We are unable to complete your request at this time." <<< "$linkJson"; then
error "Microsoft blocked the automated download request."
return 1
fi
{ link=$(printf '%s\n' "$linkJson" | jq --argjson TYPE "$type" -r 'first(.ProductDownloadOptions[]? | select(.DownloadType == $TYPE) | .Uri) // empty') 2>/dev/null; rc=$?; } || :
if [ -z "$link" ] || [[ "${link,,}" == "null" ]] || (( rc != 0 )); then
error "Microsoft server gave us no download link to our request for an automated download!"
info "Response: $linkJson"
return 1
fi
MIDO_URL="$link"
return 0
}
downloadWindows() {
local id="$1"
local lang="$2"
local desc="$3"
local agent language
local page productId
local type winVer
agent=$(getAgent)
language=$(getLanguage "$lang" "name")
case "${id,,}" in
"win10x64" )
productId="2618"
winVer="10"
type="1"
;;
"win11x64" )
productId="3321"
winVer="11"
type="1"
;;
"win11arm64" )
productId="3324"
winVer="11arm64"
type="2"
;;
* )
error "Invalid VERSION specified, value \"$id\" is not recognized!"
return 1
;;
esac
local url="https://www.microsoft.com/en-us/software-download/windows$winVer"
[[ "${id,,}" == "win10"* ]] && url+="ISO"
enabled "$DEBUG" && echo "Using Product edition ID: $productId"
if downloadWindowsLink "$productId" "$url" "$agent" "$language" "$lang" "$desc" "$type"; then
return 0
fi
sleep 1
local msg="retrying using a different method..."
info "Microsoft download request failed, $msg"
enabled "$DEBUG" && echo "Parsing download page: ${url}"
curlRequest page "Microsoft" "$agent" \
--header "Accept:" \
--max-filesize 1M \
-- "$url" || return 1
enabled "$DEBUG" && echo -n "Getting Product edition ID: "
productId=$(printf '%s' "$page" |
tr '\r\n' ' ' |
grep -Eio "<option[^>]*value=[\"'][0-9]+[\"'][^>]*>[[:space:]]*Windows[^<]*" |
sed -nE "s/.*value=[\"']([0-9]+)[\"'].*/\1/p" |
sed -n '1p' |
cut -c 1-16 || true)
enabled "$DEBUG" && echo "$productId"
if [ -z "$productId" ]; then
info "Failed to fetch the Product edition ID from the download page, $msg"
return 1
fi
if ! downloadWindowsLink "$productId" "$url" "$agent" "$language" "$lang" "$desc" "$type"; then
return 1
fi
return 0
}
downloadWindowsEval() {
local id="$1"
local lang="$2"
local desc="$3"
local culture compare type
local agent language winVer
case "${id,,}" in
"win11${PLATFORM,,}-enterprise-eval" )
type="enterprise"
winVer="windows-11-enterprise" ;;
"win11${PLATFORM,,}-enterprise-iot-eval" )
type="iot"
winVer="windows-11-iot-enterprise-ltsc-eval" ;;
"win11${PLATFORM,,}-enterprise-ltsc-eval" )
type="ltsc"
winVer="windows-11-enterprise" ;;
"win2025-eval" )
type="server"
winVer="windows-server-2025" ;;
"win2022-eval" )
type="server"
winVer="windows-server-2022" ;;
"win2019-hv" )
type="server"
winVer="hyper-v-server-2019" ;;
"win2019-eval" )
type="server"
winVer="windows-server-2019" ;;
"win2016-eval" )
type="server"
winVer="windows-server-2016" ;;
"win2012r2-eval" )
type="server"
winVer="windows-server-2012-r2" ;;
* )
error "Invalid VERSION specified, value \"$id\" is not recognized!" && return 1 ;;
esac
agent=$(getAgent)
culture=$(getLanguage "$lang" "culture")
local country="${culture#*-}"
local link="" links page
local url="https://www.microsoft.com/en-us/evalcenter/download-$winVer"
enabled "$DEBUG" && echo "Parsing download page: ${url}"
curlRequest page "Microsoft" "$agent" \
--location \
--max-filesize 1M \
-- "$url" || return 1
if ! [ "$page" ]; then
# This should only happen if there's been some change to where this download page is located
error "Windows server download page gave us an empty response"
return 1
fi
enabled "$DEBUG" && echo "Getting download link.."
# Normalize HTML-encoded query separators before extracting fwlinks.
page=${page//&amp;/&}
page=${page//&#38;/&}
links=$(printf '%s\n' "$page" |
grep -Eio "https://go\.microsoft\.com/fwlink(/p)?/\?[^\"'<>[:space:]]+" |
grep -Ei '(^|[?&])culture='"${culture,,}"'(&|$)' |
grep -Ei '(^|[?&])country='"${country,,}"'(&|$)') || {
# This should only happen if there's been some change to the download endpoint web address
if [[ "${lang,,}" == "en" || "${lang,,}" == "en-"* ]]; then
error "Windows server download page gave us no download link!"
else
language=$(getLanguage "$lang" "desc")
error "No download in the $language language available for $desc!"
fi
return 1
}
case "$type" in
"iot" )
case "${PLATFORM,,}" in
"x64" )
link=$(printf '%s\n' "$links" | head -n 1) ;;
"arm64" )
link=$(printf '%s\n' "$links" | head -n 2 | tail -n 1) ;;
esac ;;
"ltsc" )
case "${PLATFORM,,}" in
"x64" )
link=$(printf '%s\n' "$links" | head -n 2 | tail -n 1) ;;
esac ;;
"enterprise" )
case "${PLATFORM,,}" in
"x64" )
if [[ "$winVer" != "windows-10"* ]]; then
link=$(printf '%s\n' "$links" | head -n 1)
else
link=$(printf '%s\n' "$links" | head -n 2 | tail -n 1)
fi ;;
"arm64" )
link=$(printf '%s\n' "$links" | head -n 2 | tail -n 1) ;;
esac ;;
"server" )
case "${PLATFORM,,}" in
"x64" )
link=$(printf '%s\n' "$links" | head -n 1) ;;
esac ;;
* )
error "Invalid type specified, value \"$type\" is not recognized!" && return 1 ;;
esac
[ -z "$link" ] && error "Could not parse download link from page!" && return 1
# Follow redirect so proceeding log message is useful
# This is a request we make that Fido doesn't
curlRequest link "Microsoft" "$agent" \
--location \
--output /dev/null \
--write-out "%{url_effective}" \
--head \
-- "$link" || return 1
local lower="${link,,}"
local separator='(^|[[:space:]_./-])'
case "${PLATFORM,,}" in
"x64" )
if [[ "$lower" =~ ${separator}(arm64|a64) ]]; then
echo "Found download link: $link"
error "Download link is for the wrong platform? Please report this at $SUPPORT/issues"
return 1
fi ;;
"arm64" )
if [[ "$lower" =~ ${separator}(x64|x86|amd64) ]]; then
if enabled "$DEBUG"; then
echo "Found download link: $link"
echo "Link for ARM platform currently not available!"
fi
return 1
fi ;;
esac
if enabled "$DEBUG" && enabled "$VERIFY" && [[ "${lang,,}" == "en"* ]]; then
compare=$(getMido "$id" "$lang" "")
if [ -n "$compare" ]; then
link_name="${link%%[?#]*}"
link_name="${link_name##*/}"
compare_name="${compare%%[?#]*}"
compare_name="${compare_name##*/}"
if [[ "${link_name,,}" != "${compare_name,,}" ]]; then
echo "Retrieved ISO file $link_name does not match the pre-defined filename: $compare_name"
fi
fi
fi
MIDO_URL="$link"
return 0
}
getMidoDetected() {
# Return the answer-file identity for the Microsoft source that actually
# succeeded without changing the global DETECTED value.
local version="${1,,}"
local source="${2,,}"
local current="$3"
local default="$version"
local detected
[ -z "$source" ] && source="$version"
# Preserve a DETECTED value that existed before SUGGEST was assigned.
if enabled "${DETECTED_ORG:-}"; then
echo "$current"
return 0
fi
# Derive the normal answer-file identity from the requested download route.
case "$default" in
*"-enterprise-ltsc-eval" )
default="${default%-enterprise-ltsc-eval}-ltsc"
;;
*"-enterprise-iot-eval" )
default="${default%-enterprise-iot-eval}-iot"
;;
*"-eval" )
default="${default%-eval}"
;;
esac
# Preserve a genuinely different DETECTED override.
if [ -n "$current" ] && [[ "${current,,}" != "$default" ]]; then
echo "$current"
return 0
fi
# Select the answer-file identity for the source that actually succeeded.
case "$source" in
*"-enterprise-ltsc-eval" )
detected="${source%-enterprise-ltsc-eval}-ltsc-eval"
;;
*"-enterprise-iot-eval" )
detected="${source%-enterprise-iot-eval}-iot-eval"
;;
*"-eval" )
detected="$source"
;;
* )
detected="${current:-$default}"
;;
esac
echo "$detected"
return 0
}
downloadWindowsLtsc() {
local id="$1"
local lang="$2"
local desc="$3"
local alternate alternate_desc
case "${id,,}" in
"win11${PLATFORM,,}-enterprise-iot-eval" )
alternate="win11${PLATFORM,,}-enterprise-ltsc-eval"
;;
"win11${PLATFORM,,}-enterprise-ltsc-eval" )
alternate="win11${PLATFORM,,}-enterprise-iot-eval"
;;
* )
error "Invalid VERSION specified, value \"$id\" is not recognized!"
return 1
;;
esac
if downloadWindowsEval "$id" "$lang" "$desc" > /dev/null 2>&1; then
MIDO_SOURCE="$id"
return 0
fi
alternate_desc=$(printEdition "$alternate" "$alternate" "Y")
info "Primary download source failed, trying $alternate_desc instead..."
if downloadWindowsEval "$alternate" "$lang" "$alternate_desc"; then
MIDO_SOURCE="$alternate"
warn "the requested $desc was unavailable, using $alternate_desc instead."
return 0
fi
return 1
}
getWindows() {
local version="$1"
local lang="$2"
local desc="$3"
local web_desc="$4"
local language edition
MIDO_SOURCE=""
MIDO_STATIC="N"
language=$(getLanguage "$lang" "desc")
edition=$(printEdition "$version" "$desc" "Y")
local msg="Requesting $desc from the Microsoft servers..."
local web_msg="Requesting $web_desc from the Microsoft servers..."
info "$msg" && html "$web_msg"
case "${version,,}" in
"win2008r2"* | \
"win81${PLATFORM,,}"* | \
"win10${PLATFORM,,}-enterprise"* | \
"win11${PLATFORM,,}-enterprise-iot-eval" )
if [[ "${lang,,}" != "en" && "${lang,,}" != "en-"* ]]; then
error "No download in the $language language available for $edition!"
MIDO_URL=""
return 1
fi ;;
esac
case "${version,,}" in
"win10x64" ) ;;
"win11${PLATFORM,,}" ) ;;
"win11${PLATFORM,,}-enterprise"* ) ;;
* )
if [[ "${PLATFORM,,}" != "x64" ]]; then
error "No download for the ${PLATFORM^^} platform available for $edition!"
MIDO_URL=""
return 1
fi ;;
esac
case "${version,,}" in
"win10x64" | "win11${PLATFORM,,}" )
if downloadWindows "$version" "$lang" "$edition"; then
MIDO_SOURCE="$version"
return 0
fi ;;
"win11${PLATFORM,,}-enterprise-iot-eval" | \
"win11${PLATFORM,,}-enterprise-ltsc-eval" )
downloadWindowsLtsc "$version" "$lang" "$edition" && return 0
;;
"win11${PLATFORM,,}-enterprise"* )
if downloadWindowsEval "$version" "$lang" "$edition"; then
MIDO_SOURCE="$version"
return 0
fi ;;
"win2025-eval" | "win2022-eval" | "win2019-eval" | \
"win2019-hv" | "win2016-eval" | "win2012r2-eval" )
if downloadWindowsEval "$version" "$lang" "$edition"; then
MIDO_SOURCE="$version"
return 0
fi ;;
"win2008r2"*| "win81${PLATFORM,,}"* | "win10${PLATFORM,,}-enterprise"* ) ;;
* )
error "Invalid VERSION specified, value \"$version\" is not recognized!"
return 1
;;
esac
MIDO_URL=$(getMido "$version" "$lang" "")
[ -z "$MIDO_URL" ] && return 1
MIDO_STATIC="Y"
if [[ "${version,,}" == "win2008r2"* ]]; then
MIDO_SOURCE="win2008r2-eval"
return 0
fi
MIDO_SOURCE="$version"
return 0
}
getBuild() {
local id="$1"
local ret="$2"
local url=""
local name=""
local build="$3"
local edition=""
local file="catalog.xml"
case "${id,,}" in
"win11${PLATFORM,,}" )
name="Windows 11 Pro"
url="https://worproject.com/dldserv/esd/getcatalog.php?build=${build}&arch=${PLATFORM^^}&edition=Professional" ;;
"win11${PLATFORM,,}-enterprise" | "win11${PLATFORM,,}-enterprise-eval")
name="Windows 11 Enterprise"
url="https://worproject.com/dldserv/esd/getcatalog.php?build=${build}&arch=${PLATFORM^^}&edition=Enterprise" ;;
esac
case "${ret,,}" in
"url" ) echo "$url" ;;
"file" ) echo "$file" ;;
"name" ) echo "$name" ;;
"edition" ) echo "$edition" ;;
*) echo "";;
esac
return 0
}
getCatalog() {
local id="$1"
local ret="$2"
local url=""
local name=""
local edition=""
local file="catalog.cab"
if [[ "${id,,}" == "win11"* ]] && ! isCompatible; then
# ARMv8.0 cannot run Windows 11 builds 24H2 and up.
getBuild "$1" "$2" "22631.2861" && return 0
fi
case "${id,,}" in
"win11${PLATFORM,,}" )
edition="Professional"
name="Windows 11 Pro"
url="https://go.microsoft.com/fwlink?linkid=2156292" ;;
"win10${PLATFORM,,}" )
edition="Professional"
name="Windows 10 Pro"
url="https://go.microsoft.com/fwlink/?LinkId=841361" ;;
"win11${PLATFORM,,}-enterprise" | "win11${PLATFORM,,}-enterprise-eval")
edition="Enterprise"
name="Windows 11 Enterprise"
url="https://go.microsoft.com/fwlink?linkid=2156292" ;;
"win10${PLATFORM,,}-enterprise" | "win10${PLATFORM,,}-enterprise-eval" )
edition="Enterprise"
name="Windows 10 Enterprise"
url="https://go.microsoft.com/fwlink/?LinkId=841361" ;;
esac
case "${ret,,}" in
"url" ) echo "$url" ;;
"file" ) echo "$file" ;;
"name" ) echo "$name" ;;
"edition" ) echo "$edition" ;;
*) echo "";;
esac
return 0
}
getESD() {
local dir="$1"
local version="$2"
local lang="$3"
local desc="$4"
local file culture
local language edition catalog
local xmlFile="products.xml"
local log
file=$(getCatalog "$version" "file")
catalog=$(getCatalog "$version" "url")
culture=$(getLanguage "$lang" "culture")
edition=$(getCatalog "$version" "edition")
if [ -z "$file" ] || [ -z "$catalog" ]; then
error "Invalid VERSION specified, value \"$version\" is not recognized!"
return 1
fi
local msg="Downloading catalog from the Microsoft servers..."
info "$msg" && html "$msg"
rm -rf "$dir"
if ! makeDir "$dir"; then
error "Failed to create directory \"$dir\" !"
return 1
fi
if ! log=$(mktemp -p "$QEMU_DIR"); then
error "Failed to create a temporary wget log."
return 1
fi
{
LC_ALL=C wget "$catalog" -O "$dir/$file" --no-verbose --timeout=30 \
--no-http-keep-alive --output-file="$log"
local rc=$?
} || :
if (( rc != 0 )); then
local reason
reason=$(sed -n \
-e 's/^wget: //p' \
-e 's/^[0-9-]\{10\} [0-9:]\{8\} ERROR //p' \
"$log" | tail -n 1)
msg="Failed to download $catalog"
if (( rc == 3 )); then
error "$msg because the file could not be written (disk full?)."
elif [ -n "$reason" ]; then
error "$msg: ${reason%.}."
else
error "$msg with exit status $rc."
fi
rm -f "$log"
return 1
fi
rm -f "$log"
if [[ "$file" == *".xml" ]]; then
if ! mv -f "$dir/$file" "$dir/$xmlFile"; then
error "Failed to rename $file to $xmlFile."
return 1
fi
else
if ! (
cd "$dir" || exit 1
cabextract "$file" > /dev/null
); then
error "Failed to extract $file!"
return 1
fi
fi
local xml="$dir/$xmlFile"
if [ ! -s "$xml" ]; then
error "Failed to find $xmlFile in $file!"
return 1
fi
local files="//*[local-name()='File']"
local values="$dir/.catalog-values"
local -a architectures=()
local -a editions=()
local -a cultures=()
local -a paths=()
local -a sums=()
local -a sizes=()
if ! xmlstarlet sel -T -t \
-m "$files" \
-v "normalize-space(*[local-name()='Architecture'])" -n \
"$xml" > "$values"; then
error "Failed to parse $xmlFile!"
return 1
fi
mapfile -t architectures < "$values"
if ! xmlstarlet sel -T -t \
-m "$files" \
-v "normalize-space(*[local-name()='Edition'])" -n \
"$xml" > "$values"; then
error "Failed to parse $xmlFile!"
return 1
fi
mapfile -t editions < "$values"
if ! xmlstarlet sel -T -t \
-m "$files" \
-v "normalize-space(*[local-name()='LanguageCode'])" -n \
"$xml" > "$values"; then
error "Failed to parse $xmlFile!"
return 1
fi
mapfile -t cultures < "$values"
if ! xmlstarlet sel -T -t \
-m "$files" \
-v "string(*[local-name()='FilePath'])" -n \
"$xml" > "$values"; then
error "Failed to parse $xmlFile!"
return 1
fi
mapfile -t paths < "$values"
if ! xmlstarlet sel -T -t \
-m "$files" \
-v "normalize-space(*[local-name()='Sha1'])" -n \
"$xml" > "$values"; then
error "Failed to parse $xmlFile!"
return 1
fi
mapfile -t sums < "$values"
if ! xmlstarlet sel -T -t \
-m "$files" \
-v "normalize-space(*[local-name()='Size'])" -n \
"$xml" > "$values"; then
error "Failed to parse $xmlFile!"
return 1
fi
mapfile -t sizes < "$values"
rm -f "$values"
local count="${#architectures[@]}"
if [ "${#editions[@]}" -ne "$count" ] ||
[ "${#cultures[@]}" -ne "$count" ] ||
[ "${#paths[@]}" -ne "$count" ] ||
[ "${#sums[@]}" -ne "$count" ] ||
[ "${#sizes[@]}" -ne "$count" ]; then
error "Failed to read consistent records from $xmlFile!"
return 1
fi
local index
local file_match=0
local language_match=0
ESD=""
ESD_SUM=""
ESD_SIZE=""
for index in "${!architectures[@]}"; do
[ "${architectures[$index],,}" = "${PLATFORM,,}" ] || continue
if [ -n "$edition" ] &&
[ "${editions[$index],,}" != "${edition,,}" ]; then
continue
fi
file_match=1
[ "${cultures[$index],,}" = "${culture,,}" ] || continue
language_match=1
ESD="${paths[$index]}"
ESD_SUM="${sums[$index]}"
ESD_SIZE="${sizes[$index]}"
break
done
if (( ! file_match )); then
desc=$(printEdition "$version" "$desc" "Y")
error "No download link available for $desc!"
return 1
fi
if (( ! language_match )); then
desc=$(printEdition "$version" "$desc" "Y")
language=$(getLanguage "$lang" "desc")
error "No download in the $language language available for $desc!"
return 1
fi
if [ -z "$ESD" ]; then
error "Failed to find ESD URL in $xmlFile!"
return 1
fi
if [ -z "$ESD_SUM" ]; then
error "Failed to find ESD checksum in $xmlFile!"
return 1
fi
if [ -z "$ESD_SIZE" ]; then
error "Failed to find ESD filesize in $xmlFile!"
return 1
fi
rm -rf "$dir"
return 0
}
isCompressed() {
local url="${1%%\?*}"
case "${url,,}" in
*.7z | *.zip | *.rar | *.tar | *.cab | *.cpio | \
*.lzh | *.lha | *.xar | */latest-x86-gcc-lin-rel )
return 0
;;
esac
return 1
}
verifyFile() {
local iso="$1"
local size="$2"
local total="$3"
local check="$4"
if [ -n "$size" ] && [[ "$total" != "$size" && "$size" != "0" ]]; then
if enabled "$VERIFY" || enabled "$DEBUG"; then
warn "The downloaded file has a different size ( $total bytes) than expected ( $size bytes). Please report this at $SUPPORT/issues"
fi
fi
local algo="SHA256"
local hash
[ -z "$check" ] && return 0
enabled "$VERIFY" || return 0
[[ "${#check}" == "40" ]] && algo="SHA1"
local msg="Verifying downloaded ISO..."
info "$msg" && html "$msg"
if [[ "${algo,,}" != "sha256" ]]; then
hash=$(sha1sum "$iso" | cut -f1 -d' ') || {
local rc=$?
if (( rc >= 129 )); then
exit "$rc"
fi
error "Failed to calculate SHA1 checksum for $iso!"
return 1
}
else
hash=$(sha256sum "$iso" | cut -f1 -d' ') || {
local rc=$?
if (( rc >= 129 )); then
exit "$rc"
fi
error "Failed to calculate SHA256 checksum for $iso!"
return 1
}
fi
if [[ "$hash" == "$check" ]]; then
info "Successfully verified ISO!" && return 0
fi
error "The downloaded file has an unknown $algo checksum: $hash , as the expected value was: $check. Please report this at $SUPPORT/issues"
return 1
}
downloadFile() {
local iso="$1"
local url="$2"
local size="$3"
local desc="$4"
local web_desc="$5"
local connections="${6:-1}"
local msg="Downloading $web_desc"
local console_msg="Downloading $desc"
local domain dots
domain=$(echo "$url" | awk -F/ '{print $3}')
dots=$(echo "$domain" | tr -cd '.' | wc -c)
(( dots > 1 )) && domain=$(expr "$domain" : '.*\.\(.*\..*\)')
if [ -n "$domain" ] && [[ "${domain,,}" != *"microsoft.com" ]]; then
console_msg="Downloading $desc from $domain"
fi
info "$console_msg..."
downloadToFile \
"$url" \
"$iso" \
"$msg" \
"${size:-0}" \
"$connections" \
"Y"
}
tryDownload() {
local iso="$1"
local url="$2"
local sum="$3"
local size="$4"
local desc="$6"
local seconds="$7"
local web_desc="$8"
local total minimum="104857600"
if isCompressed "$url"; then
minimum="10485760"
fi
if downloadRetry \
"$iso" \
"${CONNECTIONS:-1}" \
"$seconds" \
"$desc" \
"$minimum" \
"$iso" \
"$url" \
"$size" \
"$desc" \
"$web_desc"; then
local rc=0
else
local rc=$?
fi
(( rc == 0 )) || return "$rc"
# The shared helper already inspected the file, so this should
# only fail if the downloaded file was removed unexpectedly afterward.
if ! total=$(stat -c%s -- "$iso" 2>/dev/null); then
error "Failed to determine downloaded file size: $iso"
return 1
fi
# Status 2 means the completed download failed deterministic validation.
if ! verifyFile "$iso" "$size" "$total" "$sum"; then
if ! rm -f -- "$iso" "$iso.aria2"; then
warn "failed to remove invalid download \"$iso\"!"
fi
return 2
fi
# Extract the .iso from the compressed archive if needed.
isCompressed "$url" && UNPACK="Y"
return 0
}
fallbackEnglish() {
local iso="$1"
local version="$2"
local lang="$3"
local desc="$4"
local web_desc="$5"
local culture web_msg
local msg="No working download method was found for $desc, falling back to English..."
info "$msg"
# Preserve the requested regional format and keyboard layout.
culture=$(getLanguage "$lang" "culture")
[ -z "$REGION" ] && REGION="$culture"
[ -z "$KEYBOARD" ] && KEYBOARD="$culture"
# Keep the original language-specific ISO filename so that restarts
# still locate the same image, but use English installation media.
LANGUAGE="en"
if ! rm -f -- "$iso"; then
error "Failed to remove ISO file \"$iso\" !"
return 1
fi
downloadImage "$iso" "$version" "$LANGUAGE"
}
downloadImage() {
local iso="$1"
local version="$2"
local lang="$3"
local requested="$version"
local tried="n"
local success="n"
local seconds="5"
local detected="$DETECTED"
local url sum size base desc web_desc language i
if [[ "${version,,}" == "http"* ]]; then
base=$(basename "$iso")
desc=$(fromFile "$base")
web_desc="$desc"
tryDownload "$iso" "$version" "" "" "" "$desc" "$seconds" "$web_desc" && return 0
return 1
fi
if ! validVersion "$version" "en"; then
error "Invalid VERSION specified, value \"$version\" is not recognized!"
return 1
fi
desc=$(printVariant "$version" "" "Y")
web_desc=$(printVariant "$version" "")
if [[ "${lang,,}" != "en" && "${lang,,}" != "en-"* ]]; then
language=$(getLanguage "$lang" "desc")
if ! validVersion "$version" "$lang"; then
desc=$(printEdition "$version" "$desc" "Y")
web_desc=$(printEdition "$version" "$web_desc")
desc+=" in $language"
fallbackEnglish "$iso" "$version" "$lang" "$desc" "$web_desc" && return 0
return 1
fi
desc+=" in $language"
fi
if isMido "$version" "$lang"; then
tried="y"
success="n"
if getWindows "$version" "$lang" "$desc" "$web_desc"; then
success="y"
else
delay "$seconds"
getWindows "$version" "$lang" "$desc" "$web_desc" && success="y"
fi
if [[ "$success" == "y" ]]; then
detected=$(getMidoDetected "$version" "$MIDO_SOURCE" "$DETECTED")
url=$(getMido "$version" "$lang" "")
sum=""
size=""
# Apply the metadata belonging to the configured static URL.
if [[ "${MIDO_URL%%\?*}" == "${url%%\?*}" ]]; then
size=$(getMido "$version" "$lang" "size")
sum=$(getMido "$version" "$lang" "sum")
fi
local download_desc="$desc"
if enabled "$MIDO_STATIC"; then
download_desc+=" using a static link"
fi
if tryDownload "$iso" "$MIDO_URL" "$sum" "$size" "$lang" "$download_desc" "$seconds" "$web_desc"; then
# Commit the candidate only after the image was downloaded and verified.
DETECTED="$detected"
return 0
fi
fi
fi
if switchEdition version; then
desc=$(printVariant "$DETECTED" "" "Y")
web_desc=$(printVariant "$DETECTED" "")
if [[ "${lang,,}" != "en" && "${lang,,}" != "en-"* ]]; then
desc+=" in $language"
fi
fi
if isESD "$version" "$lang"; then
if [[ "$tried" != "n" ]]; then
info "Failed to download $desc, will try a different method now..."
fi
tried="y"
success="n"
if getESD "$TMP/esd" "$version" "$lang" "$desc"; then
success="y"
else
delay "$seconds"
getESD "$TMP/esd" "$version" "$lang" "$desc" && success="y"
fi
if [[ "$success" == "y" ]]; then
ISO="${ISO%.*}.esd"
if tryDownload "$ISO" "$ESD" "$ESD_SUM" "$ESD_SIZE" "$lang" "$desc" "$seconds" "$web_desc"; then
return 0
fi
ISO="$iso"
fi
fi
for ((i=1;i<=MIRRORS;i++)); do
url=$(getLink "$i" "$version" "$lang")
if [ -n "$url" ]; then
if [[ "$tried" != "n" ]]; then
info "Failed to download $desc, will try another mirror now..."
fi
tried="y"
size=$(getSize "$i" "$version" "$lang")
sum=$(getHash "$i" "$version" "$lang")
tryDownload "$iso" "$url" "$sum" "$size" "$lang" "$desc" "$seconds" "$web_desc" && return 0
fi
done
if [[ "${lang,,}" != "en" && "${lang,,}" != "en-"* ]]; then
if fallbackEnglish "$iso" "$requested" "$lang" "$desc" "$web_desc"; then
return 0
fi
fi
return 1
}
return 0