Files
windows/src/image.sh
T

2179 lines
53 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
getVersions() {
local xml="$1"
local image_index image edition_id
local install_type candidate_base
local candidate candidate_id flags
local evaluation key name structured
local platform records display product
local separator=$'\x1f'
local -A indexes=()
platform=$(getPlatform "$xml") || return 1
if ! records=$(xmlstarlet sel \
-T -t \
-m '/WIM/IMAGE' \
-v 'normalize-space(@INDEX)' -o "$separator" \
-v 'normalize-space(DISPLAYNAME)' -o "$separator" \
-v 'normalize-space(WINDOWS/PRODUCTNAME)' -o "$separator" \
-v 'normalize-space(NAME)' -o "$separator" \
-v 'normalize-space(WINDOWS/EDITIONID)' -o "$separator" \
-v 'normalize-space(WINDOWS/INSTALLATIONTYPE)' -o "$separator" \
-v 'normalize-space(FLAGS)' -n \
- 2>/dev/null <<< "$xml"); then
error "Failed to read image records from WIM metadata!"
return 1
fi
while IFS="$separator" read -r image_index display product image edition_id install_type flags; do
[ -n "$image_index" ] || continue
if [[ ! "$image_index" =~ ^[1-9][0-9]*$ ]]; then
warn "Invalid image index in WIM metadata: '$image_index'"
continue
fi
candidate_id=""
candidate_base=""
for candidate in "$image" "$display" "$product"; do
[[ "$candidate" == *"Operating System"* ]] && continue
[ -n "$candidate" ] || continue
candidate_id=$(getVersion "$candidate" "$platform")
candidate_base="${candidate_id%%-*}"
[ -n "$candidate_id" ] && break
done
# NAME sometimes contains only the Windows family while DISPLAYNAME
# contains the actual edition. Use the more specific DISPLAYNAME then.
if [ -n "$candidate_base" ] && [ -n "$candidate_id" ] && [ -n "$display" ]; then
name=$(normalizeEdition "$(printVersion "$candidate_base" "")") || return 1
candidate=$(normalizeEdition "$candidate") || return 1
if [ "$candidate" = "$name" ]; then
structured=$(getVersion "$display" "$platform")
if [ -n "$structured" ] && [ "${structured%%-*}" = "$candidate_base" ] &&
[[ "${structured%-eval}" != "$candidate_base" ]] &&
[ "$(getEditionRank "$structured")" -lt 99 ]; then
candidate_id="$structured"
fi
fi
fi
if [ -z "$candidate_base" ] || [ -z "$candidate_id" ]; then
name="${display:-${image:-$product}}"
[ -n "$name" ] && warn "Unknown image name: '$name'"
continue
fi
evaluation=""
if [[ "${image,,}" == *"evaluation"* || "${display,,}" == *"evaluation"* ||
"${product,,}" == *"evaluation"* || "${edition_id,,}" == *"eval"* || "${flags,,}" == *"eval"* ]]; then
evaluation="-eval"
fi
if [ -n "$evaluation" ] && [[ "${candidate_id,,}" != *"-eval" ]]; then
candidate_id+="$evaluation"
fi
key="${candidate_id,,}"
# Friendly names can collapse two different images to the same ID.
# Structured metadata is only needed to disambiguate that collision.
if [[ -v "indexes[$key]" ]]; then
structured=""
case "${candidate_base,,}" in
"win20"* ) structured=$(normalizeServerEditionID "${flags:-$edition_id}") || return 1
if [[ "${install_type,,}" == *"core"* && "$structured" != *"-core" ]]; then
structured+="-core"
fi ;;
* ) structured=$(normalizeEditionID "${edition_id:-${flags:-}}" "$candidate_base") || return 1 ;;
esac
if [ -n "$structured" ]; then
candidate_id="$candidate_base-$structured$evaluation"
key="${candidate_id,,}"
fi
fi
if [[ -v "indexes[$key]" ]]; then
warn "Duplicate image identity '$candidate_id' at indexes ${indexes[$key]} and $image_index"
continue
fi
indexes["$key"]="$image_index"
printf '%s\t%s\n' "$candidate_id" "$image_index"
done <<< "$records"
return 0
}
getEditionRank() {
local id="${1,,}"
local base="${id%%-*}"
local edition
id="${id%-eval}"
edition="${id#"$base"}"
edition="${edition#-}"
case "$base" in
"win20"* )
case "$edition" in
"" ) echo 0 ;;
"datacenter-azure-core" | "datacenter-azure-core-"* | \
"datacenter-core" | "datacenter-core-"* ) echo 7 ;;
"enterprise-core" | "enterprise-core-"* ) echo 8 ;;
"web-core" | "web-core-"* ) echo 9 ;;
"standard-core" | "standard-core-"* ) echo 6 ;;
"datacenter" | "datacenter-"* ) echo 1 ;;
"enterprise" | "enterprise-"* ) echo 2 ;;
"web" | "web-"* ) echo 3 ;;
"foundation" | "foundation-"* ) echo 4 ;;
"essentials" | "essentials-"* ) echo 5 ;;
"hv" | "hv-"* ) echo 10 ;;
* ) echo 99 ;;
esac
;;
* )
case "$edition" in
"enterprise-iot" | "enterprise-iot-"* | "iot" | "iot-"* ) echo 3 ;;
"enterprise-ltsc" | "enterprise-ltsc-"* | "ltsc" | "ltsc-"* ) echo 4 ;;
"pro-education" | "pro-education-"* | "education" | "education-"* ) echo 5 ;;
"enterprise" | "enterprise-"* ) echo 0 ;;
"ultimate" | "ultimate-"* ) echo 1 ;;
"" | "n" | "pro" | "pro-"* | "professional" | "professional-"* | \
"business" | "business-"* ) echo 2 ;;
"home" | "home-"* ) echo 6 ;;
"starter" | "starter-"* ) echo 7 ;;
* ) echo 99 ;;
esac
;;
esac
}
selectVersion() {
local wanted="$1"
shift
local candidate record id index
local -a candidates=("$wanted")
# Normal and Evaluation variants of the same edition are compatible.
if [[ "${wanted,,}" == *"-eval" ]]; then
candidates+=("${wanted%-eval}")
else
candidates+=("$wanted-eval")
fi
for candidate in "${candidates[@]}"; do
for record in "$@"; do
IFS=$'\t' read -r id index <<< "$record"
[[ "${id,,}" == "${candidate,,}" ]] || continue
hasAnswerFile "$id" || continue
printf '%s\n%s\n' "$id" "$index"
return 0
done
done
return 1
}
detectVersion() {
local xml="$1"
local normalize="normalizeEditionID"
local output record id index base edition suffix
local -a images=() bases=() order=()
output=$(getVersions "$xml") || return
[ -n "$output" ] && mapfile -t images <<< "$output"
if [ "${#images[@]}" -eq 0 ]; then
printf '\n\n'
return 0
fi
for record in "${images[@]}"; do
IFS=$'\t' read -r id _ <<< "$record"
bases+=("${id%%-*}")
done
case "${bases[0],,}" in
"win20"* )
normalize="normalizeServerEditionID"
order=(
"" "-datacenter" "-datacenter-azure" "-enterprise" "-web"
"-foundation" "-essentials" "-standard-core" "-datacenter-core"
"-datacenter-azure-core" "-enterprise-core" "-web-core" "-hv"
) ;;
* )
order=(
"-enterprise" "-ultimate" "" "-iot" "-ltsc" "-education"
"-home" "-home-premium" "-home-basic" "-starter"
) ;;
esac
# An explicit EDITION always gets first choice.
if [ -n "$EDITION" ]; then
for base in "${bases[@]}"; do
edition=$("$normalize" "$EDITION" "$base") || return 1
if output=$(selectVersion "$base${edition:+-$edition}" "${images[@]}"); then
printf '%s\n' "$output"
return 0
fi
done
warn "edition '$EDITION' is not supported by this image, using automatic selection instead."
fi
# Prefer the normal editions in their established order.
for suffix in "${order[@]}"; do
for base in "${bases[@]}"; do
if output=$(selectVersion "$base$suffix" "${images[@]}"); then
printf '%s\n' "$output"
return 0
fi
done
done
# Otherwise keep the established family preference for noncanonical
# editions, while preserving WIM order within the same family.
local rank best_rank=99 result="" result_index=""
for record in "${images[@]}"; do
IFS=$'\t' read -r id index <<< "$record"
hasAnswerFile "$id" || continue
rank=$(getEditionRank "$id")
(( rank < best_rank )) || continue
best_rank="$rank"
result="$id"
result_index="$index"
done
if [ -n "$result" ]; then
printf '%s\n%s\n' "$result" "$result_index"
return 0
fi
# Keep the first detected identity when only manual/generic fallback remains.
IFS=$'\t' read -r id index <<< "${images[0]}"
printf '%s\n%s\n' "$id" "$index"
return 0
}
detectLanguage() {
local xml="$1"
local index="${2:-}"
local -a paths=()
local lang culture path
local image='/WIM/IMAGE'
if [[ "$index" =~ ^[1-9][0-9]*$ ]]; then
image="/WIM/IMAGE[@INDEX='$index']"
fi
# Prefer the selected image's default language, then its fallback default,
# and finally the first listed language.
paths=(
"$image/WINDOWS/LANGUAGES/DEFAULT[1]"
"$image/WINDOWS/LANGUAGES/FALLBACK/DEFAULT[1]"
"$image/WINDOWS/LANGUAGES/LANGUAGE[1]"
)
lang=""
for path in "${paths[@]}"; do
lang=$(xmlstarlet sel -T -t -v "normalize-space(string(($path)[1]))" - 2>/dev/null <<< "$xml") || lang=""
[ -n "$lang" ] && break
done
if [ -z "$lang" ]; then
warn "Language could not be detected from ISO!"
return 0
fi
culture=$(getLanguage "$lang" "culture") || return 1
if [ -n "$culture" ]; then
LANGUAGE="$lang"
return 0
fi
warn "Invalid language detected: \"$lang\""
return 0
}
getImageSize() {
local stage="$1"
local folder="${2:-}"
local size bytes path output
local required large_file
local mib=$((1024 * 1024))
local minimum=$((64 * mib))
local payload=0 paths=("$stage")
if [ ! -d "$stage" ]; then
error "Failed to find setup directory: $stage"
return 1
fi
[ -n "$folder" ] && paths+=("$folder")
# The setup image uses FAT32, so reject files that cannot be represented even
# when the image itself has enough free space.
large_file=$(find -L "${paths[@]}" -type f -size +4294967295c -print -quit) || return 1
if [ -n "$large_file" ]; then
error "Setup file exceeds the FAT32 limit: $large_file"
return 1
fi
for path in "${paths[@]}"; do
if ! output=$(du -Llsb --apparent-size -- "$path"); then
error "Failed to calculate setup size!"
return 1
fi
if ! read -r bytes _ <<< "$output" || [[ ! "$bytes" =~ ^[0-9]+$ ]]; then
error "Failed to calculate setup size!"
return 1
fi
payload=$((payload + bytes))
done
# Reserve generous filesystem and directory overhead, then round up to a
# power-of-two image size with a 64 MiB minimum.
size="$minimum"
required=$((payload + ((payload + 3) / 4) + (32 * mib)))
while ((size < required)); do
size=$((size * 2))
done
printf '%s\n' "$size"
}
checkPlatform() {
local xml="$1"
local platform
IMAGE_PLATFORM=""
platform=$(getPlatform "$xml") || return 1
case "${platform,,}" in
"mixed" )
error "Windows images with mixed architectures are not supported!"
return 1 ;;
"x86" | "x64" | "arm64" ) ;;
* )
error "Unsupported Windows image architecture: ${platform:-unknown}"
return 1 ;;
esac
case "${PLATFORM,,}" in
"x64" )
if [[ "${platform,,}" != "x64" && "${platform,,}" != "x86" ]]; then
error "You cannot boot ${platform^^} images on a $PLATFORM CPU!"
return 1
fi ;;
"arm64" )
if [[ "${platform,,}" != "arm64" ]]; then
error "You cannot boot ${platform^^} images on a $PLATFORM CPU!"
return 1
fi ;;
* )
error "Unsupported container platform: $PLATFORM"
return 1 ;;
esac
IMAGE_PLATFORM="${platform,,}"
return 0
}
getPlatform() {
local xml="$1"
local arch current platform=""
local image_count output records
local separator=$'\x1f'
if ! output=$(xmlstarlet sel \
-T -t \
-v 'count(/WIM/IMAGE)' -n \
-m '/WIM/IMAGE' \
-v 'count(WINDOWS/ARCH)' -o "$separator" \
-v 'normalize-space(WINDOWS/ARCH)' -n \
- 2>/dev/null <<< "$xml"); then
error "Failed to read architecture metadata from WIM image!"
return 1
fi
image_count="${output%%$'\n'*}"
if [ "$image_count" = "0" ]; then
error "No images were found in WIM metadata!"
return 1
fi
records="${output#*$'\n'}"
while IFS="$separator" read -r count arch; do
if [ "$count" != "1" ]; then
error "Missing or duplicate architecture metadata in WIM image!"
return 1
fi
case "$arch" in
"0" ) current="x86" ;;
"9" ) current="x64" ;;
"12" ) current="arm64" ;;
* )
error "Unsupported architecture value in WIM metadata!"
return 1 ;;
esac
if [ -z "$platform" ]; then
platform="$current"
elif [ "$platform" != "$current" ]; then
platform="mixed"
fi
done <<< "$records"
echo "$platform"
return 0
}
createImageDirectory() {
local image="$1"
local directory="$2"
if mdir -i "$image" "$directory" >/dev/null 2>&1; then
return 0
fi
mmd -i "$image" "$directory" >/dev/null
}
createSetupImage() {
local stage="$1"
local image="$2"
local tmp="${image}.tmp"
local target="::/\$OEM\$/\$1/OEM"
local install="$stage/.overlay-install.bat"
local folder size sectors entry find_pid
local entries=()
folder=$(getOemFolder) || return 1
if ! size=$(getImageSize "$stage" "$folder"); then
return 1
fi
sectors=$((size / 512))
local msg="Writing overlay image..."
info "$msg" && html "$msg"
# Build and verify a temporary FAT32 image before publishing it as setup
# media, so a partial write never becomes boot media.
rm -f -- "$tmp" || return 1
checkFreeSpace "$(dirname "$image")" "$size" || return 1
if ! mformat -i "$tmp" -C -F -T "$sectors" -v "SETUP" ::; then
rm -f -- "$tmp"
error "Failed to format setup image!"
return 1
fi
mapfile -d '' entries < <(
find "$stage" -mindepth 1 -maxdepth 1 ! -name '.overlay-install.bat' -print0
)
# Process substitution hides the find status, so wait for it explicitly.
find_pid=$!
if ! wait "$find_pid"; then
rm -f -- "$tmp"
error "Failed to enumerate image files!"
return 1
fi
for entry in "${entries[@]}"; do
if ! mcopy -Q -s -i "$tmp" "$entry" ::; then
rm -f -- "$tmp"
error "Failed to copy image file: $entry"
return 1
fi
done
if [ -n "$folder" ] || [ -f "$install" ]; then
if ! createImageDirectory "$tmp" "::/\$OEM\$" || ! createImageDirectory "$tmp" "::/\$OEM\$/\$1" ||
! createImageDirectory "$tmp" "$target"; then
rm -f -- "$tmp"
error "Failed to create OEM directory in setup image!"
return 1
fi
fi
if [ -n "$folder" ]; then
mapfile -d '' entries < <(
find "$folder" -mindepth 1 -maxdepth 1 -print0
)
# Preserve errors from the second process-substitution find as well.
find_pid=$!
if ! wait "$find_pid"; then
rm -f -- "$tmp"
error "Failed to enumerate OEM files!"
return 1
fi
for entry in "${entries[@]}"; do
if ! mcopy -Q -s -o -i "$tmp" "$entry" "$target"; then
rm -f -- "$tmp"
error "Failed to copy OEM file: $entry"
return 1
fi
done
fi
# Copy the generated overlay script last so it replaces an install.bat from
# the mounted OEM folder when both are present.
if [ -f "$install" ]; then
if ! mcopy -Q -o -i "$tmp" "$install" "$target/install.bat"; then
rm -f -- "$tmp"
error "Failed to replace install.bat in setup image!"
return 1
fi
fi
# Verify that mtools can read the completed filesystem before publishing it.
if ! mdir -i "$tmp" :: >/dev/null; then
rm -f -- "$tmp"
error "Failed to verify image!"
return 1
fi
if ! enabled "$MANUAL"; then
local answer="$stage/Autounattend.xml"
if [ ! -f "$answer" ] || [ ! -s "$answer" ]; then
rm -f -- "$tmp"
error "Failed to find staged answer file: $answer"
return 1
fi
# Ensure the answer file survived the FAT32 copy byte-for-byte.
if ! mtype -i "$tmp" ::/Autounattend.xml | cmp -s - "$answer"; then
rm -f -- "$tmp"
error "Failed to verify staged answer file!"
return 1
fi
fi
if ! mv -f -- "$tmp" "$image"; then
rm -f -- "$tmp"
error "Failed to save setup image: $image"
return 1
fi
if ! setOwner "$image"; then
warn "Failed to set the owner for \"$image\" !"
fi
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!"
exit 67
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"
local path
# Direct UDF inspection is unavailable for valid ISO9660-only media, so
# fall back to extraction when the image can still be read as ISO9660.
if ! udfread stat --ignore-case "$iso" / >/dev/null 2>&1; then
if isoinfo -d -i "$iso" >/dev/null 2>&1; then
return 1
fi
enabled "$DEBUG" && echo "Neither UDF nor ISO9660 inspection could read the ISO image: $iso" >&2
error "Failed to read ISO image: $iso"
return 2
fi
if ! udfread stat --ignore-case "$iso" /sources >/dev/null 2>&1; then
enabled "$DEBUG" && echo "The UDF filesystem is readable, but the /sources directory is unavailable." >&2
return 1
fi
# Prefer install.wim when both payload forms are present.
for path in /sources/install.wim /sources/install.esd; do
if udfread stat --ignore-case "$iso" "$path" >/dev/null 2>&1; then
printf '%s' "$path"
return 0
fi
done
enabled "$DEBUG" && echo "No install.wim or install.esd payload was found in /sources." >&2
return 1
}
readWimHeader() {
local iso="$1"
local image="$2"
local size signature
local header="$TMP/wim-header.bin"
if ! rm -f -- "$header"; then
enabled "$DEBUG" && echo "Failed to remove the previous temporary WIM header: $header" >&2
error "Failed to prepare Windows image header!"
return 1
fi
# Read only the fixed WIM header so metadata can be located without
# extracting install.wim or install.esd from the ISO.
if ! udfread range --ignore-case -o "$header" "$iso" "$image" 0 208 >/dev/null 2>&1; then
enabled "$DEBUG" && echo "udfread failed to read the first 208 bytes of $image from $iso." >&2
error "Failed to read Windows image header!"
rm -f -- "$header"
return 1
fi
if ! size=$(stat -c%s -- "$header"); then
enabled "$DEBUG" && echo "Failed to determine the size of the temporary WIM header: $header" >&2
error "Failed to read Windows image header!"
rm -f -- "$header"
return 1
fi
if (( size != 208 )); then
enabled "$DEBUG" && echo "The WIM header is $size bytes instead of the expected 208 bytes." >&2
error "Failed to read Windows image header!"
rm -f -- "$header"
return 1
fi
if ! signature=$(od -An -N8 -tx1 "$header" | tr -d ' \n'); then
enabled "$DEBUG" && echo "Failed to read the WIM header signature from $header." >&2
error "Failed to read Windows image header!"
rm -f -- "$header"
return 1
fi
if [[ "$signature" != "4d5357494d000000" ]]; then
enabled "$DEBUG" && echo "The WIM header has an invalid signature: ${signature:-empty}." >&2
error "Failed to read Windows image header!"
rm -f -- "$header"
return 1
fi
echo "$header"
return 0
}
readIsoImageInfo() {
local iso="$1"
local image="$2"
local header="$3"
local raw result xml_count rc
local root header_size version
local part_number total_parts image_count
local xml_offset xml_size xml_original xml_flags
local -a bytes=() values=()
if [ ! -f "$header" ]; then
enabled "$DEBUG" && echo "The temporary WIM header does not exist: $header" >&2
return 1
fi
raw=$(od -An -v -N208 -tu1 -- "$header") || {
enabled "$DEBUG" && echo "Failed to read the 208-byte WIM header from $header." >&2
return 1
}
read -r -a bytes <<< "${raw//$'\n'/ }"
if (( ${#bytes[@]} != 208 )); then
enabled "$DEBUG" && echo "The WIM header decoded to ${#bytes[@]} bytes instead of 208." >&2
return 1
fi
# Validate the MSWIM\0\0\0 signature.
if ! (( bytes[0] == 77 &&
bytes[1] == 83 &&
bytes[2] == 87 &&
bytes[3] == 73 &&
bytes[4] == 77 &&
bytes[5] == 0 &&
bytes[6] == 0 &&
bytes[7] == 0 )); then
enabled "$DEBUG" && echo "The decoded WIM header does not contain the expected MSWIM signature." >&2
return 1
fi
# Header size at offset 0x08.
header_size=$(( \
bytes[8] |
bytes[9] << 8 |
bytes[10] << 16 |
bytes[11] << 24
))
if (( header_size != 208 )); then
enabled "$DEBUG" && echo "The WIM header declares an unexpected header size: $header_size bytes." >&2
return 1
fi
# WIM version at offset 0x0c.
version=$(( \
bytes[12] |
bytes[13] << 8 |
bytes[14] << 16 |
bytes[15] << 24
))
if ! (( version == 0x10d00 || version == 0x0e00 )); then
enabled "$DEBUG" && echo "The WIM header contains an unsupported version value: $version." >&2
return 1
fi
# Split-WIM information at offsets 0x28 and 0x2a.
part_number=$((bytes[40] | bytes[41] << 8))
total_parts=$((bytes[42] | bytes[43] << 8))
if ! (( part_number > 0 &&
total_parts > 0 &&
part_number <= total_parts )); then
enabled "$DEBUG" && echo "The WIM split-image fields are invalid: part $part_number of $total_parts." >&2
return 1
fi
# Image count at offset 0x2c.
image_count=$(( \
bytes[44] |
bytes[45] << 8 |
bytes[46] << 16 |
bytes[47] << 24
))
if ! (( image_count > 0 && image_count <= 65535 )); then
enabled "$DEBUG" && echo "The WIM header contains an invalid image count: $image_count." >&2
return 1
fi
result=$(parseWimHeader "$iso" "$image" "$header") || {
enabled "$DEBUG" && echo "Failed to parse the WIM XML resource descriptor." >&2
return 1
}
mapfile -t values <<< "$result"
if (( ${#values[@]} != 4 )); then
enabled "$DEBUG" && echo "The WIM XML resource descriptor returned ${#values[@]} values instead of 4." >&2
return 1
fi
xml_offset="${values[0]}"
xml_size="${values[1]}"
xml_original="${values[2]}"
xml_flags="${values[3]}"
if ! [[ "$xml_offset" =~ ^[0-9]+$ &&
"$xml_size" =~ ^[0-9]+$ &&
"$xml_original" =~ ^[0-9]+$ &&
"$xml_flags" =~ ^[0-9]+$ ]]; then
enabled "$DEBUG" && echo "The WIM XML resource descriptor contains non-numeric values: offset=$xml_offset size=$xml_size original=$xml_original flags=$xml_flags." >&2
return 1
fi
if ! (( xml_size > 0 &&
xml_original > 0 &&
xml_size == xml_original &&
xml_size % 2 == 0 )); then
enabled "$DEBUG" && echo "The WIM XML resource sizes are invalid: size=$xml_size original=$xml_original." >&2
return 1
fi
# These resource forms cannot be decoded as a direct UTF-16LE byte range:
#
# 0x04: compressed
# 0x08: spanned
# 0x10: solid
#
# The metadata flag 0x02 is expected and deliberately allowed.
if (( xml_flags & 0x1c )); then
enabled "$DEBUG" && echo "The WIM XML resource uses unsupported flags: $xml_flags." >&2
return 1
fi
result=$(udfread range --ignore-case "$iso" "$image" "$xml_offset" "$xml_size" \
2>/dev/null | iconv -f UTF-16LE -t UTF-8 2>/dev/null
) || {
rc=$?
enabled "$DEBUG" && echo "Failed to read or decode the WIM XML metadata range at offset $xml_offset with size $xml_size (status $rc)." >&2
return "$rc"
}
if [ -z "$result" ]; then
enabled "$DEBUG" && echo "The WIM XML metadata range was empty." >&2
return 1
fi
local metadata separator=$'\x1f'
metadata=$(xmlstarlet sel \
-T -t \
-v 'local-name(/*)' -o "$separator" \
-v 'count(/*[local-name()="WIM"]/*[local-name()="IMAGE"])' \
- 2>/dev/null <<< "$result") || {
enabled "$DEBUG" && echo "Failed to parse the WIM XML metadata document." >&2
return 1
}
IFS="$separator" read -r root xml_count <<< "$metadata"
if [ "$root" != "WIM" ]; then
enabled "$DEBUG" && echo "The WIM XML metadata has an unexpected root element: ${root:-empty}." >&2
return 1
fi
if [[ ! "$xml_count" =~ ^[0-9]+$ ]]; then
enabled "$DEBUG" && echo "The WIM XML metadata returned an invalid image count: ${xml_count:-empty}." >&2
return 1
fi
if (( xml_count != image_count )); then
enabled "$DEBUG" && echo "The WIM header image count ($image_count) does not match the XML metadata image count ($xml_count)." >&2
return 1
fi
printf '%s' "$result"
return 0
}
parseWimHeader() {
local iso="$1"
local image="$2"
local header="$3"
local -a bytes=()
local header_size=0
local parsed_size=0
local parsed_flags=0
local parsed_offset=0
local parsed_original=0
local details image_size raw
if [ ! -f "$header" ] || [ ! -s "$header" ]; then
enabled "$DEBUG" && echo "The WIM header file is missing or empty: $header" >&2
return 1
fi
if ! raw=$(od -An -v -j8 -N88 -tu1 -- "$header"); then
enabled "$DEBUG" && echo "Failed to read the WIM header fields from offset 0x08." >&2
return 1
fi
read -r -a bytes <<< "${raw//$'\n'/ }"
if (( ${#bytes[@]} != 88 )); then
enabled "$DEBUG" && echo "The WIM resource-header section decoded to ${#bytes[@]} bytes instead of 88." >&2
return 1
fi
local i
# The WIM header size is a 32-bit little-endian value at offset 0x08.
for ((i=3; i>=0; i--)); do
header_size=$((header_size * 256 + bytes[i]))
done
if (( header_size != 208 )); then
enabled "$DEBUG" && echo "The WIM resource parser found an unexpected header size: $header_size bytes." >&2
return 1
fi
# The XML resource descriptor starts at offset 0x48. Its first seven bytes
# contain the stored size and its eighth byte contains the resource flags.
for ((i=70; i>=64; i--)); do
parsed_size=$((parsed_size * 256 + bytes[i]))
done
parsed_flags="${bytes[71]}"
# The XML resource offset is an unsigned 64-bit little-endian value at 0x50.
if (( bytes[79] >= 128 )); then
enabled "$DEBUG" && echo "The WIM XML resource offset cannot be represented safely as a signed shell integer." >&2
return 1
fi
for ((i=79; i>=72; i--)); do
parsed_offset=$((parsed_offset * 256 + bytes[i]))
done
# The uncompressed XML size is an unsigned 64-bit value at offset 0x58.
if (( bytes[87] >= 128 )); then
enabled "$DEBUG" && echo "The WIM XML resource size cannot be represented safely as a signed shell integer." >&2
return 1
fi
for ((i=87; i>=80; i--)); do
parsed_original=$((parsed_original * 256 + bytes[i]))
done
if (( parsed_size <= 0 || parsed_offset < header_size || parsed_original <= 0 )); then
enabled "$DEBUG" && echo "The WIM XML resource descriptor is invalid: offset=$parsed_offset size=$parsed_size original=$parsed_original header=$header_size." >&2
return 1
fi
if ! details=$(udfread stat --ignore-case "$iso" "$image" 2>/dev/null); then
enabled "$DEBUG" && echo "udfread failed to determine the size of $image in $iso." >&2
return 1
fi
image_size=$(sed -n 's/^Size: \([0-9][0-9]*\) bytes$/\1/p' <<< "$details")
if [[ ! "$image_size" =~ ^[0-9]+$ ]]; then
enabled "$DEBUG" && echo "udfread returned no usable size for $image." >&2
return 1
fi
if (( parsed_offset > image_size || parsed_size > image_size - parsed_offset )); then
enabled "$DEBUG" && echo "The WIM XML resource lies outside the image bounds: offset=$parsed_offset size=$parsed_size image=$image_size." >&2
return 1
fi
printf '%s\n' "$parsed_offset" "$parsed_size" "$parsed_original" "$parsed_flags"
return 0
}
findImage() {
local dir="$1"
local name sources result
sources=$(find "$dir" -maxdepth 1 -type d -iname sources -print -quit) || return 2
if [ ! -d "$sources" ]; then
warn "failed to locate 'sources' folder in ISO image, $FB"
return 1
fi
for name in install.wim install.esd; do
result=$(find "$sources" -maxdepth 1 -type f -iname "$name" -print -quit) || return 2
[ -n "$result" ] && break
done
if [ ! -f "$result" ]; then
warn "failed to locate 'install.wim' or 'install.esd' in ISO image, $FB"
return 1
fi
echo "$result"
return 0
}
readImageInfo() {
local wim="$1"
local result=""
local msg="Failed to read Windows image information!"
if ! result=$(wimlib-imagex info --xml "$wim" | iconv -f UTF-16LE -t UTF-8); then
error "$msg"
return 2
fi
if [ -z "$result" ]; then
error "$msg"
return 2
fi
printf '%s' "$result"
return 0
}
validateEdition() {
[ -n "$EDITION" ] || return 0
[[ "${DETECTED,,}" == "win20"* ]] || return 0
local edition
edition=$(normalizeServerEditionID "$EDITION") || return 1
[ -n "$edition" ] || return 0
if [[ "${DETECTED,,}" == *"-${edition,,}" || "${DETECTED,,}" == *"-${edition,,}-eval" ]]; then
return 0
fi
# Discard a stale server-edition override when it conflicts with the image
# that was actually detected.
EDITION=""
return 0
}
unknownImage() {
local rc=0
local msg="Failed to determine Windows version from image"
setXML "" || rc=$?
if (( rc == 0 )) || enabled "$MANUAL"; then
info "${msg}!"
return 0
fi
# Only absence of a usable answer file is a supported manual fallback.
(( rc == 1 )) || return "$rc"
MANUAL="Y"
warn "${msg}, $FB."
return 0
}
describeImage() {
local result
result=$(printEdition "$DETECTED" "$DETECTED" "Y") || return 1
if [[ "${LANGUAGE,,}" != "en" && "${LANGUAGE,,}" != "en-"* ]]; then
local language
language=$(getLanguage "$LANGUAGE" "desc") || return 1
result+=" ($language)"
fi
printf '%s' "$result"
return 0
}
configureImage() {
local index="$1"
local desc="$2"
local rc=0
# Prefer the exact answer file, then a family-level fallback. Manual mode is
# the final supported path only when no usable template exists.
setXML "" "$index" || rc=$?
if (( rc == 0 )); then
return 0
fi
enabled "$MANUAL" && return 0
# Generation or preparation errors must not be hidden by another fallback.
(( rc == 1 )) || return "$rc"
if [[ "$DETECTED" == "win81x86"* || "$DETECTED" == "win10x86"* ]]; then
error "The 32-bit version of $desc is not supported!"
exit 67
fi
local msg="the answer file for $desc was not found ($DETECTED.xml)"
local fallback="/run/assets/${DETECTED%%-*}.xml"
rc=0
setXML "$fallback" "$index" || rc=$?
if (( rc == 0 )); then
warn "${msg}."
return 0
fi
enabled "$MANUAL" && return 0
# The family-level template was present but could not be generated/prepared.
(( rc == 1 )) || return "$rc"
MANUAL="Y"
warn "${msg}, $FB."
return 0
}
detectImageInfo() {
local image_info="$1"
local desc index rc
checkPlatform "$image_info" || {
enabled "$DEBUG" && echo "Platform validation failed for the Windows image metadata." >&2
exit 67
}
local output
output=$(detectVersion "$image_info") || {
enabled "$DEBUG" && echo "Version detection failed while parsing the Windows image metadata." >&2
error "Failed to detect Windows version from image metadata!"
return 1
}
local -a detected=()
mapfile -t detected <<< "$output"
DETECTED="${detected[0]:-}"
index="${detected[1]:-}"
validateEdition || {
enabled "$DEBUG" && echo "Edition validation failed for detected image: ${DETECTED:-empty}, index: ${index:-empty}." >&2
error "Failed to validate Windows edition from image metadata!"
return 1
}
if [ -z "$DETECTED" ]; then
unknownImage || {
rc=$?
enabled "$DEBUG" && echo "Unknown-image handling failed after no Windows version could be detected (status $rc)." >&2
return "$rc"
}
return 0
fi
detectLanguage "$image_info" "$index" || {
rc=$?
enabled "$DEBUG" && echo "Failed to detect the language for image $DETECTED at index ${index:-empty} (status $rc)." >&2
return "$rc"
}
desc=$(describeImage) || {
rc=$?
enabled "$DEBUG" && echo "Failed to describe the detected Windows image $DETECTED (status $rc)." >&2
return "$rc"
}
info "Detected: $desc"
configureImage "$index" "$desc" || {
rc=$?
enabled "$DEBUG" && echo "Failed to configure the detected Windows image $DETECTED at index ${index:-empty} (status $rc)." >&2
return "$rc"
}
return 0
}
detectIsoImage() {
local iso="$1"
local image header image_info rc
# Return 1 only when no directly inspectable WIM/ESD payload is available so
# the caller may extract the media. Metadata parsing/configuration errors use 2.
image=$(findIsoImage "$iso") || {
rc=$?
enabled "$DEBUG" && echo "Direct ISO image lookup stopped with status $rc." >&2
return "$rc"
}
header=$(readWimHeader "$iso" "$image") || {
enabled "$DEBUG" && echo "Reading the WIM header failed for $image." >&2
return 2
}
image_info=$(readIsoImageInfo "$iso" "$image" "$header") || {
enabled "$DEBUG" && echo "Reading the WIM XML metadata failed for $image." >&2
error "Failed to read Windows image metadata!"
return 2
}
info "Detecting version from ISO image..."
detectImageInfo "$image_info" || {
enabled "$DEBUG" && echo "Processing the Windows image metadata failed." >&2
return 2
}
return 0
}
baseDir() {
local path="${1%/}"
[[ -z "$path" || "$path" == "/" ]] && {
echo "/"
return 0
}
path="${path#/}"
path="${path%%/*}"
echo "/$path"
return 0
}
checkFreeSpace() {
local dir="$1"
local size="$2"
local base space size_gb space_gb
base=$(baseDir "$dir")
if ! space=$(df --output=avail -B 1 "$dir" | tail -n 1); then
error "Failed to check free space in $dir."
return 1
fi
if [[ ! "$space" =~ ^[[:space:]]*[0-9]+[[:space:]]*$ ]]; then
error "Failed to determine available disk space for $dir."
return 1
fi
space="${space//[[:space:]]/}"
if (( size > space )); then
size_gb=$(formatBytes "$size")
space_gb=$(formatBytes "$space")
error "Not enough free space in $base, have $space_gb available but need at least $size_gb."
return 1
fi
return 0
}
extractESD() {
local iso="$1"
local dir="$2"
local version="$3"
local desc="$4"
local installSize size edition imgEdition
local bootTotal bootLinks wimTotal wimLinks
local bootWim installWim bootSize wimSize
local image index line ret metadata count
local resultCount resultIndex resultEdition
local xml installXml output result
local -a detected fields
local minSize=100000000
local bootPad=60000000
local installPad=3000000
local spacePad=1073741824
local msg="Extracting bootdisk from ESD file"
info "$msg..." && html "$msg..."
if ! size=$(stat -c%s -- "$iso"); then
error "Failed to determine size of ISO file \"$iso\" !"
return 1
fi
if (( size < minSize )); then
error "The downloaded ESD file is too small!"
return 1
fi
if ! rm -rf -- "$dir"; then
error "Failed to remove directory \"$dir\" !"
return 1
fi
if ! makeDir "$dir"; then
error "Failed to create directory \"$dir\" !"
return 1
fi
xml=$(wimlib-imagex info "$iso" --xml 2>/dev/null |
iconv -f UTF-16LE -t UTF-8 2>/dev/null) || {
ret=$?
error "Cannot read ESD file information!"
return "$ret"
}
# Microsoft download ESDs use images 1-3 for setup media, WinPE, and Windows
# Setup; images 4 and higher contain installable editions. Read all metadata
# once because repeatedly inspecting a solid-compressed ESD is expensive.
if ! metadata=$(xmlstarlet sel -t \
-v 'count(/WIM/IMAGE)' -n \
-v 'normalize-space(/WIM/IMAGE[@INDEX="1"]/TOTALBYTES)' -n \
-v 'normalize-space(/WIM/IMAGE[@INDEX="1"]/HARDLINKBYTES)' -n \
-v 'normalize-space(/WIM/IMAGE[@INDEX="3"]/TOTALBYTES)' -n \
-v 'normalize-space(/WIM/IMAGE[@INDEX="3"]/HARDLINKBYTES)' -n \
-m '/WIM/IMAGE[number(@INDEX) >= 4]' -v '@INDEX' -o $'\t' -v 'DESCRIPTION' -n \
<<< "$xml" 2>/dev/null); then
error "Cannot read ESD file information!"
return 1
fi
mapfile -t fields <<< "$metadata"
count="${fields[0]:-}"
if [[ ! "$count" =~ ^[0-9]+$ ]]; then
error "Cannot read the image count in ESD file!"
return 1
fi
if (( count < 4 )); then
error "Invalid ESD file: expected at least 4 images, found $count."
return 1
fi
bootTotal="${fields[1]:-}"
bootLinks="${fields[2]:-}"
if [[ ! "$bootTotal" =~ ^[0-9]+$ ]] || [[ ! "$bootLinks" =~ ^[0-9]+$ ]]; then
error "Cannot read bootdisk size from ESD file!"
return 1
fi
bootSize=$(( bootTotal - bootLinks ))
wimTotal="${fields[3]:-}"
wimLinks="${fields[4]:-}"
if [[ ! "$wimTotal" =~ ^[0-9]+$ ]] ||
[[ ! "$wimLinks" =~ ^[0-9]+$ ]]; then
error "Cannot read boot.wim size from ESD file!"
return 1
fi
wimSize=$(( wimTotal - wimLinks + bootPad ))
# The downloaded ESD already occupies disk space and is moved into the
# installation media. Peak additional usage consists of the extracted setup
# files and boot.wim, plus the final ISO containing those files and the ESD.
local freeSpace=$(( size + 2 * (bootSize + wimSize) + spacePad ))
checkFreeSpace "$dir" "$freeSpace" || return
/run/progress.sh "$dir" "$bootSize" "$msg ([P])..." &
index="1"
wimlib-imagex apply "$iso" "$index" "$dir" --quiet 2>/dev/null || {
ret=$?
fKill "progress.sh"
error "Extracting $desc bootdisk failed ($ret)"
return "$ret"
}
fKill "progress.sh"
bootWim="$dir/sources/boot.wim"
installWim="$dir/sources/install.esd"
msg="Extracting environment from ESD file"
info "$msg..." && html "$msg..."
index="2"
/run/progress.sh "$bootWim" "$wimSize" "$msg ([P])..." &
wimlib-imagex export "$iso" "$index" "$bootWim" \
--compress=none --quiet || {
ret=$?
fKill "progress.sh"
error "Adding WinPE failed ($ret)"
return "$ret"
}
fKill "progress.sh"
msg="Extracting setup from ESD file"
info "$msg..."
index="3"
/run/progress.sh "$bootWim" "$wimSize" "$msg ([P])..." &
wimlib-imagex export "$iso" "$index" "$bootWim" \
--compress=none --boot --quiet || {
ret=$?
fKill "progress.sh"
error "Adding Windows Setup failed ($ret)"
return "$ret"
}
fKill "progress.sh"
html "$msg..."
if [[ "${PLATFORM,,}" == "x64" ]]; then
LABEL="CCCOMA_X64FRE_EN-US_DV9"
else
LABEL="CPBA_A64FRE_EN-US_DV9"
fi
index=""
if [[ "${version,,}" == "http"* ]]; then
# Direct ESD URLs have no catalog identity. Restrict automatic detection
# to installable images because indexes 1-3 contain setup components.
if ! installXml=$(xmlstarlet ed \
-d '/WIM/IMAGE[number(@INDEX) < 4]' \
<<< "$xml" 2>/dev/null); then
error "Cannot read installable images from ESD file!"
return 1
fi
checkPlatform "$xml" || return
output=$(detectVersion "$installXml") || return
mapfile -t detected <<< "$output"
index="${detected[1]:-}"
if [ -z "$index" ]; then
error "Failed to select an installation image from install.esd!"
return 1
fi
else
edition=$(getCatalog "$version" "name")
if [ -z "$edition" ]; then
error "Invalid VERSION specified, value \"$version\" is not recognized!"
return 1
fi
for line in "${fields[@]:5}"; do
IFS=$'\t' read -r image imgEdition <<< "$line"
[[ ! "$image" =~ ^[0-9]+$ ]] && continue
[[ "${imgEdition,,}" != "${edition,,}" ]] && continue
index="$image"
break
done
if [ -z "$index" ]; then
error "Failed to find product '$edition' in install.esd!"
return 1
fi
fi
installSize=$(( size + installPad ))
if ! rm -f -- "$dir/sources/install.wim" "$installWim"; then
error "Failed to remove previous Windows installation image!"
return 1
fi
# Reuse the downloaded solid ESD instead of exporting the selected image.
# Both paths are below $TMP, so this is a same-filesystem rename.
if ! mv -f -- "$iso" "$installWim"; then
error "Failed to move downloaded ESD file into the installation media!"
return 1
fi
# Remove all other images without rebuilding the solid-compressed resources.
# Deleting in descending order leaves the selected edition at index 1.
for (( image=count; image >= 1; image-- )); do
(( image == index )) && continue
wimlib-imagex delete "$installWim" "$image" --soft --quiet || {
ret=$?
error "Failed to remove image $image from install.esd!"
return "$ret"
}
done
result=$(wimlib-imagex info "$installWim" --xml 2>/dev/null |
iconv -f UTF-16LE -t UTF-8 2>/dev/null) || {
ret=$?
error "Cannot verify the prepared install.esd file!"
return "$ret"
}
if ! metadata=$(xmlstarlet sel -t \
-v 'count(/WIM/IMAGE)' -n \
-v 'count(/WIM/IMAGE[@INDEX="1"])' -n \
-v 'normalize-space(/WIM/IMAGE[@INDEX="1"]/DESCRIPTION)' -n \
<<< "$result" 2>/dev/null); then
error "Cannot verify the prepared install.esd file!"
return 1
fi
mapfile -t fields <<< "$metadata"
resultCount="${fields[0]:-}"
resultIndex="${fields[1]:-}"
resultEdition="${fields[2]:-}"
if [[ "$resultCount" != "1" ]] || [[ "$resultIndex" != "1" ]]; then
error "Prepared install.esd does not contain exactly one image at index 1!"
return 1
fi
if [[ "${version,,}" != "http"* ]] &&
[[ "${resultEdition,,}" != "${edition,,}" ]]; then
error "Prepared install.esd does not contain only '$edition' at index 1!"
return 1
fi
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" <<EOC
[general]
min_severity = warning
show_summary = false
[rules]
enabled_rules = $enabled_rules
EOC
output=$(
cd "$tmp"
python3 -m blinter "$file" 2>&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" \
'(?<!\\)\\host[.]lan[\\]' \
"invalid single-backslash UNC path detected" \
'Use "\\host.lan\Data\..." instead of "\host.lan\Data\...".'
reportBatchMatches \
"$file" \
"$source" \
'(?<![\\[:alnum:]._-])host[.]lan[\\]' \
"UNC path without leading backslashes detected" \
'Use "\\host.lan\Data\..." instead of "host.lan\Data\...".'
reportBatchMatches \
"$file" \
"$source" \
'//host[.]lan/' \
"invalid forward-slash UNC path detected" \
'Use "\\host.lan\Data\..." instead of "//host.lan/Data/...".'
reportBatchMatches \
"$file" \
"$source" \
'\\\\host[.]lan\\shared(?:[\\/]|$)' \
"invalid Samba share name detected" \
'The "/shared" folder is exposed to Windows as "\\host.lan\Data".'
return 0
}
getBootLoadSize() {
local iso="$1"
local dir="$2"
local desc="$3"
local boot_info size value
case "${DETECTED,,}" in
"win2k"* | "winxp"* | "win2003"* )
# NT 5.x media may not expose a reliable catalog sector count, so derive
# it directly from the extracted boot image.
if [ ! -s "$dir/$ETFS" ]; then
error "Failed to locate file \"$ETFS\" in $desc ISO image!"
return 1
fi
if ! size=$(stat -c%s "$dir/$ETFS"); then
error "Failed to determine boot image size from $desc ISO!"
return 1
fi
if (( size < 512 || size % 512 != 0 )); then
error "Invalid boot image size found in $desc ISO!"
return 1
fi
BOOT_LOAD_SIZE=$((size / 512)) ;;
* )
# Other legacy media use the El Torito Nsect value from the ISO catalog.
if ! boot_info=$(isoinfo -d -i "$iso"); then
error "Failed to read boot image information from $desc ISO!"
return 1
fi
value=$(awk '/Nsect / { print $NF; exit }' <<< "$boot_info")
if [ -z "$value" ]; then
error "Failed to determine boot image load size from $desc ISO!"
return 1
fi
if [[ ! "$value" =~ ^[[:xdigit:]]{1,4}$ ]]; then
error "Invalid boot image load size found in $desc ISO!"
return 1
fi
BOOT_LOAD_SIZE=$((16#$value)) ;;
esac
if (( BOOT_LOAD_SIZE < 1 || BOOT_LOAD_SIZE > 65535 )); then
error "Invalid boot image load size found in $desc ISO!"
return 1
fi
return 0
}
extractBootImage() {
local iso="$1"
local dir="$2"
local desc="$3"
local actual expected offset info
ETFS="boot.img"
[ -s "$dir/$ETFS" ] && return 0
rm -f "$dir/$ETFS" || return 1
if ! info=$(isoinfo -d -i "$iso"); then
error "Failed to read boot image information from $desc ISO!"
return 1
fi
offset=$(awk '/Bootoff / { print $NF; exit }' <<< "$info")
if [ -z "$offset" ]; then
error "Failed to determine boot image offset from $desc ISO!"
return 1
fi
if [[ ! "$offset" =~ ^[0-9]+$ ]]; then
error "Invalid boot image location found in $desc ISO!"
return 1
fi
# isoinfo reports the boot offset in 2048-byte sectors, while dd below uses
# 512-byte blocks, hence the factor of four.
if ! dd "if=$iso" "of=$dir/$ETFS" bs=512 "count=$BOOT_LOAD_SIZE" "skip=$((offset * 4))" status=none; then
rm -f "$dir/$ETFS" || true
error "Failed to extract boot image from $desc ISO!"
return 1
fi
expected=$((BOOT_LOAD_SIZE * 512))
if ! actual=$(stat -c%s "$dir/$ETFS"); then
rm -f "$dir/$ETFS" || true
error "Failed to determine boot image size from $desc ISO!"
return 1
fi
if (( actual != expected )); then
rm -f "$dir/$ETFS" || true
error "Failed to extract complete boot image from $desc ISO!"
return 1
fi
return 0
}
buildImage() {
local dir="$1"
local cat="BOOT.CAT"
local log="/run/shm/iso.log"
local base size desc rc=0
if [ -f "$BOOT" ]; then
error "File $BOOT does already exist?!" && return 1
fi
base=$(basename "$BOOT")
local out="$TMP/${base%.*}.tmp"
rm -f "$out"
desc=$(printVariant "$DETECTED" "ISO")
local msg="Building $desc image"
[[ "${ISO,,}" == *.esd ]] && msg+=" from ESD file"
info "$msg..." && html "$msg..."
[ -z "$LABEL" ] && LABEL="Windows"
if [ ! -f "$dir/$ETFS" ] || [ ! -s "$dir/$ETFS" ]; then
error "Failed to locate file \"$ETFS\" in ISO image!" && return 1
fi
if ! size=$(du -b --max-depth=0 "$dir" | cut -f1); then
error "Failed to calculate the size of directory \"$dir\"!"
return 1
fi
checkFreeSpace "$TMP" "$size" || return 1
if [[ "${BOOT_MODE,,}" == "windows_legacy" ]] && [ -z "${BOOT_LOAD_SIZE:-}" ]; then
if [[ "${DETECTED,,}" != "win9"* && "${DETECTED,,}" != "winnt4" ]]; then
error "Failed to determine the boot image load size!"
return 1
fi
fi
/run/progress.sh "$out" "$size" "$msg ([P])..." &
local -a args=(
-o "$out"
-b "$ETFS"
)
local -a name_args=(
-J
-l
-D
-N
-joliet-long
-relaxed-filenames
-V "${LABEL::30}"
)
# Use separate layouts for modern hybrid media, NT 5.x legacy media, Win9x,
# and other legacy releases because their El Torito requirements differ.
if [[ "${BOOT_MODE,,}" != "windows_legacy" ]]; then
args+=(
-no-emul-boot
-c "$cat"
-iso-level 4
"${name_args[@]}"
-udf
-boot-info-table
-eltorito-alt-boot
-eltorito-boot "$EFISYS"
-no-emul-boot
-allow-limited-size
)
else
case "${DETECTED,,}" in
"win2k"* | "winxp"* | "win2003"* )
args+=(
-no-emul-boot
-boot-load-seg 1984
-boot-load-size "$BOOT_LOAD_SIZE"
-c "$cat"
-iso-level 2
"${name_args[@]}"
) ;;
"win9"* | "winnt4" )
args+=(
-J
-r
-V "${LABEL::30}"
) ;;
* )
args+=(
-no-emul-boot
-boot-load-size "$BOOT_LOAD_SIZE"
-c "$cat"
-iso-level 2
"${name_args[@]}"
-udf
-allow-limited-size
) ;;
esac
fi
genisoimage "${args[@]}" -quiet "$dir" 2> "$log" || rc=$?
fKill "progress.sh"
if (( rc != 0 )); then
[ -s "$log" ] && echo "$(<"$log")"
error "Failed to build image!"
return "$rc"
fi
local err=""
local hide="Warning: creating filesystem that does not conform to ISO-9660."
[ -s "$log" ] && err="$(<"$log")"
# UDF hybrid media intentionally triggers this genisoimage warning.
if [ -n "$err" ] && [[ "$err" != "$hide" ]]; then
echo "$err"
fi
mv -f "$out" "$BOOT" || return
if ! setOwner "$BOOT"; then
warn "Failed to set the owner for \"$BOOT\" !"
fi
return 0
}
return 0