#!/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 } selectVersion() { local wanted="$1" shift local -a candidates=("$wanted") local candidate record id index # 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 -a images=() bases=() order=() preferences=() local normalize output policy record id index base edition suffix 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 policy=$(getEditionPolicy "${bases[0]}") || return 1 mapfile -t preferences <<< "$policy" normalize="${preferences[0]}" order=("${preferences[@]:1}") # 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 if ! lang=$(xmlstarlet sel -T -t -v "normalize-space(string(($path)[1]))" - 2>/dev/null <<< "$xml"); then warn "failed to read language metadata from Windows image!" return 0 fi [ -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" } 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" 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 } 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 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 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 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 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 rm -f -- "$header" return 1 fi if [[ "$signature" != "4d5357494d000000" ]]; then enabled "$DEBUG" && echo "The WIM header has an invalid signature: ${signature:-empty}." >&2 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!" return 2 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" || return 2 local output output=$(detectVersion "$image_info") || { enabled "$DEBUG" && echo "Version detection failed while parsing the Windows image metadata." >&2 error "Failed to detect the 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 the 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 "ISO image lookup failed (status $rc)." >&2 return "$rc" } header=$(readWimHeader "$iso" "$image") || { error "Failed to read the Windows image header!" return 2 } image_info=$(readIsoImageInfo "$iso" "$image" "$header") || { error "Failed to read the Windows image metadata!" return 2 } info "Detecting version from ISO image..." detectImageInfo "$image_info" || { error "Failed to process the Windows image metadata!" return 2 } return 0 } detectESDImage() { local iso="$1" local image_info install_info output index rc local -a detected=() image_info=$(wimlib-imagex info "$iso" --xml 2>/dev/null | iconv -f UTF-16LE -t UTF-8 2>/dev/null) || { rc=$? error "Cannot read ESD file information (status $rc)." return 2 } # Microsoft download ESDs use images 1-3 for setup media, WinPE, and Windows # Setup; images 4 and higher contain installable editions. if ! install_info=$(xmlstarlet ed \ -d '/WIM/IMAGE[number(@INDEX) < 4]' \ <<< "$image_info" 2>/dev/null); then error "Cannot read installable images from ESD file!" return 2 fi checkPlatform "$install_info" || return 2 local output output=$(detectVersion "$install_info") || { error "Failed to detect Windows version from the ESD metadata!" return 2 } mapfile -t detected <<< "$output" index="${detected[1]:-}" if [ -z "$index" ]; then error "Failed to select an installation image based on the ESD metadata!" return 2 fi # extractESD removes every other image, leaving the selected edition at # index 1. Detect against that final layout so the generated answer file # already references the index that will exist after extraction. if ! image_info=$(xmlstarlet ed \ -d "/WIM/IMAGE[number(@INDEX) != $index]" \ -u "/WIM/IMAGE[@INDEX='$index']/@INDEX" -v '1' \ <<< "$install_info" 2>/dev/null); then error "Cannot prepare ESD image information!" return 2 fi info "Detecting version from ESD image..." detectImageInfo "$image_info" || { error "Failed to process the ESD image metadata!" return 2 } 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 "$installXml" || 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 } 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" if ! rm -f "$out"; then error "Failed to remove temporary ISO image: $out" return 1 fi 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