#!/usr/bin/env bash set -Eeuo pipefail getVersions() { local xml="$1" local versions_name="$2" local bases_name="$3" local groups_name="$4" local indexes_name="$5" local -n bases_ref="$bases_name" local -n groups_ref="$groups_name" local -n indexes_ref="$indexes_name" local -n versions_ref="$versions_name" local platform image_count records record_count=0 local image_index display product image edition_id local install_type flags candidate candidate_id local candidate_base evaluation key name structured local separator=$'\x1f' bases_ref=() groups_ref=() indexes_ref=() versions_ref=() platform=$(getPlatform "$xml") || return 1 image_count=$(xmlstarlet sel -T -t -v 'count(/WIM/IMAGE)' - 2>/dev/null <<< "$xml") || return 1 if [[ ! "$image_count" =~ ^[0-9]+$ ]]; then error "Invalid image count in WIM metadata: '$image_count'" return 1 fi (( image_count > 0 )) || return 0 # Keep one compact record per image. XML 1.0 cannot contain U+001F, so it # can safely separate fields while all edition logic remains in Bash. 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 ((record_count += 1)) [ -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="" # NAME normally contains the most precise edition identifier (including # Server Core), while DISPLAYNAME is the best fallback for other images. for candidate in "$image" "$display" "$product"; do [[ "$candidate" == *"Operating System"* ]] && continue [ -n "$candidate" ] || continue candidate_base=$(fromName "$candidate" "$platform") candidate_id=$(getVersion "$candidate" "$platform") [ -n "$candidate_base" ] && [ -n "$candidate_id" ] && break done # Preserve NAME precedence, but allow a recognized DISPLAYNAME edition to # refine an otherwise generic family name. 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 [ "$(fromName "$display" "$platform")" = "$candidate_base" ] && [ -n "$structured" ] && [[ "${structured%-eval}" != "$candidate_base" ]] && [[ "$(getVersionPriority "$structured" "$candidate_base")" != "other" ]]; 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,,}" # Some client media use the same friendly name-derived ID for distinct # editions. Preserve the established unsuffixed Pro ID, and use the # structured edition metadata only to disambiguate a collision. if [[ -v "indexes_ref[$key]" ]]; then structured="" case "${candidate_base,,}" in "winvista"* | "win7"* | "win8"* | "win10"* | "win11"* ) structured=$(normalizeEditionID "${edition_id:-${flags:-}}" "$candidate_base") || return 1 ;; "win20"* ) structured=$(normalizeServerEditionID "${flags:-$edition_id}") || return 1 # Some media use the same EDITIONID for Core and Desktop images. # INSTALLATIONTYPE provides the structural distinction without # requiring a hardcoded marketing name. if [[ "${install_type,,}" == *"core"* && "$structured" != *"-core" ]]; then structured+="-core" fi ;; esac if [ -n "$structured" ]; then candidate_id="$candidate_base-$structured$evaluation" key="${candidate_id,,}" fi fi if [[ -v "indexes_ref[$key]" ]]; then warn "Duplicate image identity '$candidate_id' at indexes ${indexes_ref[$key]} and $image_index" continue fi indexes_ref["$key"]="$image_index" versions_ref+=( "$candidate_id" ) bases_ref+=( "$candidate_base" ) groups_ref+=( "$(getVersionPriority "$candidate_id" "$candidate_base")" ) done <<< "$records" if (( record_count != image_count )); then error "Expected $image_count image records in WIM metadata, found $record_count!" return 1 fi return 0 } getVersionPriority() { local id="${1,,}" local base="${2,,}" local entry priority patterns pattern local result="other" score best_score=-1 local -a order=() pattern_list=() id="${id%-eval}" mapfile -t order < <(getEditionOrder "$base") local edition="${id#"$base"}" edition="${edition#-}" # Use the most specific matching pattern. This prevents broad patterns # such as enterprise-* from taking precedence over enterprise-iot-*. for entry in "${order[@]}"; do IFS='|' read -r _ priority patterns <<< "$entry" read -r -a pattern_list <<< "$patterns" for pattern in "${pattern_list[@]}"; do if [ "$pattern" = "@default" ]; then [ -z "$edition" ] || continue score=1 elif [[ "$pattern" == *"*" ]]; then local prefix="${pattern%\*}" [[ "$edition" == "$prefix"* ]] || continue score="${#pattern}" elif [ "$edition" = "$pattern" ]; then score="${#pattern}" else continue fi if (( score > best_score )); then result="$priority" best_score="$score" fi done done echo "$result" return 0 } selectVersion() { local versions_name="$1" local indexes_name="$2" local preferred_name="$3" local result_name="$4" local index_name="$5" local -a candidates=() local -n index_map="$indexes_name" local -n version_list="$versions_name" local -n selected_version="$result_name" local -n preference_list="$preferred_name" local -n selected_image_index="$index_name" local wanted candidate match # A detected edition is only selectable when a matching answer file can # actually be staged for it. for wanted in "${preference_list[@]}"; do [ -n "$wanted" ] || continue mapfile -t candidates < <(getCompatibleVersions "$wanted") for candidate in "${candidates[@]}"; do match=$(hasVersion "$candidate" "${version_list[@]}") || continue hasAnswerFile "$match" || continue local key="${match,,}" selected_version="$match" selected_image_index="${index_map[$key]}" return 0 done done return 1 } getCompatibleVersions() { local wanted="$1" printf '%s\n' "$wanted" # Treat normal and Evaluation variants of the same edition as compatible. # The exact requested variant is always checked first. if [[ "${wanted,,}" == *"-eval" ]]; then printf '%s\n' "${wanted%-eval}" else printf '%s\n' "$wanted-eval" fi } selectEdition() { local versions_name="$1" local bases_name="$2" local groups_name="$3" local indexes_name="$4" local suggested="$5" local result_name="$6" local index_name="$7" local normalize_name="$8" local order_name="$9" local -A seen=() local -a preferred=() local -n edition_bases="$bases_name" local -n edition_order="$order_name" local -n edition_groups="$groups_name" local -n edition_versions="$versions_name" local base edition entry suffix priority i # Selection precedence is explicit EDITION, source suggestion, canonical # edition order, then noncanonical editions from the same priority groups. if [ -n "$EDITION" ]; then for base in "${edition_bases[@]}"; do edition=$("$normalize_name" "$EDITION" "$base") || return 1 preferred+=("$base${edition:+-$edition}") done if selectVersion "$versions_name" "$indexes_name" preferred "$result_name" "$index_name"; then return 0 fi warn "edition '$EDITION' is not supported by this image, using automatic selection instead." fi if [ -n "$suggested" ]; then preferred=("$suggested") if selectVersion "$versions_name" "$indexes_name" preferred "$result_name" "$index_name"; then return 0 fi fi # First try each canonical edition in its configured order. preferred=() for entry in "${edition_order[@]}"; do IFS='|' read -r suffix _ _ <<< "$entry" for base in "${edition_bases[@]}"; do preferred+=("$base$suffix") done done if selectVersion "$versions_name" "$indexes_name" preferred "$result_name" "$index_name"; then return 0 fi # Then try noncanonical editions from the same preference groups. seen=() preferred=() for entry in "${edition_order[@]}"; do IFS='|' read -r _ priority _ <<< "$entry" [[ -v "seen[$priority]" ]] && continue seen["$priority"]="Y" for ((i=0;i<${#edition_versions[@]};i++)); do [[ "${edition_groups[$i]}" == "$priority" ]] || continue preferred+=("${edition_versions[$i]}") done done selectVersion "$versions_name" "$indexes_name" preferred "$result_name" "$index_name" } detectVersion() { local xml="$1" local suggested="${2:-}" local -a bases=() local -a groups=() local -a versions=() local -A image_indexes=() local -a selection_order=() local result="" index="" getVersions "$xml" versions bases groups image_indexes || return 1 if [ "${#versions[@]}" -eq 0 ]; then printf '%s\n%s\n' "$result" "$index" return 0 fi local normalize="normalizeEditionID" case "${bases[0],,}" in "win20"* ) normalize="normalizeServerEditionID" ;; esac mapfile -t selection_order < <(getEditionOrder "${bases[0]}") if selectEdition versions bases groups image_indexes "$suggested" result index "$normalize" selection_order; then printf '%s\n%s\n' "$result" "$index" return 0 fi # Keep the first detected image identity when no edition with a usable answer # file was found, so manual and generic fallback handling can still continue. result="${versions[0]}" local key="${result,,}" index="${image_indexes[$key]}" printf '%s\n%s\n' "$result" "$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" } hasVersion() { local wanted="$1" shift local actual for actual in "$@"; do [[ "${actual,,}" == "${wanted,,}" ]] || continue echo "$actual" return 0 done return 1 } 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 image_count invalid_count local x86 x64 arm64 unknown value local platform="" count=0 output local -a counts=() if ! output=$(xmlstarlet sel \ -T -t \ -v 'count(/WIM/IMAGE)' -n \ -v 'count(/WIM/IMAGE[count(WINDOWS/ARCH) != 1])' -n \ -v 'count(/WIM/IMAGE/WINDOWS/ARCH[normalize-space(.)="0"])' -n \ -v 'count(/WIM/IMAGE/WINDOWS/ARCH[normalize-space(.)="9"])' -n \ -v 'count(/WIM/IMAGE/WINDOWS/ARCH[normalize-space(.)="12"])' -n \ -v 'count(/WIM/IMAGE/WINDOWS/ARCH[normalize-space(.)!="0" and normalize-space(.)!="9" and normalize-space(.)!="12"])' -n \ - 2>/dev/null <<< "$xml"); then error "Failed to read architecture metadata from WIM image!" return 1 fi mapfile -t counts <<< "$output" if (( ${#counts[@]} != 6 )); then error "Failed to read architecture counts from WIM metadata!" return 1 fi for value in "${counts[@]}"; do if [[ ! "$value" =~ ^[0-9]+$ ]]; then error "Invalid architecture count in WIM metadata: '$value'" return 1 fi done image_count="${counts[0]}" invalid_count="${counts[1]}" x86="${counts[2]}" x64="${counts[3]}" arm64="${counts[4]}" unknown="${counts[5]}" if (( image_count == 0 )); then error "No images were found in WIM metadata!" return 1 fi if (( invalid_count > 0 )); then error "Missing or duplicate architecture metadata in WIM image!" return 1 fi if (( unknown > 0 )); then error "Unsupported architecture value in WIM metadata!" return 1 fi (( x86 > 0 )) && ((count += 1)) (( x64 > 0 )) && ((count += 1)) (( arm64 > 0 )) && ((count += 1)) if (( count > 1 )); then platform="mixed" elif (( x86 > 0 )); then platform="x86" elif (( x64 > 0 )); then platform="x64" elif (( arm64 > 0 )); then platform="arm64" else error "Failed to determine architecture from WIM metadata!" return 1 fi echo "$platform" return 0 } createImageDirectory() { local image="$1" local directory="$2" # Treat an existing directory as success; create it only when mdir cannot # already resolve it. 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 # 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 return 1 } readWimHeader() { local iso="$1" local image="$2" local size signature local header="$TMP/wim-header.bin" rm -f -- "$header" || return 1 # 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 || ! size=$(stat -c%s -- "$header") || (( size != 208 )) || ! signature=$(od -An -N8 -tx1 "$header" | tr -d ' \n') || [[ "$signature" != "4d5357494d000000" ]]; then rm -f -- "$header" return 1 fi echo "$header" return 0 } readIsoImageInfo() { local iso="$1" local image="$2" local header="$3" local raw result root xml_count local header_size version local part_number total_parts image_count local xml_offset xml_size xml_original xml_flags local -a bytes=() values=() [ -f "$header" ] || return 1 raw=$(od -An -v -N208 -tu1 -- "$header") || return 1 read -r -a bytes <<< "${raw//$'\n'/ }" (( ${#bytes[@]} == 208 )) || return 1 # Validate the MSWIM\0\0\0 signature. (( bytes[0] == 77 && bytes[1] == 83 && bytes[2] == 87 && bytes[3] == 73 && bytes[4] == 77 && bytes[5] == 0 && bytes[6] == 0 && bytes[7] == 0 )) || return 1 # Header size at offset 0x08. header_size=$(( \ bytes[8] | bytes[9] << 8 | bytes[10] << 16 | bytes[11] << 24 )) (( header_size == 208 )) || return 1 # WIM version at offset 0x0c. version=$(( \ bytes[12] | bytes[13] << 8 | bytes[14] << 16 | bytes[15] << 24 )) (( version == 0x10d00 || version == 0x0e00 )) || return 1 # Split-WIM information at offsets 0x28 and 0x2a. part_number=$((bytes[40] | bytes[41] << 8)) total_parts=$((bytes[42] | bytes[43] << 8)) (( part_number > 0 && total_parts > 0 && part_number <= total_parts )) || return 1 # Image count at offset 0x2c. image_count=$(( \ bytes[44] | bytes[45] << 8 | bytes[46] << 16 | bytes[47] << 24 )) (( image_count > 0 && image_count <= 65535 )) || return 1 result=$(parseWimHeader "$iso" "$image" "$header") || return 1 mapfile -t values <<< "$result" (( ${#values[@]} == 4 )) || return 1 xml_offset="${values[0]}" xml_size="${values[1]}" xml_original="${values[2]}" xml_flags="${values[3]}" [[ "$xml_offset" =~ ^[0-9]+$ && "$xml_size" =~ ^[0-9]+$ && "$xml_original" =~ ^[0-9]+$ && "$xml_flags" =~ ^[0-9]+$ ]] || return 1 (( xml_size > 0 && xml_original > 0 && xml_size == xml_original && xml_size % 2 == 0 )) || return 1 # 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. (( !(xml_flags & 0x1c) )) || return 1 result=$(udfread range --ignore-case "$iso" "$image" "$xml_offset" "$xml_size" \ 2>/dev/null | iconv -f UTF-16LE -t UTF-8 2>/dev/null ) || { local rc=$? if (( rc >= 129 )); then exit "$rc" fi return 1 } [ -n "$result" ] || return 1 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") || return 1 IFS="$separator" read -r root xml_count <<< "$metadata" [ "$root" = "WIM" ] || return 1 [[ "$xml_count" =~ ^[0-9]+$ ]] || return 1 (( xml_count == image_count )) || return 1 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 return 1 fi if ! raw=$(od -An -v -j8 -N88 -tu1 -- "$header"); then return 1 fi read -r -a bytes <<< "${raw//$'\n'/ }" if (( ${#bytes[@]} != 88 )); then 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 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 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 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 return 1 fi if ! details=$(udfread stat --ignore-case "$iso" "$image" 2>/dev/null); then return 1 fi image_size=$(sed -n 's/^Size: \([0-9][0-9]*\) bytes$/\1/p' <<< "$details") if [[ ! "$image_size" =~ ^[0-9]+$ ]]; then return 1 fi if (( parsed_offset > image_size || parsed_size > image_size - parsed_offset )); then 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="" result=$(wimlib-imagex info --xml "$wim" | iconv -f UTF-16LE -t UTF-8) || { local rc=$? if (( rc >= 129 )); then exit "$rc" fi result="" } if [ -z "$result" ]; then error "Failed to read Windows image information!" return 2 fi printf '%s' "$result" return 0 } getSuggestion() { [ -z "$CUSTOM" ] || return 0 [ -n "${REUSED_ISO:-}" ] || return 0 # A reused ISO may still correspond to the originally requested catalog # version, but the suggestion remains only a preference during detection. echo "${SUGGEST:-}" } 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 msg="Failed to determine Windows version from image" local rc=0 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 suggested index checkPlatform "$image_info" || exit 67 suggested=$(getSuggestion) || return 1 local output output=$(detectVersion "$image_info" "$suggested") || return 1 local -a detected=() mapfile -t detected <<< "$output" DETECTED="${detected[0]:-}" index="${detected[1]:-}" validateEdition || return 1 if [ -z "$DETECTED" ]; then unknownImage || return 1 return 0 fi detectLanguage "$image_info" "$index" || return 1 desc=$(describeImage) || return 1 info "Detected: $desc" configureImage "$index" "$desc" || return 1 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") || return 1 header=$(readWimHeader "$iso" "$image") || return 2 image_info=$(readIsoImageInfo "$iso" "$image" "$header") || { rc=$? (( rc >= 129 )) && return "$rc" return 2 } info "Detecting version from ISO image..." detectImageInfo "$image_info" || return 2 return 0 } checkFreeSpace() { local dir="$1" local size="$2" local space size_gb space_gb space=$(df --output=avail -B 1 "$dir" | tail -n 1) || return 1 [[ "$space" =~ ^[[:space:]]*[0-9]+[[:space:]]*$ ]] || { error "Failed to determine available disk space for $dir!" return 1 } space="${space//[[:space:]]/}" if (( size > space )); then size_gb=$(formatBytes "$size") space_gb=$(formatBytes "$space") error "Not enough free space in $STORAGE, 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 ISO 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 if ! xml=$(wimlib-imagex info "$iso" --xml 2>/dev/null | iconv -f UTF-16LE -t UTF-8 2>/dev/null); then error "Cannot read ESD file information!" return 1 fi # 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 1 /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 1 } 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 1 } 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 1 } fKill "progress.sh" if [[ "${PLATFORM,,}" == "x64" ]]; then LABEL="CCCOMA_X64FRE_EN-US_DV9" else LABEL="CPBA_A64FRE_EN-US_DV9" fi msg="Extracting image from ESD file" info "$msg..." && html "$msg..." 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 1 output=$(detectVersion "$installXml") || return 1 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 )) /run/progress.sh "$installWim" "$installSize" "$msg ([P])..." & if ! rm -f -- "$dir/sources/install.wim" "$installWim"; then fKill "progress.sh" 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 fKill "progress.sh" 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 if ! wimlib-imagex delete "$installWim" "$image" --soft --quiet; then fKill "progress.sh" error "Failed to remove image $image from install.esd!" return 1 fi done if ! result=$(wimlib-imagex info "$installWim" --xml 2>/dev/null | iconv -f UTF-16LE -t UTF-8 2>/dev/null); then fKill "progress.sh" error "Cannot verify the prepared install.esd file!" return 1 fi 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 fKill "progress.sh" 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 fKill "progress.sh" error "Prepared install.esd does not contain exactly one image at index 1!" return 1 fi if [[ "${version,,}" != "http"* ]] && [[ "${resultEdition,,}" != "${edition,,}" ]]; then fKill "progress.sh" error "Prepared install.esd does not contain only '$edition' at index 1!" return 1 fi fKill "progress.sh" return 0 } normalizeBatch() { local file="$1" local bom tmp encoding [ ! -s "$file" ] && return 0 bom=$(od -An -N2 -tx1 "$file" | tr -d ' \n') || return 1 # Convert only BOM-marked UTF-16 files; unmarked ANSI and UTF-8 batch files # are deliberately left unchanged. case "$bom" in "fffe" ) encoding="UTF-16LE" ;; "feff" ) encoding="UTF-16BE" ;; * ) return 0 ;; esac if ! tmp=$(mktemp "${file}.XXXXXX"); then error "Failed to create temporary batch file!" return 1 fi if ! tail -c +3 "$file" | iconv -f "$encoding" -t UTF-8 > "$tmp"; then rm -f "$tmp" error "Failed to convert $file from $encoding to UTF-8!" return 1 fi if ! chmod --reference="$file" "$tmp" || ! mv -f "$tmp" "$file"; then rm -f "$tmp" error "Failed to replace batch file: $file" return 1 fi return 0 } reportBatchMatches() { local file="$1" local source="$2" local pattern="$3" local message="$4" local suggestion="$5" local matches line matches=$(grep -Pin "$pattern" "$file" || true) [ -n "$matches" ] || return 0 warn "$message in $source:" while IFS= read -r line; do printf ' %s\n' "$line" >&2 done <<< "$matches" printf ' %s\n\n' "$suggestion" >&2 return 0 } checkBatch() { local file="$1" local tmp output local matches line local enabled_rules [ -s "$file" ] || return 0 if ! tmp=$(mktemp -d /tmp/blinter.XXXXXX); then warn "failed to create temporary Blinter directory." return 0 fi local source="your install.bat file" [ -n "${COMMAND:-}" ] && source="your COMMAND variable" # Only check rules that indicate likely execution or behavioural failures. enabled_rules="E001,E002,E003,E004,E005,E006,E007,E008" enabled_rules+=",E009,E010,E011,E012,E013,E014,E015,E016" enabled_rules+=",E017,E018,E019,E020,E021,E022,E023,E024" enabled_rules+=",E025,E027,E028,E029,E030,E031,E032,E033,E034" enabled_rules+=",W004,W005,W013,W017,W021,W022,W034,W038,W040" if enabled "$DEBUG"; then if LC_ALL=C grep -Pq '[^\x09\x0D\x20-\x7E]' "$file"; then warn "non-ASCII characters were detected in $source and may not execute correctly in Windows Command Prompt." fi fi cat > "$tmp/blinter.ini" <&1 || true ) # Remove header. output=$( awk ' /^DETAILED ISSUES:/ { found = 1 next } found && !started { if (/^-+$/) { started = 1 } next } started { print } ' <<< "$output" ) output="${output#"${output%%[!$'\r\n ']*}"}" output="${output%"${output##*[!$'\r\n ']}"}" if grep -Eq '^(ERROR|WARNING|SECURITY) LEVEL ISSUES:$' <<< "$output"; then warn "possible issues were detected in $source:" printf '\n%s\n\n' "$output" >&2 fi rm -rf "$tmp" || true reportBatchMatches \ "$file" \ "$source" \ '(? 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 failed="" 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 if ! genisoimage "${args[@]}" -quiet "$dir" 2> "$log"; then failed="y" fi fKill "progress.sh" if [ -n "$failed" ]; then [ -s "$log" ] && echo "$(<"$log")" error "Failed to build image!" && return 1 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 1 if ! setOwner "$BOOT"; then warn "Failed to set the owner for \"$BOOT\" !" fi return 0 } return 0