#!/usr/bin/env bash set -Eeuo pipefail validateResolution() { local name="$1" local value="$2" local minimum="$3" if [[ ! "$value" =~ ^[0-9]+$ ]] || [ "${#value}" -gt 5 ]; then error "The $name variable must be between $minimum and 16384!" return 1 fi local number=$((10#$value)) if [ "$number" -lt "$minimum" ] || [ "$number" -gt 16384 ]; then error "The $name variable must be between $minimum and 16384!" return 1 fi return 0 } validateProductKey() { local value="$1" [ -z "$value" ] && return 0 if [[ ! "$value" =~ ^[A-Za-z0-9]{5}(-[A-Za-z0-9]{5}){4}$ ]]; then error "The KEY variable must contain a valid 25-character product key!" return 1 fi return 0 } validateComputerName() { local value="$1" [ -z "$value" ] && return 0 if [ "${#value}" -gt 15 ]; then error "The HOST variable cannot contain more than 15 characters!" return 1 fi if [[ ! "$value" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$ ]]; then error "The HOST variable may only contain letters, digits, and hyphens, and cannot start or end with a hyphen!" return 1 fi if [[ "$value" =~ ^[0-9]+$ ]]; then error "The HOST variable cannot contain only digits!" return 1 fi return 0 } validateWorkgroup() { local value="$1" local safe [ -z "$value" ] && return 0 if [ "${#value}" -gt 15 ]; then error "The WORKGROUP variable cannot contain more than 15 characters!" return 1 fi safe=$(printf '%s' "$value" | tr -d '"/\\[]:;|=,+*?<>') || return 1 if [[ "$safe" != "$value" ]]; then error "The WORKGROUP variable contains characters that are not valid in a NetBIOS name!" return 1 fi if [[ "$value" =~ ^[.[:space:]]+$ ]]; then error "The WORKGROUP variable cannot consist only of spaces or periods!" return 1 fi return 0 } validateMembership() { if [ -n "$DOMAIN" ] && [ -n "$WORKGROUP" ]; then error "The DOMAIN and WORKGROUP variables cannot be used together!" return 1 fi if [ -n "$DOMAIN_OU" ] && [ -z "$DOMAIN" ]; then error "The DOMAIN_OU variable requires DOMAIN to be specified!" return 1 fi validateWorkgroup "$WORKGROUP" || return 1 return 0 } validatePassword() { local value="$1" local desc="${2:-}" local suffix="" [ -n "$desc" ] && suffix=" for $desc" if [ "${#value}" -gt 127 ]; then error "The PASSWORD variable cannot contain more than 127 characters$suffix!" return 1 fi if [[ "$value" =~ [[:cntrl:]] ]]; then error "The PASSWORD variable cannot contain control characters$suffix!" return 1 fi return 0 } escapeXMLSed() { local s s=$(escapeXML "$1") || return 1 s=${s//\\/\\\\} s=${s//&/\\&} s=${s//|/\\|} printf '%s' "$s" return 0 } validateUsername() { local value="$1" local type="$2" local maximum length_suffix invalid_message case "$type" in "local" ) [ -z "$value" ] && return 0 maximum=20 length_suffix="" invalid_message="The USERNAME variable contains characters that are not supported by Windows local accounts!" ;; "domain" ) if [ -z "$value" ]; then error "The USERNAME variable does not contain a valid domain account name!" return 1 fi maximum=256 length_suffix=" for a domain account" invalid_message="The domain account name contains characters that are not supported by Windows unattended setup!" ;; * ) return 1 ;; esac if [ "${#value}" -gt "$maximum" ]; then error "The USERNAME variable cannot contain more than $maximum characters$length_suffix!" return 1 fi if [[ "$value" =~ [[:cntrl:]] ]]; then error "The USERNAME variable cannot contain control characters!" return 1 fi case "$value" in *'"'* | *'/'* | *\\* | *'['* | *']'* | *':'* | *';'* | *'|'* | *'='* | *','* | *'+'* | *'*'* | *'?'* | *'<'* | *'>'* | *'%'* | *'@'* ) error "$invalid_message" return 1 ;; esac if [[ "$value" == *"." ]]; then error "The USERNAME variable cannot end with a period!" return 1 fi if [[ "$value" =~ ^[.[:space:]]+$ ]]; then error "The USERNAME variable cannot consist only of spaces or periods!" return 1 fi case "${value^^}" in "NONE" ) error "The USERNAME value \"NONE\" is reserved by Windows!" return 1 ;; "ADMINISTRATOR" | "GUEST" | "DEFAULTACCOUNT" | "WDAGUTILITYACCOUNT" | "WSIACCOUNT" ) [[ "$type" == "domain" ]] && return 0 error "The USERNAME value \"$value\" is reserved for a built-in Windows account!" return 1 ;; esac return 0 } validateDomainName() { local value="$1" local name="${2:-DOMAIN}" if [ -z "$value" ]; then error "The $name variable must contain a valid domain name!" return 1 fi if [[ "$value" == *"://"* ]]; then error "The $name variable must contain a domain name, not a URL!" return 1 fi if [ "${#value}" -gt 255 ] || [[ "$value" =~ [[:cntrl:]] ]] || [[ "$value" =~ [[:space:]] ]] || [[ ! "$value" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$ ]]; then error "The $name variable does not contain a valid domain name!" return 1 fi return 0 } getXMLArchitecture() { local asset="$1" local arch arch=$(sed -n -E \ '0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' \ "$asset") || return 1 [ -n "$arch" ] || return 1 printf '%s' "$arch" return 0 } updateWorkgroup() { local asset="$1" local workgroup arch tmp workgroup=$(escapeXML "$2") || return 1 arch=$(getXMLArchitecture "$asset") || return 1 grep -q 'Microsoft-Windows-UnattendedJoin' "$asset" && return 1 tmp=$(mktemp -d) || return 1 local result="$tmp/answer.xml" if ! WORKGROUP_XML="$workgroup" ARCH_XML="$arch" awk ' /]*pass="specialize"[^>]*>/ { section = "specialize" } section == "specialize" && !workgroup_added && /^[[:space:]]*<\/settings>[[:space:]]*$/ { print " \n" \ " \n" \ " " ENVIRON["WORKGROUP_XML"] "\n" \ " \n" \ " " workgroup_added = 1 } { print } /^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" } END { exit !workgroup_added } ' "$asset" > "$result" || ! mv -f "$result" "$asset"; then rm -rf "$tmp" || true return 1 fi rm -rf "$tmp" || return 1 return 0 } updateDomain() { local asset="$1" local domain account auth pass local ou arch tmp domain=$(escapeXML "$2") || return 1 account=$(escapeXML "$3") || return 1 auth=$(escapeXML "$4") || return 1 pass=$(escapeXML "$5") || return 1 ou=$(escapeXML "$6") || return 1 arch=$(getXMLArchitecture "$asset") || return 1 local cred_domain="$domain" case "$4" in *@* ) cred_domain="" ;; esac grep -Eq 'Microsoft-Windows-UnattendedJoin|])' "$asset" && return 1 tmp=$(mktemp -d) || return 1 local result="$tmp/answer.xml" if ! DOMAIN_XML="$domain" ACCOUNT_XML="$account" \ AUTH_XML="$auth" PASS_XML="$pass" \ CRED_DOMAIN="$cred_domain" OU_XML="$ou" \ ARCH_XML="$arch" \ awk ' /]*pass="specialize"[^>]*>/ { section = "specialize" } /]*pass="oobeSystem"[^>]*>/ { section = "oobeSystem" } section == "oobeSystem" && /])/ { in_accounts = 1 } section == "oobeSystem" && /])/ { in_autologon = 1 } section == "oobeSystem" && in_accounts && !accounts_added && /])/ { print " \n" \ " \n" \ " \n" \ " " ENVIRON["ACCOUNT_XML"] "\n" \ " Administrators\n" \ " \n" \ " " ENVIRON["DOMAIN_XML"] "\n" \ " \n" \ " " accounts_added = 1 } section == "oobeSystem" && in_autologon && /^[[:space:]]*.*<\/Username>[[:space:]]*$/ { print " " ENVIRON["ACCOUNT_XML"] "\n" \ " " ENVIRON["DOMAIN_XML"] "" autologon_added = 1 next } section == "oobeSystem" && in_autologon && /^[[:space:]]*])/ { next } section == "oobeSystem" && in_autologon && /^[[:space:]]*.*<\/Value>[[:space:]]*$/ { print " " ENVIRON["PASS_XML"] "" password_added = 1 next } section == "oobeSystem" && in_autologon && /^[[:space:]]*])/ { print " true</PlainText>" plaintext_added = 1 next } section == "specialize" && !join_added && /^[[:space:]]*<\/settings>[[:space:]]*$/ { print " <component name=\"Microsoft-Windows-UnattendedJoin\" processorArchitecture=\"" ENVIRON["ARCH_XML"] "\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\">\n" \ " <Identification>\n" \ " <Credentials>" if (ENVIRON["CRED_DOMAIN"] != "") { print " <Domain>" ENVIRON["CRED_DOMAIN"] "</Domain>" } print " <Username>" ENVIRON["AUTH_XML"] "</Username>\n" \ " <Password>" ENVIRON["PASS_XML"] "</Password>\n" \ " </Credentials>\n" \ " <JoinDomain>" ENVIRON["DOMAIN_XML"] "</JoinDomain>" if (ENVIRON["OU_XML"] != "") { print " <MachineObjectOU>" ENVIRON["OU_XML"] "</MachineObjectOU>" } print " </Identification>\n" \ " </component>" join_added = 1 } { print } section == "oobeSystem" && /<\/AutoLogon>/ { in_autologon = 0 } section == "oobeSystem" && /<\/UserAccounts>/ { in_accounts = 0 } /^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" } END { exit !(join_added && accounts_added && autologon_added && password_added && plaintext_added) } ' "$asset" > "$result" || ! mv -f "$result" "$asset"; then rm -rf "$tmp" || true return 1 fi rm -rf "$tmp" || return 1 return 0 } removeLocalAccountXML() { local asset="$1" if ! sed -i -E \ -e '/^[[:space:]]*<LocalAccounts([[:space:]>])/,/^[[:space:]]*<\/LocalAccounts>[[:space:]]*$/d' \ -e '/^[[:space:]]*<AdministratorPassword([[:space:]>])/,/^[[:space:]]*<\/AdministratorPassword>[[:space:]]*$/d' \ "$asset"; then error "Failed to remove local account configuration from answer file!" return 1 fi if ! sed -i -E ' /<SynchronousCommand([[:space:]>])/ { :command N /<\/SynchronousCommand>/!b command /<Description>Password Never Expires<\/Description>/d } ' "$asset"; then error "Failed to remove local account commands from answer file!" return 1 fi return 0 } enableLog() { local file="$1" local old='C:\OEM\install.bat"</CommandLine>' local msg="failed to enable install logging in the answer file!" enabled "${LOG:-}" || return 0 [ -f "$file" ] || return 1 if ! grep -Fq "$old" "$file"; then enabled "$DEBUG" && warn "$msg" return 0 fi if ! sed -i \ 's|C:\\OEM\\install\.bat"</CommandLine>|C:\\OEM\\install.bat \&gt; C:\\OEM\\install.log 2\&gt;\&amp;1"</CommandLine>|' \ "$file"; then warn "$msg" fi return 0 } markGeneratedXML() { local file="$1" local marker='<!-- generated-answer-file: do not reuse as a template -->' [ -s "$file" ] || return 1 if head -n 1 "$file" | grep -q '^<?xml'; then sed -i "1a$marker" "$file" || return 1 else sed -i "1i$marker" "$file" || return 1 fi return 0 } removeGeneratedXML() { local file="$1" [ -n "$file" ] || return 0 [ -f "$file" ] || return 0 head -n 5 "$file" | grep -Fqi 'generated-answer-file' || return 0 if ! rm -f "$file"; then error "Failed to remove generated answer file: $file" return 1 fi return 0 } generateAnswerFile() { local id="$1" local source="$2" local target="$3" local index="$4" local type="$5" local remove_selector="$6" local tmp if [ -n "$index" ] && [[ ! "$index" =~ ^[1-9][0-9]*$ ]]; then error "Invalid $type image index: $index" return 1 fi if ! tmp=$(mktemp -p /run/assets ".${id}.XXXXXX"); then error "Failed to create a temporary $type answer file!" return 1 fi local expressions if [ "$type" = "evaluation" ]; then expressions=( -e '/<ProductKey>.*<\/ProductKey>/d' -e '/<ProductKey>/,/<\/ProductKey>/d' ) else expressions=( -e '/<InstallFrom>.*<\/InstallFrom>/d' -e '/<ProductKey>.*<\/ProductKey>/d' -e '/<InstallFrom>/,/<\/InstallFrom>/d' -e '/<ProductKey>/,/<\/ProductKey>/d' ) fi if ! sed "${expressions[@]}" "$source" > "$tmp"; then rm -f "$tmp" error "Failed to generate $type answer file from $source!" return 1 fi if [ "$type" = "evaluation" ] && [ "$remove_selector" = "Y" ]; then if ! sed -i \ -e '/<InstallFrom>.*<\/InstallFrom>/d' \ -e '/<InstallFrom>/,/<\/InstallFrom>/d' \ "$tmp"; then rm -f "$tmp" error "Failed to replace evaluation image selector!" return 1 fi fi if [ -n "$index" ] && ! grep -q '<InstallFrom>' "$tmp"; then if ! sed -i \ '0,/<InstallTo>/{ /<InstallTo>/i\ <InstallFrom>\ <MetaData wcm:action="add">\ <Key>/IMAGE/INDEX</Key>\ <Value>'"$index"'</Value>\ </MetaData>\ </InstallFrom> }' "$tmp"; then rm -f "$tmp" error "Failed to select $type image index $index!" return 1 fi fi if ! markGeneratedXML "$tmp" || ! xmllint --nonet --noout "$tmp"; then rm -f "$tmp" error "Generated $type answer file is invalid!" return 1 fi if ! chmod 644 "$tmp" || ! mv -f "$tmp" "$target"; then rm -f "$tmp" error "Failed to create $type answer file: $target" return 1 fi return 0 } generateEvalXML() { # Evaluation templates are generated from their normal counterpart so # both variants remain identical except for evaluation-specific selectors. local id="$1" local detected_index="${2:-}" [[ "${id,,}" == *"-eval" ]] || return 1 local normal="${id::-5}" local source="/run/assets/$normal.xml" local target="/run/assets/$id.xml" local index="$detected_index" local remove_selector="N" removeGeneratedXML "$source" || return 1 if [ ! -s "$source" ]; then source="/run/assets/${normal%%-*}.xml" removeGeneratedXML "$source" || return 1 fi [ -s "$source" ] || return 1 if [ -n "$detected_index" ]; then remove_selector="Y" else # No WIM was inspected, so retain the known defaults for download routes. case "${id,,}" in *"-ltsc-eval" ) index="1" ;; *"-iot-eval" ) index="2" ;; esac fi generateAnswerFile \ "$id" "$source" "$target" "$index" "evaluation" "$remove_selector" || return 1 return 0 } generateFallbackXML() { # Fallback templates are generated from the generic version so unsupported # editions can use the detected WIM index without inheriting a product key. local id="$1" local index="${2:-}" local source="/run/assets/${id%%-*}.xml" local target="/run/assets/$id.xml" [ "$source" != "$target" ] || return 1 removeGeneratedXML "$source" || return 1 [ -s "$source" ] || return 1 generateAnswerFile \ "$id" "$source" "$target" "$index" "fallback" "Y" || return 1 return 0 } setXML() { local file="$1" local index="${2:-}" local target="/run/assets/$DETECTED.xml" local custom_files=( "/custom.xml" "$STORAGE/custom.xml" "/run/assets/custom.xml" ) CUSTOM_XML="" removeGeneratedXML "$target" || return 1 if [ -d "${custom_files[0]}" ]; then error "The bind ${custom_files[0]} maps to a file that does not exist!" exit 67 fi for file in "${custom_files[@]}"; do if [ -f "$file" ] && [ -s "$file" ]; then CUSTOM_XML="Y" XML="$file" return 0 fi done file="$1" if [[ "${DETECTED,,}" == *"-eval" ]] && { [ ! -f "$file" ] || [ ! -s "$file" ]; }; then generateEvalXML "$DETECTED" "$index" || return 1 file="$target" elif [ ! -f "$file" ] || [ ! -s "$file" ]; then file="$target" elif [[ "$file" != "$target" ]]; then generateFallbackXML "$DETECTED" "$index" || return 1 file="$target" fi [ -f "$file" ] && [ -s "$file" ] || return 1 XML="$file" return 0 } validateXMLSettings() { validateResolution "WIDTH" "$WIDTH" 320 || return 1 validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 validateMembership || return 1 validateComputerName "${HOST:-}" || return 1 validateProductKey "${KEY:-}" || return 1 validatePassword "${PASSWORD:-}" || return 1 return 0 } updateDisplayXML() { local asset="$1" local app host app=$(escapeXMLSed "$APP for $ENGINE") || return 1 sed -i "s|>Windows for Docker<|>$app<|g" "$asset" || return 1 sed -i -E "s|<VerticalResolution>[^<]*</VerticalResolution>|<VerticalResolution>$HEIGHT</VerticalResolution>|g" "$asset" || return 1 sed -i -E "s|<HorizontalResolution>[^<]*</HorizontalResolution>|<HorizontalResolution>$WIDTH</HorizontalResolution>|g" "$asset" || return 1 [ -n "${HOST:-}" ] || return 0 host=$(escapeXMLSed "$HOST") || return 1 sed -i -E "s|<ComputerName>[^<]*</ComputerName>|<ComputerName>$host</ComputerName>|g" "$asset" || return 1 return 0 } updateLocaleXML() { local asset="$1" local language="$2" local culture region keyboard value culture=$(getLanguage "$language" "culture") || return 1 if [ -n "$culture" ] && [[ "${culture,,}" != "en-us" ]]; then value=$(escapeXMLSed "$culture") || return 1 sed -i "s|<UILanguage>en-US</UILanguage>|<UILanguage>$value</UILanguage>|g" "$asset" || return 1 fi region="${REGION:-$culture}" if [ -n "$region" ] && [[ "${region,,}" != "en-us" ]]; then value=$(escapeXMLSed "$region") || return 1 sed -i "s|<UserLocale>en-US</UserLocale>|<UserLocale>$value</UserLocale>|g" "$asset" || return 1 sed -i "s|<SystemLocale>en-US</SystemLocale>|<SystemLocale>$value</SystemLocale>|g" "$asset" || return 1 fi keyboard="${KEYBOARD:-$culture}" if [ -n "$keyboard" ] && [[ "${keyboard,,}" != "en-us" ]]; then value=$(escapeXMLSed "$keyboard") || return 1 sed -i "s|<InputLocale>en-US</InputLocale>|<InputLocale>$value</InputLocale>|g" "$asset" || return 1 sed -i "s|<InputLocale>0409:00000409</InputLocale>|<InputLocale>$value</InputLocale>|g" "$asset" || return 1 fi return 0 } prepareDomainAccount() { local domain="$1" local -n account_ref="$2" local -n auth_ref="$3" local qualifier="" auth_ref="${USERNAME:-}" account_ref="" if [ -z "$auth_ref" ]; then error "The USERNAME variable must be specified when joining a domain!" return 1 fi if [ -z "${PASSWORD:-}" ]; then error "The PASSWORD variable must be specified when joining a domain!" return 1 fi validateDomainName "$domain" || return 1 if [[ "$auth_ref" == *\\* ]]; then error "The USERNAME variable must use either \"user\" or \"user@domain\" format!" return 1 fi case "$auth_ref" in *@* ) account_ref="${auth_ref%%@*}" qualifier="${auth_ref#*@}" if [ -z "$account_ref" ] || [ -z "$qualifier" ] || [[ "$qualifier" == *@* ]]; then error "The USERNAME variable does not contain a valid domain account name!" return 1 fi validateDomainName "$qualifier" "USERNAME" || return 1 if [[ "${qualifier,,}" != "${domain,,}" ]]; then error "The domain in the USERNAME variable must match the DOMAIN variable!" return 1 fi ;; * ) account_ref="$auth_ref" ;; esac validateUsername "$account_ref" "domain" || return 1 if [[ "${account_ref,,}" == "docker" ]]; then error "The USERNAME variable must be changed from its default value when joining a domain!" return 1 fi if [[ "$PASSWORD" == "admin" ]]; then error "The PASSWORD variable must be changed from its default value when joining a domain!" return 1 fi return 0 } updateLocalAccountXML() { local asset="$1" local user="${USERNAME:-}" local pass="${PASSWORD:-admin}" local user_xml pw admin validateUsername "$user" "local" || return 1 if [ -n "$user" ]; then user_xml=$(escapeXMLSed "$user") || return 1 sed -i "s|-name \"Docker\"|-name \"\$env:USERNAME\"|g" "$asset" || return 1 sed -i 's|where name="Docker"|where name="%USERNAME%"|g' "$asset" || return 1 sed -i "s|<Name>Docker</Name>|<Name>$user_xml</Name>|g" "$asset" || return 1 sed -i "s|<FullName>Docker</FullName>|<FullName>$user_xml</FullName>|g" "$asset" || return 1 sed -i "s|<Username>Docker</Username>|<Username>$user_xml</Username>|g" "$asset" || return 1 fi pw=$(printf '%s' "${pass}Password" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1 admin=$(printf '%s' "${pass}AdministratorPassword" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1 sed -i -z -E \ "s#(<Password>[[:space:]]*<Value)([[:space:]]*/>|>[^<]*</Value>)#\1>$pw</Value>#g" \ "$asset" || return 1 sed -i -z -E \ "s#(<AdministratorPassword>[[:space:]]*<Value)([[:space:]]*/>|>[^<]*</Value>)#\1>$admin</Value>#g" \ "$asset" || return 1 return 0 } updateMembershipXML() { local asset="$1" local domain="$2" local workgroup="$3" local account="$4" local auth="$5" if [ -n "$domain" ]; then if ! updateDomain \ "$asset" \ "$domain" \ "$account" \ "$auth" \ "$PASSWORD" \ "${DOMAIN_OU:-}"; then warn "failed to add domain configuration to answer file!" return 0 fi removeLocalAccountXML "$asset" || return 1 return 0 fi [ -n "$workgroup" ] || return 0 if ! updateWorkgroup "$asset" "$workgroup"; then warn "failed to add workgroup configuration to answer file!" fi return 0 } updateAutologinXML() { local asset="$1" disabled "${AUTOLOGIN:-}" || return 0 sed -i -E \ '/^[[:space:]]*<AutoLogon([[:space:]>])/,/^[[:space:]]*<\/AutoLogon>[[:space:]]*$/d' \ "$asset" || return 1 return 0 } updateEditionXML() { local asset="$1" local edition [ -n "${EDITION:-}" ] || return 0 edition=$(normalizeServerEdition "$EDITION") || return 1 edition="${edition//-/}" edition="${edition^^}" edition=$(escapeXMLSed "$edition") || return 1 sed -i \ "s|SERVERSTANDARD</Value>|SERVER$edition</Value>|g" \ "$asset" || return 1 return 0 } updateProductKeyXML() { local asset="$1" local key [ -n "${KEY:-}" ] || return 0 key=$(escapeXMLSed "$KEY") || return 1 sed -i -E \ '/^[[:space:]]*<ProductKey>[[:space:]]*$/,/^[[:space:]]*<\/ProductKey>[[:space:]]*$/d' \ "$asset" || return 1 sed -i -E \ "s|<ProductKey>[^<]*</ProductKey>|<ProductKey>$key</ProductKey>|g" \ "$asset" || return 1 sed -i \ "s|</UserData>| <ProductKey>\n <Key>$key</Key>\n <WillShowUI>OnError</WillShowUI>\n </ProductKey>\n </UserData>|g" \ "$asset" || return 1 return 0 } removeSharedFolderXML() { local asset="$1" if ! disabled "${SHORTCUT:-}" && ! disabled "${SAMBA:-}"; then return 0 fi if ! sed -i -E ' /<SynchronousCommand([[:space:]>])/ { :command N /<\/SynchronousCommand>/!b command /<Description>Create desktop shortcut to shared folder<\/Description>/d /<Description>Map shared folder<\/Description>/d } ' "$asset"; then error "Failed to remove shared folder shortcuts from answer file!" return 1 fi return 0 } validateGeneratedXML() { local asset="$1" if ! xmllint --nonet --noout "$asset"; then error "The generated answer file is not valid XML!" return 1 fi return 0 } updateXML() { local asset="$1" local language="$2" local domain="${DOMAIN:-}" local workgroup="${WORKGROUP:-}" local account="" local auth="" [ -z "${WIDTH:-}" ] && WIDTH="1280" [ -z "${HEIGHT:-}" ] && HEIGHT="720" validateXMLSettings || return 1 updateDisplayXML "$asset" || return 1 updateLocaleXML "$asset" "$language" || return 1 if [ -n "$domain" ]; then prepareDomainAccount "$domain" account auth || return 1 else updateLocalAccountXML "$asset" || return 1 fi sed -i -E \ "s|<PlainText>[^<]*</PlainText>|<PlainText>false</PlainText>|g" \ "$asset" || return 1 updateMembershipXML \ "$asset" \ "$domain" \ "$workgroup" \ "$account" \ "$auth" || return 1 updateAutologinXML "$asset" || return 1 enableLog "$asset" || return 1 updateEditionXML "$asset" || return 1 updateProductKeyXML "$asset" || return 1 removeSharedFolderXML "$asset" || return 1 validateGeneratedXML "$asset" || return 1 return 0 } escapeSIFValue() { local s="$1" s=${s//%/%%} s=${s//\"/\"\"} printf '%s' "$s" return 0 } escapeRegistryValue() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' } validateLegacyText() { local name="$1" local value="$2" local desc="${3:-}" local suffix="" [ -n "$desc" ] && suffix=" for $desc" if [[ "$value" =~ [[:cntrl:]] ]]; then error "The $name variable cannot contain control characters$suffix!" return 1 fi if [[ "$value" == *'"'* ]]; then error "The $name variable cannot contain double quotes$suffix!" return 1 fi return 0 } validateLegacyUsername() { local value="$1" local desc="${2:-}" local suffix="" [ -n "$desc" ] && suffix=" for $desc" if [ -z "$value" ]; then error "The USERNAME variable cannot be empty$suffix!" return 1 fi if [ "${#value}" -gt 20 ]; then error "The USERNAME variable cannot contain more than 20 characters$suffix!" return 1 fi if [[ "$value" =~ [[:cntrl:]] ]]; then error "The USERNAME variable cannot contain control characters$suffix!" return 1 fi case "$value" in *'"'* | *'/'* | *\\* | *'['* | *']'* | *':'* | *';'* | *'|'* | *'='* | \ *','* | *'+'* | *'*'* | *'?'* | *'<'* | *'>'* | *'%'* ) error "The USERNAME variable contains unsupported characters$suffix!" return 1 ;; esac if [[ "$value" == *"." ]]; then error "The USERNAME variable cannot end with a period$suffix!" return 1 fi if [[ "$value" =~ ^[.[:space:]]+$ ]]; then error "The USERNAME variable cannot consist only of spaces or periods$suffix!" return 1 fi case "${value^^}" in "NONE" ) error "The USERNAME value \"NONE\" is reserved by Windows$suffix!" return 1 ;; "ADMINISTRATOR" | "GUEST" | "DEFAULTACCOUNT" | "WDAGUTILITYACCOUNT" | "WSIACCOUNT" ) error "The USERNAME value \"$value\" is reserved for a built-in Windows account$suffix!" return 1 ;; esac return 0 } extractLegacyDrivers() { local drivers="$1" rm -rf "$drivers" || return 1 mkdir -p "$drivers" || return 1 if ! bsdtar -xf /var/drivers.txz -C "$drivers"; then error "Failed to extract drivers!" return 1 fi return 0 } copyLegacyStorageDriver() { local dir="$1" local target="$2" local driver="$3" local arch="$4" local drivers="$5" local destination="$dir/\$OEM\$/\$1/Drivers/viostor" if [ ! -f "$drivers/viostor/$driver/$arch/viostor.sys" ]; then error "Failed to locate required storage drivers!" return 1 fi cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$target" || return 1 mkdir -p "$destination" || return 1 cp -L "$drivers/viostor/$driver/$arch/viostor.cat" "$destination" || return 1 cp -L "$drivers/viostor/$driver/$arch/viostor.inf" "$destination" || return 1 cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$destination" || return 1 return 0 } addLegacyNetworkDriver() { local dir="$1" local driver="$2" local arch="$3" local drivers="$4" local destination="$dir/\$OEM\$/\$1/Drivers/NetKVM" if [ ! -f "$drivers/NetKVM/$driver/$arch/netkvm.sys" ]; then error "Failed to locate required network drivers!" return 1 fi mkdir -p "$destination" || return 1 cp -L "$drivers/NetKVM/$driver/$arch/netkvm.cat" "$destination" || return 1 cp -L "$drivers/NetKVM/$driver/$arch/netkvm.inf" "$destination" || return 1 cp -L "$drivers/NetKVM/$driver/$arch/netkvm.sys" "$destination" || return 1 return 0 } patchLegacyStorageDriver() { local file="$1" local arch="$2" sed -i '/^\[SCSI.Load\]/s/$/\nviostor=viostor.sys,4/' "$file" || return 1 sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\nviostor.sys=1,,,,,,4_,4,1,,,1,4/' "$file" || return 1 sed -i '/^\[SCSI\]/s/$/\nviostor=\"Red Hat VirtIO SCSI Disk Device\"/' "$file" || return 1 sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_1AF4\&DEV_1001\&SUBSYS_00000000=\"viostor\"/' "$file" || return 1 sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_1AF4\&DEV_1001\&SUBSYS_00020000=\"viostor\"/' "$file" || return 1 sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_1AF4\&DEV_1001\&SUBSYS_00021AF4=\"viostor\"/' "$file" || return 1 return 0 } addLegacySataDriver() { local dir="$1" local target="$2" local arch="$3" local drivers="$4" local file="$5" local destination="$dir/\$OEM\$/\$1/Drivers/sata" if [ ! -d "$drivers/sata/xp/$arch" ]; then error "Failed to locate required SATA drivers!" return 1 fi mkdir -p "$destination" || return 1 cp -Lr "$drivers/sata/xp/$arch/." "$destination" || return 1 cp -Lr "$drivers/sata/xp/$arch/." "$target" || return 1 sed -i '/^\[SCSI.Load\]/s/$/\niaStor=iaStor.sys,4/' "$file" || return 1 sed -i '/^\[FileFlags\]/s/$/\niaStor.sys = 16/' "$file" || return 1 sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaStor.cat = 1,,,,,,,1,0,0/' "$file" || return 1 sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaStor.inf = 1,,,,,,,1,0,0/' "$file" || return 1 sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaStor.sys = 1,,,,,,4_,4,1,,,1,4/' "$file" || return 1 sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaStor.sys = 1,,,,,,,1,0,0/' "$file" || return 1 sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaahci.cat = 1,,,,,,,1,0,0/' "$file" || return 1 sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaAHCI.inf = 1,,,,,,,1,0,0/' "$file" || return 1 sed -i '/^\[SCSI\]/s/$/\niaStor=\"Intel\(R\) SATA RAID\/AHCI Controller\"/' "$file" || return 1 sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_8086\&DEV_2922\&CC_0106=\"iaStor\"/' "$file" || return 1 return 0 } addLegacyDrivers() { local dir="$1" local target="$2" local driver="$3" local arch="$4" local drivers="$5" local file local msg="Adding drivers to image..." info "$msg" && html "$msg" extractLegacyDrivers "$drivers" || return 1 copyLegacyStorageDriver "$dir" "$target" "$driver" "$arch" "$drivers" || return 1 addLegacyNetworkDriver "$dir" "$driver" "$arch" "$drivers" || return 1 file=$(find "$target" -maxdepth 1 -type f -iname TXTSETUP.SIF -print -quit) || return 1 if [ -z "$file" ]; then error "The file TXTSETUP.SIF could not be found!" return 1 fi patchLegacyStorageDriver "$file" "$arch" || return 1 addLegacySataDriver "$dir" "$target" "$arch" "$drivers" "$file" || return 1 rm -rf "$drivers" || return 1 return 0 } setLegacyKey() { local target="$1" local driver="$2" local arch="$3" local desc="$4" local setup pid key file setup=$(find "$target" -maxdepth 1 -type f -iname setupp.ini -print -quit) || return 1 [[ -n "$setup" ]] || return 0 [[ -z "$KEY" ]] || return 0 pid=$(<"$setup") || return 1 pid="${pid%$'\r'}" if [[ "$driver" == "2k" ]]; then echo "${pid::-3}270" > "$setup" || return 1 return 0 fi if [[ "$pid" == *"270" ]]; then warn "this version of $desc requires a volume license key (VLK), it will ask for one during installation." return 0 fi file=$(find "$target" -maxdepth 1 -type f -iname PID.INF -print -quit) || return 1 if [[ -n "$file" ]]; then if [[ "$driver" == "2k3" ]]; then key=$(grep -i -A 2 "StagingKey" "$file" | tail -n 2 | head -n 1) || key="" else key="${pid: -8:5}" if [[ "${pid^^}" == *"OEM" ]]; then key=$(grep -i -A 2 "$key" "$file" | tail -n 2 | head -n 1) || key="" else key=$(grep -i -m 1 -A 2 "$key" "$file" | tail -n 2 | head -n 1) || key="" fi key="${key#*= }" fi key="${key%$'\r'}" [[ "${#key}" == "29" ]] && KEY="$key" fi [[ -n "$KEY" ]] && return 0 # These are NOT pirated keys, they come from official MS documentation. case "${driver,,}" in "xp" ) if [[ "${arch,,}" == "x86" ]]; then # Windows XP Professional x86 generic trial key (no activation) KEY="DR8GV-C8V6J-BYXHG-7PYJR-DB66Y" else # Windows XP Professional x64 generic trial key (no activation) KEY="B2RBK-7KPT9-4JP6X-QQFWM-PJD6G" fi ;; "2k3" ) if [[ "${arch,,}" == "x86" ]]; then # Windows Server 2003 Standard x86 generic trial key (no activation) KEY="QKDCQ-TP2JM-G4MDG-VR6F2-P9C48" else # Windows Server 2003 Standard x64 generic trial key (no activation) KEY="P4WJG-WK3W7-3HM8W-RWHCK-8JTRY" fi ;; esac echo "${pid::-3}000" > "$setup" || return 1 return 0 } writeCommand() { local install="$1" [ -f "$install" ] || return 0 if enabled "${LOG:-}"; then printf '%s' "\"Script\"=\"cmd /C start \\\"Install\\\" \\\"cmd /C C:\\\\OEM\\\\install.bat > C:\\\\OEM\\\\install.log 2>&1\\\"\"" else printf '%s' "\"Script\"=\"cmd /C start \\\"Install\\\" \\\"cmd /C C:\\\\OEM\\\\install.bat\\\"\"" fi return 0 } writeSIF() { local target="$1" local driver="$2" local product="$3" local sifHost="$4" local sifUsername="$5" local sifPassword="$6" local sifOrganization="$7" local sifWorkgroup="$8" find "$target" -maxdepth 1 -type f -iname winnt.sif -delete || return 1 { printf '%s\n' \ '[Data]' \ ' AutoPartition=1' \ ' MsDosInitiated="0"' \ ' UnattendedInstall="Yes"' \ ' AutomaticUpdates="Yes"' \ '' \ '[Unattended]' \ ' UnattendSwitch=Yes' \ ' UnattendMode=FullUnattended' \ ' FileSystem=NTFS' \ ' OemSkipEula=Yes' \ ' OemPreinstall=Yes' \ ' Repartition=Yes' \ ' WaitForReboot="No"' \ ' DriverSigningPolicy="Ignore"' \ ' NonDriverSigningPolicy="Ignore"' \ ' OemPnPDriversPath="Drivers\viostor;Drivers\NetKVM;Drivers\sata"' \ ' NoWaitAfterTextMode=1' \ ' NoWaitAfterGUIMode=1' \ ' FileSystem=ConvertNTFS' \ ' ExtendOemPartition=0' \ ' Hibernation="No"' \ '' \ '[GuiUnattended]' \ ' OEMSkipRegional=1' \ ' OemSkipWelcome=1' \ " AdminPassword=\"$sifPassword\"" \ ' TimeZone=0' if disabled "$AUTOLOGIN"; then printf '%s\n' ' AutoLogon=No' else printf '%s\n' \ ' AutoLogon=Yes' \ ' AutoLogonCount=65432' fi printf '%s\n' \ '' \ '[UserData]' \ " FullName=\"$sifUsername\"" \ " ComputerName=\"$sifHost\"" \ " OrgName=\"$sifOrganization\"" \ " $product" \ '' \ '[Identification]' \ " JoinWorkgroup = \"$sifWorkgroup\"" \ '' \ '[Display]' \ ' BitsPerPel=32' \ " XResolution=$WIDTH" \ " YResolution=$HEIGHT" \ '' \ '[Networking]' \ ' InstallDefaultComponents=Yes' \ '' \ '[Branding]' \ ' BrandIEUsingUnattended=Yes' \ '' \ '[URL]' \ ' Home_Page = http://www.google.com' \ ' Search_Page = http://www.google.com' \ '' \ '[TerminalServices]' \ ' AllowConnections=1' \ '' } | unix2dos > "$target/WINNT.SIF" || return 1 if [[ "$driver" == "2k3" ]]; then { printf '%s\n' \ '[Components]' \ ' TerminalServer=On' \ '' \ '[LicenseFilePrintData]' \ ' AutoMode=PerServer' \ ' AutoUsers=5' \ '' } | unix2dos >> "$target/WINNT.SIF" || return 1 fi return 0 } writeRegistry() { local dir="$1" local shortcut="$2" local oem="$3" local regUsername="$4" local regPassword="$5" { printf '%s\n' \ 'Windows Registry Editor Version 5.00' \ '' \ '[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Security]' \ '"FirstRunDisabled"=dword:00000001' \ '"UpdatesDisableNotify"=dword:00000001' \ '"FirewallDisableNotify"=dword:00000001' \ '"AntiVirusDisableNotify"=dword:00000001' \ '' \ '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\wscsvc]' \ '"Start"=dword:00000004' \ '' \ '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile\GloballyOpenPorts\List]' \ '"3389:TCP"="3389:TCP:*:Enabled:@xpsp2res.dll,-22009"' \ '' \ '[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Applets\Tour]' \ '"RunCount"=dword:00000000' \ '' \ '[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced]' \ '"HideFileExt"=dword:00000000' \ '' \ '[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer]' \ '"NoWelcomeScreen"="1"' \ '' \ '[HKEY_CURRENT_USER\Software\Microsoft\Internet Connection Wizard]' \ '"Completed"="1"' \ '"Desktopchanged"="1"' \ '' \ '[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]' if disabled "$AUTOLOGIN"; then printf '%s\n' '"AutoAdminLogon"="0"' else printf '%s\n' \ '"AutoAdminLogon"="1"' \ "\"DefaultUserName\"=\"$regUsername\"" \ "\"DefaultPassword\"=\"$regPassword\"" fi printf '%s\n' \ '' \ '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Video\{23A77BF7-ED96-40EC-AF06-9B1F4867732A}\0000]' \ '"DefaultSettings.BitsPerPel"=dword:00000020' \ "\"DefaultSettings.XResolution\"=dword:$XHEX" \ "\"DefaultSettings.YResolution\"=dword:$YHEX" \ '' \ '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Hardware Profiles\Current\System\CurrentControlSet\Control\VIDEO\{23A77BF7-ED96-40EC-AF06-9B1F4867732A}\0000]' \ '"DefaultSettings.BitsPerPel"=dword:00000020' \ "\"DefaultSettings.XResolution\"=dword:$XHEX" \ "\"DefaultSettings.YResolution\"=dword:$YHEX" \ '' \ '[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce]' \ '"ScreenSaver"="reg add \"HKCU\\Control Panel\\Desktop\" /f /v \"SCRNSAVE.EXE\" /t REG_SZ /d \"off\""' \ '"ScreenSaverOff"="reg add \"HKCU\\Control Panel\\Desktop\" /f /v \"ScreenSaveActive\" /t REG_SZ /d \"0\""' if enabled "$shortcut"; then printf '%s\n' '"SharedDrive"="cmd /C net use Z: \\\\host.lan\\Data /persistent:yes"' fi printf '%s\n' "$oem" '' } | unix2dos > "$dir/\$OEM\$/install.reg" || return 1 return 0 } appendRegistry() { local dir="$1" local driver="$2" if [[ "$driver" == "2k" ]]; then { printf '%s\n' \ '[HKEY_USERS\.DEFAULT\Software\Microsoft\Windows\CurrentVersion\Runonce]' \ '"^SetupICWDesktop"=-' \ '' } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 fi if [[ "$driver" == "2k3" ]]; then { printf '%s\n' \ '[HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\srvWiz]' \ '@=dword:00000000' \ '' \ '[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ServerOOBE\SecurityOOBE]' \ '"DontLaunchSecurityOOBE"=dword:00000000' \ '' } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 fi return 0 } writeVBS() { local dir="$1" local username="$2" local shortcut="$3" { printf '%s\n' \ 'Set WshShell = WScript.CreateObject("WScript.Shell")' \ 'Set WshNetwork = WScript.CreateObject("WScript.Network")' \ 'Set Domain = GetObject("WinNT://" & WshNetwork.ComputerName)' \ '' \ 'Function DecodeSID(binSID)' \ ' ReDim o(LenB(binSID))' \ '' \ ' For i = 1 To LenB(binSID)' \ ' o(i-1) = AscB(MidB(binSID, i, 1))' \ ' Next' \ '' \ ' sid = "S-" & CStr(o(0)) & "-" & OctetArrayToString _' \ ' (Array(o(2), o(3), o(4), o(5), o(6), o(7)))' \ ' For i = 8 To (4 * o(1) + 4) Step 4' \ ' sid = sid & "-" & OctetArrayToString _' \ ' (Array(o(i+3), o(i+2), o(i+1), o(i)))' \ ' Next' \ '' \ ' DecodeSID = sid' \ 'End Function' \ '' \ 'Function OctetArrayToString(arr)' \ ' v = 0' \ ' For i = 0 To UBound(arr)' \ ' v = v * 256 + arr(i)' \ ' Next' \ '' \ ' OctetArrayToString = CStr(v)' \ 'End Function' \ '' \ 'For Each DomainItem in Domain' \ ' If DomainItem.Class = "User" Then' \ ' sid = DecodeSID(DomainItem.Get("objectSID"))' \ ' If Left(sid, 9) = "S-1-5-21-" And Right(sid, 4) = "-500" Then' \ ' LocalAdminADsPath = DomainItem.ADsPath' \ ' Exit For' \ ' End If' \ ' End If' \ 'Next' \ '' \ "Call Domain.MoveHere(LocalAdminADsPath, \"$username\")" \ '' if enabled "$shortcut"; then printf '%s\n' \ 'Set oLink = WshShell.CreateShortcut(WshShell.SpecialFolders("Desktop") & "\Shared.lnk")' \ 'With oLink' \ ' .TargetPath = "\\host.lan\Data"' \ ' .Save' \ 'End With' \ 'Set oLink = Nothing' \ '' fi } | unix2dos > "$dir/\$OEM\$/install.vbs" || return 1 { printf '%s\n' \ '[COMMANDS]' \ '"REGEDIT /s install.reg"' \ '"Wscript install.vbs"' \ '' } | unix2dos > "$dir/\$OEM\$/cmdlines.txt" || return 1 return 0 } legacyInstall() { local dir="$2" local desc="$3" local driver="$4" local shortcut="Y" local drivers="/tmp/drivers" if disabled "$SHORTCUT" || disabled "${SAMBA:-Y}"; then shortcut="N" fi if [ -n "$DOMAIN" ]; then error "The DOMAIN variable is not supported for $desc!" return 1 fi ETFS="[BOOT]/Boot-NoEmul.img" if [ ! -f "$dir/$ETFS" ] || [ ! -s "$dir/$ETFS" ]; then error "Failed to locate file \"$ETFS\" in $desc ISO image!" return 1 fi local arch="amd64" [ ! -d "$dir/AMD64" ] && arch="x86" local target="$dir/AMD64" [[ "${arch,,}" == "x86" ]] && target="$dir/I386" if [ ! -d "$target" ]; then error "Failed to locate directory \"$target\" in $desc ISO image!" return 1 fi if [[ "${driver,,}" == "xp" || "${driver,,}" == "2k3" ]]; then addLegacyDrivers "$dir" "$target" "$driver" "$arch" "$drivers" || return 1 fi setLegacyKey "$target" "$driver" "$arch" "$desc" || return 1 validateProductKey "$KEY" || return 1 local product="" [ -n "$KEY" ] && product="ProductID=$KEY" mkdir -p "$dir/\$OEM\$" || return 1 if ! addFolder "$dir"; then error "Failed to add OEM folder to image!" return 1 fi local oem="" local install="" local oem_dir="$dir/\$OEM\$/\$1/OEM" if [ -d "$oem_dir" ]; then install=$(find \ "$oem_dir" \ -maxdepth 1 \ -type f \ -iname install.bat \ -print -quit ) || return 1 fi oem=$(writeCommand "$install") || return 1 [ -z "$WIDTH" ] && WIDTH="1280" [ -z "$HEIGHT" ] && HEIGHT="720" validateResolution "WIDTH" "$WIDTH" 320 || return 1 validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 validateMembership || return 1 validateComputerName "$HOST" || return 1 validateLegacyText "APP" "$APP" "$desc" || return 1 validateLegacyText "ENGINE" "$ENGINE" "$desc" || return 1 XHEX=$(printf '%08x\n' "$((10#$WIDTH))") || return 1 YHEX=$(printf '%08x\n' "$((10#$HEIGHT))") || return 1 local username="${USERNAME:-Docker}" local password="${PASSWORD:-admin}" local workgroup="${WORKGROUP:-WORKGROUP}" local sifHost sifUsername sifPassword sifOrganization sifWorkgroup local regUsername regPassword validateLegacyUsername "$username" "$desc" || return 1 validatePassword "$password" "$desc" || return 1 sifHost=$(escapeSIFValue "${HOST:-*}") || return 1 sifUsername=$(escapeSIFValue "$username") || return 1 sifPassword=$(escapeSIFValue "$password") || return 1 sifOrganization=$(escapeSIFValue "$APP for $ENGINE") || return 1 sifWorkgroup=$(escapeSIFValue "$workgroup") || return 1 regUsername=$(escapeRegistryValue "$username") || return 1 regPassword=$(escapeRegistryValue "$password") || return 1 writeSIF \ "$target" \ "$driver" \ "$product" \ "$sifHost" \ "$sifUsername" \ "$sifPassword" \ "$sifOrganization" \ "$sifWorkgroup" || return 1 writeRegistry \ "$dir" \ "$shortcut" \ "$oem" \ "$regUsername" \ "$regPassword" || return 1 appendRegistry "$dir" "$driver" || return 1 writeVBS "$dir" "$username" "$shortcut" || return 1 return 0 } return 0