Current File : //usr/local/jetapps/usr/share/rear/lib/layout-functions.sh
# Utility functions for the system layout processing.

# Each file will be only saved once by save_original_file()
# and all subsequent save_original_file() for the same file do nothing
# because each saved file is remembered in the SAVED_ORIGINAL_FILES array:
SAVED_ORIGINAL_FILES=()
SAVED_ORIGINAL_FILE_SUFFIX="orig"

# Save the original content of the file $1 to $1.$START_DATE_TIME_NUMBER.$2.orig
# or to $1.$START_DATE_TIME_NUMBER.$WORKFLOW.$MASTER_PID.orig when no $2 is specified:
save_original_file() {
    local filename="$1"
    test -r "$filename" || return 1
    IsInArray "$filename" "${SAVED_ORIGINAL_FILES[@]}" && return 0
    local extension="$2"
    test "$extension" || extension=$WORKFLOW.$MASTER_PID
    local saved_original_file="$filename.$START_DATE_TIME_NUMBER.$extension.$SAVED_ORIGINAL_FILE_SUFFIX"
    cp -ar $filename $saved_original_file && SAVED_ORIGINAL_FILES+=( "$filename" )
}

# Restore the saved original content of the original file named $1
# that was saved as $1.$START_DATE_TIME_NUMBER.$2.orig or $1.$START_DATE_TIME_NUMBER.$WORKFLOW.$MASTER_PID.orig
restore_original_file() {
    local filename="$1"
    local extension="$2"
    test "$extension" || extension=$WORKFLOW.$MASTER_PID
    local saved_original_file="$filename.$START_DATE_TIME_NUMBER.$extension.$SAVED_ORIGINAL_FILE_SUFFIX"
    test -r "$saved_original_file" || return 1
    cp -ar $saved_original_file $filename
}

# Generate code to recreate a device $1 of type $2 and then mount it.
# Note that we do not handle partitioning here.
create_device() {
    local device="$1"
    local type="$2"
    local name # used to extract the actual name of the device

    cat <<EOF >> "$LAYOUT_CODE"
if create_component "$device" "$type" ; then
EOF
    echo "# Create $device ($type)" >> "$LAYOUT_CODE"
    if type -t create_$type >/dev/null ; then
        create_$type "$device"
    fi
    cat <<EOF >> "$LAYOUT_CODE"
component_created "$device" "$type"
else
    LogPrint "Skipping $device ($type) as it has already been created."
fi

EOF
}

# Generate code to mount a device $1 of type $2 ('mountonly' workflow).
do_mount_device() {
    local device="$1"
    local type="$2"
    local name # used to extract the actual name of the device

    cat <<EOF >> "$LAYOUT_CODE"
if create_component "$device" "$type" ; then
EOF
    # This can be used a.o. to decrypt a LUKS device
    echo "# Open $device ($type)" >> "$LAYOUT_CODE"
    if type -t open_$type >/dev/null ; then
        open_$type "$device"
    fi

    # If the device is mountable, it will then be mounted
    echo "# Mount $device ($type)" >> "$LAYOUT_CODE"
    if type -t mount_$type >/dev/null ; then
        mount_$type "$device"
    fi
    cat <<EOF >> "$LAYOUT_CODE"
component_created "$device" "$type"
else
    LogPrint "Skipping $device ($type) as it has already been mounted."
fi

EOF
}

abort_recreate() {
    Log "Error detected during restore."
    Log "Restoring saved original $LAYOUT_FILE"
    restore_original_file "$LAYOUT_FILE"
}

abort_dasd_format() {
    Log "Error detected during DASD formatting."
    Log "Restoring saved original $DASD_FORMAT_FILE"
    restore_original_file "$DASD_FORMAT_FILE"
}

# Test and log if a component $1 (type $2) needs to be recreated.
create_component() {
    local device="$1"
    local type="$2"
    # If a touchfile already exists, no need to recreate this component.
    local touchfile="$type-${device//\//-}"
    if [ -e "$LAYOUT_TOUCHDIR/$touchfile" ] ; then
        return 1
    else
        return 0
    fi
}

# Mark a component as created.
component_created() {
    local device=$1
    local type=$2
    # Create a touchfile.
    local touchfile="$type-${device//\//-}"
    touch "$LAYOUT_TOUCHDIR/$touchfile"
}

# Generate dependencies between disks as found in $LAYOUT_FILE.
# This will be written to $LAYOUT_DEPS.
# Also generate a list of disks to be restored in $LAYOUT_TODO.
generate_layout_dependencies() {
    # $LAYOUT_DEPS is a list of:
    # <item> <depends on>
    : > $LAYOUT_DEPS

    # $LAYOUT_TODO is a list of:
    # [todo|done] <type> <item>
    : > $LAYOUT_TODO

    local type dev remainder name disk disks vgrp dm_vgrp lvol dm_lvol part mp fs bd nmp temp_nm
    while read type remainder ; do
        case $type in
            disk)
                name=$(echo "$remainder" | cut -d " " -f "1")
                add_component "$name" "disk"
                ;;
            part)
                # disk is the first field of the remainder
                disk=$(echo "$remainder" | cut -d " " -f "1")
                name=$(echo "$remainder" | cut -d " " -f "6")
                add_dependency "$name" "$disk"
                add_component "$name" "part"
                ;;
            lvmgrp)
                name=$(echo "$remainder" | cut -d " " -f "1")
                add_component "$name" "lvmgrp"
                ;;
            lvmdev)
                vgrp=$(echo "$remainder" | cut -d " " -f "1")
                part=$(echo "$remainder" | cut -d " " -f "2")
                add_dependency "$vgrp" "pv:$part"
                add_dependency "pv:$part" "$part"
                add_component "pv:$part" "lvmdev"
                ;;
            lvmvol)
                vgrp=$(echo "$remainder" | cut -d " " -f "1")
                lvol=$(echo "$remainder" | cut -d " " -f "2")
                # When a LV is a Thin, then we need to create the Thin Pool first
                pool=$(echo "$remainder" | grep -Eow "thinpool:\\S+" | cut -d ":" -f 2)

                # Vgs and Lvs containing - in their name have a double dash in DM
                dm_vgrp=${vgrp//-/--}
                dm_lvol=${lvol//-/--}
                dm_pool=${pool//-/--}

                dm_prefix="/dev/mapper/${dm_vgrp#/dev/}"
                add_dependency "$dm_prefix-$dm_lvol" "$vgrp"
                [ -z "$pool" ] || add_dependency "$dm_prefix-$dm_lvol" "$dm_prefix-$dm_pool"
                add_component "$dm_prefix-$dm_lvol" "lvmvol"
                ;;
            raidarray)
                name=$(echo "$remainder" | cut -d " " -f "1")
                disks=$(echo "$remainder" | sed -r "s/.*devices=([^ ]+).*/\1/" | tr ',' ' ')
                for disk in $disks ; do
                    add_dependency "$name" "$disk"
                done
                add_component "$name" "raidarray"
                ;;
            fs|btrfsmountedsubvol)
                dev=$(echo "$remainder" | cut -d " " -f "1")
                mp=$(echo "$remainder" | cut -d " " -f "2")
                add_dependency "$type:$mp" "$dev"
                add_component "$type:$mp" "$type"

                # find dependencies on other filesystems
                while read dep_type bd dep_mp junk; do
                    if [ "$dep_mp" != "/" ] ; then
                        # make sure we only match complete paths
                        # e.g. not /data as a parent of /data1
                        temp_dep_mp="$dep_mp/"
                    else
                        temp_dep_mp="$dep_mp"
                    fi

                    if [ "${mp#$temp_dep_mp}" != "${mp}" ] && [ "$mp" != "$dep_mp" ]; then
                        add_dependency "$type:$mp" "$dep_type:$dep_mp"
                    fi
                done < <( grep -E '^fs |^btrfsmountedsubvol ' $LAYOUT_FILE )
                ;;
            swap)
                dev=$(echo "$remainder" | cut -d " " -f "1")
                add_dependency "swap:$dev" "$dev"
                add_component "swap:$dev" "swap"
                ;;
            drbd)
                dev=$(echo "$remainder" | cut -d " " -f "1")
                disk=$(echo "$remainder" | cut -d " " -f "3")
                add_dependency "$dev" "$disk"
                add_component "$dev" "drbd"
                ;;
            crypt)
                name=$(echo "$remainder" | cut -d " " -f "1")
                dev=$(echo "$remainder" | cut -d " " -f "2")
                add_dependency "$name" "$dev"
                add_component "$name" "crypt"
                ;;
            multipath)
                name=$(echo "$remainder" | cut -d " " -f "1")
                disks=$(echo "$remainder" | cut -d " " -f "4" | tr "," " ")
                add_component "$name" "multipath"
                for disk in $disks ; do
                    add_dependency "$name" "$disk"
                done
                ;;
            opaldisk)
                dev=$(echo "$remainder" | cut -d " " -f "1")
                add_component "opaldisk:$dev" "opaldisk"
                for disk in $(opal_device_disks "$dev"); do
                    add_dependency "$disk" "opaldisk:$dev"
                done
                ;;
        esac
    done < $LAYOUT_FILE
}

# Add a dependency from one component on another
# add_dependency <from> <on>
add_dependency() {
    echo "$1 $2" >> $LAYOUT_DEPS
}

# Add a component to be restored
# add_component <name> <type>
# The name must be equal to the one used in dependency resolution
# The type is needed to restore the component.
add_component() {
    echo "todo $1 $2" >> $LAYOUT_TODO
}

# The distinction in the mark_as_done and mark_tree_as_done functions what messages should appear
# - only in the log file in debug '-d' mode via 'Debug'
# - in the log file and on the user's terminal in debug '-d' mode via 'DebugPrint'
# matches the same kind of distinction in the disable_component_... functions
# in layout/save/default/330_remove_exclusions.sh
# but no LogPrint is used in the lower-level mark_as_done and mark_tree_as_done functions.

# Mark component $1 as done.
mark_as_done() {
    # The trailing blank in "... $1 " is crucial to not match wrong components
    # for example the component "... /dev/sda1" must not match accidentally
    # other components like "... /dev/sda12" in var/lib/rear/layout/disktodo.conf
    if grep -q "done $1 " $LAYOUT_TODO ; then
        DebugPrint "Component '$1' is marked as 'done $1' in $LAYOUT_TODO"
        return 0
    fi
    if ! grep -q "todo $1 " $LAYOUT_TODO ; then
        Debug "Cannot mark component '$1' as done because there is no 'todo $1 ' in $LAYOUT_TODO"
        return 1
    fi
    DebugPrint "Marking component '$1' as done in $LAYOUT_TODO"
    sed -i "s;todo\ $1\ ;done\ $1\ ;" $LAYOUT_TODO
}

# Mark all components that depend on component $1 as done.
mark_tree_as_done() {
    for component in $( get_child_components "$1" ) ; do
        DebugPrint "Dependent component $component is a child of component $1"
        mark_as_done "$component"
    done
}

# Return all the (grand-)child components of $1 [filtered by type $2]
get_child_components() {
    declare -a devlist children
    declare current child parent

    devlist=( "$1" )
    while (( ${#devlist[@]} )) ; do
        current=${devlist[0]}

        ### Find all direct child elements of the current component...
        while read child parent junk ; do
            if [[ "$parent" = "$current" ]] ; then
                ### ...and add them to the list
                if IsInArray "$child" "${children[@]}" ; then
                    continue
                fi
                devlist+=( "$child" )
                children+=( "$child" )
            fi
        done < $LAYOUT_DEPS

        # remove the current element from the array and re-index it because
        # "unset does not remove the element it just sets null string to the particular index in array"
        # see https://stackoverflow.com/questions/16860877/remove-an-element-from-a-bash-array
        unset "devlist[0]"
        devlist=( "${devlist[@]}" )
    done

    ### Filter for the wanted type
    declare component type
    for component in "${children[@]}" ; do
        if [[ "$2" ]] ; then
            type=$(get_component_type "$component")
            if [[ "$type" != "$2" ]] ; then
                continue
            fi
        fi
        echo "$component"
    done
}

# Return all ancestors of component $1 [ of type $2 [ skipping types $3 during resolution ] ]
get_parent_components() {
    declare -a ancestors devlist ignoretypes
    declare current child parent parenttype

    devlist=( "$1" )
    if [[ "$3" ]] ; then
        # third argument should, if present, be a space-separated list
        # of types to ignore when walking up the dependency tree.
        # Convert it to array
        ignoretypes=( $3 )
    else
        ignoretypes=()
    fi
    while (( ${#devlist[@]} )) ; do
        current=${devlist[0]}

        ### Find all direct parent elements of the current component...
        while read child parent junk ; do
            if [[ "$child" = "$current" ]] ; then
                ### ...test if we visited them already...
                if IsInArray "$parent" "${ancestors[@]}" ; then
                    continue
                fi
                ### ...test if parent is of a correct type if requested...
                if [[ ${#ignoretypes[@]} -gt 0 ]] ; then
                    parenttype=$(get_component_type "$parent")
                    if IsInArray "$parenttype" "${ignoretypes[@]}" ; then
                        continue
                    fi
                fi
                ### ...and add them to the list
                devlist+=( "$parent" )
                ancestors+=( "$parent" )
            fi
        done < $LAYOUT_DEPS

        # remove the current element from the array and re-index it because
        # "unset does not remove the element it just sets null string to the particular index in array"
        # see https://stackoverflow.com/questions/16860877/remove-an-element-from-a-bash-array
        unset "devlist[0]"
        devlist=( "${devlist[@]}" )
    done

    ### Filter the ancestors for the correct type.
    declare component type
    for component in "${ancestors[@]}" ; do
        if [[ "$2" ]] ; then
            type=$(get_component_type "$component")
            if [[ "$type" != "$2" ]] ; then
                continue
            fi
        fi
        echo "$component"
    done
}

# find_devices <other>
# ${2+"$2"} in the following functions ensures that $2 gets passed down quoted if present
# and ignored if not present
# Find the disk device(s) component $1 resides on.
find_disk() {
    get_parent_components "$1" "disk" ${2+"$2"}
}

find_multipath() {
    get_parent_components "$1" "multipath" ${2+"$2"}
}

find_disk_and_multipath() {
    find_disk "$1" ${2+"$2"}
    is_true "$AUTOEXCLUDE_MULTIPATH" || find_multipath "$1" ${2+"$2"}
}

find_partition() {
    get_parent_components "$1" "part" ${2+"$2"}
}

# The get_partition_number function
# outputs the trailing digits of a partition block device as its partition number.
# Usually only the basename of the partition block device is used as function argument
# e.g. "get_partition_number sda2" instead of "get_partition_number /dev/sda2".
# The implementation requires grep v2.5 or higher (option -o is used).
# This function should support:
#   /dev/mapper/36001438005deb05d0000e00005c40000p1
#   /dev/mapper/36001438005deb05d0000e00005c40000_part1
#   /dev/sda1
#   /dev/cciss/c0d0p1
get_partition_number() {
    local partition_block_device=$1

    # The partition number is the trailing digits of the partition block device:
    local partition_number=$( echo "$partition_block_device" | grep -o -E '[0-9]+$' )

    # Test if partition_number is a positive integer, if not it is likely a bug in ReaR.
    # Because the above 'grep' outputs only trailing digits this BugError gets triggred
    # when partition_block_device device does not contain trailing digits so that partition_number is empty
    # which can happen when get_partition_number is called with a block device as argument
    # that is not a partition block device (e.g. /dev/sda instead of /dev/sda1) which is likely a bug in ReaR:
    test $partition_number -gt 0 || BugError "Partition number '$partition_number' of partition $partition_block_device is not a valid partition number."

    # Test if partition_number is greater than 128 and report it as a bug in ReaR.
    # FIXME: Why are more than 128 partitions not supported?
    # Why is it a bug in ReaR when more than 128 partitions are not supported?
    # A GPT must be for at least 128 partitions but why does ReaR not support bigger GPT?
    # I <jsmeix@suse.de> found https://github.com/rear/rear/commit/e758bba0a415173952cc588e5cf80570a6385f7e that links to
    # https://github.com/rear/rear/issues/263 that contains https://github.com/rear/rear/issues/263#issuecomment-20464763
    # which reads (excerpt): "The GPT standard allows maximum of 128 partitions per disk" which is not true
    # according to how I understand the German https://de.wikipedia.org/wiki/GUID_Partition_Table that reads
    # (excerpt, umlauts written as 'ae' and 'ue' to get ASCII see https://github.com/rear/rear/wiki/Coding-Style#character-encoding)
    # "Die EFI-Spezifikationen schreiben ein Minimum von 16384 Bytes fuer die Partitionstabelle vor, so dass es Platz fuer 128 Eintraege gibt."
    # in English "EFI specification mandate a minimum of 16384 bytes for the partition table so that there is space for 128 entries"
    # which matches the English https://en.wikipedia.org/wiki/GUID_Partition_Table that reads (excerpt)
    # "The UEFI specification stipulates that a minimum of 16384 bytes ... are allocated for the Partition Entry Array. Each entry has a size of 128 bytes."
    # and because 16384 / 128 = 128 it results that 128 partition table entries (each of 128 bytes) are possible as a minimum
    # which means that the GPT standard requires a minimum of 128 possible partitions per disk.
    # So the current BugError here might be changed into only a user notification, for example something like
    #   LogPrintError "Partition $partition_block_device is numbered '$partition_number'. More than 128 partitions may not work (GPT must be extra large)."
    # But on the other hand ReaR errors out relatively often at that place here in particular
    # when weird partition related errors before had been ignored and it proceeded until it finally errors out here
    # cf. "Try hard to care about possible errors" in https://github.com/rear/rear/wiki/Coding-Style
    # so we keep the BugError for the time being as some kind of generic safeguard to catch bugs in ReaR elsewhere
    # until we fully understand what is going on in our partitioning related code, cf. https://github.com/rear/rear/pull/2260
    test $partition_number -le 128 || BugError "Partition $partition_block_device is numbered '$partition_number'. More than 128 partitions are not supported."

    # Output the trailing digits of the partition block device as its partition number:
    echo $partition_number
}

# Extract the underlying device name from the full partition device name.
# Underlying device may be a disk, a multipath device or other devices that can be partitioned.
# Should we use the information in $LAYOUT_DEPS, like get_parent_component does,
# instead of string munging?
function get_device_from_partition() {
    local partition_block_device
    local device
    local partition_number

    partition_block_device=$1
    test -b "$partition_block_device" || BugError "get_device_from_partition called with '$partition_block_device' that is no block device"
    partition_number=${2-$(get_partition_number $partition_block_device )}
    # /dev/sda or /dev/mapper/vol34_part or /dev/mapper/mpath99p or /dev/mmcblk0p
    device=${partition_block_device%$partition_number}

    # Strip trailing partition remainders like '_part' or '-part' or 'p'
    # if we have 'mapper' in disk device name:
    if [[ ${partition_block_device/mapper//} != $partition_block_device ]] ; then
        # we only expect mpath_partX or mpathpX or mpath-partX
        case $device in
            (*p)     device=${device%p} ;;
            (*-part) device=${device%-part} ;;
            (*_part) device=${device%_part} ;;
            (*)      Log "Unsupported kpartx partition delimiter for $partition_block_device"
        esac
    fi

    # For eMMC devices the trailing 'p' in the $device value
    # (as in /dev/mmcblk0p that is derived from /dev/mmcblk0p1)
    # needs to be stripped (to get /dev/mmcblk0), otherwise the
    # efibootmgr call fails because of a wrong disk device name.
    # See also https://github.com/rear/rear/issues/2103
    if [[ $device = *'/mmcblk'+([0-9])p ]] ; then
        device=${device%p}
    fi

    # For NVMe devices the trailing 'p' in the $device value
    # (as in /dev/nvme0n1p that is derived from /dev/nvme0n1p1)
    # needs to be stripped (to get /dev/nvme0n1), otherwise the
    # efibootmgr call fails because of a wrong disk device name.
    # See also https://github.com/rear/rear/issues/1564
    if [[ $device = *'/nvme'+([0-9])n+([0-9])p ]] ; then
        device=${device%p}
    fi

    test -b "$device" && echo $device
}

# Returns partition start block or 'unknown'
# sda/sda1 or
# dm-XX
get_partition_start() {
    local disk_name=$1
    local start_block start

    # When reading /sys/block/.../start or "dmsetup table", output is always in
    # 512 bytes blocks
    local block_size=512

    if [[ -r /sys/block/$disk_name/start ]] ; then
        start_block=$(< $path/start)
    elif [[ $disk_name =~ ^dm- ]]; then
        # /dev/mapper/mpath4-part1
        local devname=$(get_device_name $disk_name)
        devname=${devname#/dev/mapper/}

        # 0 536846303 linear 253:7 536895488
        read junk junk junk junk start_block < <( dmsetup table ${devname} 2>/dev/null )
    fi
    if [[ -z $start_block ]]; then
        Log "Could not determine start of partition $partition_name."
        start="unknown"
    else
        start=$(( start_block * block_size ))
    fi

    echo $start
}

# Get the type of a layout component
get_component_type() {
    grep -E "^[^ ]+ $1 " $LAYOUT_TODO | cut -d " " -f 3
}

# Get the disklabel (partition table) type of the disk $1 from the layout file
# (NOT from the actual disk, so layout file must exist before calling this,
# and it is useful during recovery even before the disk layout has been recreated)
function get_disklabel_type () {
    # from create_disk() in layout/prepare/GNU/Linux/100_include_partition_code.sh
    local component disk size label junk

    disk=''

    read component disk size label junk < <(grep -E "^(disk|multipath) $1 " "$LAYOUT_FILE")
    test $disk || return 1

    echo $label
}

# Get partition flags from layout (space-separated) of partition given as $1
function get_partition_flags () {
    local part disk size pstart name flags partition junk

    while read part disk size pstart name flags partition junk; do
        if [ "$partition" == "$1" ] ; then
            echo "$flags" | tr ',' ' '
            return 0
        fi
    done < <(grep "^part " $LAYOUT_FILE)
}

# Function returns 0 when v1 is greater or equal than v2
version_newer() {
  local v1list=( ${1//[-.]/ } )
  local v2list=( ${2//[-.]/ } )

  ### Take largest
  local max=${#v1list[@]}
  if (( $max < ${#v2list[@]} )); then
    max=${#v2list[@]}
  fi

  local pos
  for pos in $(seq 0 $(( max -1 ))); do
    ### Arithmetic comparison
    if (( 10#0${v1list[$pos]} >= 0 && 10#0${v2list[$pos]} >= 0 )) 2>/dev/null; then
#      echo "pos $pos: arithm ${v1list[$pos]} vs ${v2list[$pos]}"
      if (( 10#0${v1list[$pos]} < 10#0${v2list[$pos]} )); then
        return 1
      elif (( 10#0${v1list[$pos]} > 10#0${v2list[$pos]} )); then
        return 0
      fi
    ### String comparison
    else
#      echo "pos $pos: string ${v1list[$pos]} vs ${v2list[$pos]}"
      if [[ "${v1list[$pos]}" < "${v2list[$pos]}" ]]; then
        return 1
      elif [[ "${v1list[$pos]}" > "${v2list[$pos]}" ]]; then
        return 0
      fi
    fi
  done

  return 0
}

# Function to get version from tool.
get_version() {
  TERM=dumb "$@" 2>&1 | sed -rn 's/^[^0-9\.]*([0-9]+\.[-0-9a-z\.]+).*$/\1/p' | head -1
}

# Translate a device name to a sysfs name.
get_sysfs_name() {
    local name=${1#/dev/}
    name=${name#/sys/block/}

    if [[ -e /sys/block/${name//\//!} ]] ; then
        echo "${name//\//!}"
        return 0
    fi

    ### Follow symlinks.
    if [[ -h /dev/$name ]] ; then
        local target=$(readlink -f /dev/$name)
        if [[ -e /sys/block/${target#/dev/} ]] ; then
            echo "${target#/dev/}"
            return 0
        fi
    fi

    # Accommodate for mapper/test -> dm-? mapping.
    local dev_number=$(dmsetup info -c --noheadings -o major,minor ${name##*/} 2>/dev/null )
    if [[ "$dev_number" ]] ; then
        local dev_name sysfs_device
        for sysfs_device in /sys/block/*/dev ; do
            if [[ "$dev_number" = "$( < $sysfs_device)" ]] ; then
                dev_name=${sysfs_device#/sys/block/}
                echo "${dev_name%/*}"
                return 0
            fi
        done
    fi

    # Otherwise, it can be the case that we just want to translate the name.
    echo "${name//\//!}"
    return 1
}

### Translate a sysfs name or device name to the name preferred in
### Relax-and-Recover.
### The device does not necessarily exist.
###     cciss!c0d0 -> /dev/cciss/c0d0
###     /dev/dm-3 -> /dev/mapper/system-tmp
###     /dev/dm-4 -> /dev/mapper/oralun
###     /dev/dm-5 -> /dev/mapper/oralunp1
###     /dev/sda -> /dev/sda
###
### Returns 0 on success, 1 if device is not existing
get_device_name() {
    ### strip common prefixes
    local name=${1#/dev/}
    name=${name#/sys/block/}

    contains_visible_char "$name" || BugError "Empty string passed to get_device_name"

    if [[ "$name" =~ ^mapper/ ]]; then
        echo "/dev/$name"
        return 0
    fi

    if [[ -L "/dev/$name" ]] ; then
        # Map vg/lv into dm-X, which will then be resolved later
        name="$( basename $(readlink -f /dev/$name) )"
    fi

    if [[ "$name" =~ ^dm- ]] ; then
        local device
        if [[ -r /sys/block/$name/dm/name ]] ; then
            ### recent kernels have a dm subfolder
            device="$( < /sys/block/$name/dm/name )"
        else
            local dev_number=$( < /sys/block/$name/dev)
            if [[ ! -r "$TMP_DIR/dmsetup_info.txt" ]] ; then
                dmsetup info --noheadings -c -o name,major,minor > "$TMP_DIR/dmsetup_info.txt"
            fi
            device="$( awk -F ':' "/$dev_number\$/ { print \$1 }" < "$TMP_DIR/dmsetup_info.txt" )"
            [[ -n "$device" ]] || BugError "No device returned for major/minor $dev_number"
        fi
        echo "/dev/mapper/$device"
        return 0
    fi

    ### handle cciss sysfs naming
    name=${name//!//}

    ### just return the possibly nonexisting name
    echo "/dev/$name"
    [[ -r "/dev/$name" ]] && return 0
    return 1
}

# check $VAR_LIB/recovery/diskbyid_mappings file to see whether we find
# a disk/by-id mapping to dm style (the by-id dev is not translated
# properly by get_device_name function - dm dev are better)
# 220_lvm_layout.sh uses get_device_mapping to translate lvmdev better
### ciss-3600508b1001fffffa004f7b3f209000b-part2 -> cciss/c0d0p2
# see issue #305
get_device_mapping() {
    if [[ ! -s "${VAR_DIR}/recovery/diskbyid_mappings" ]]; then
        echo $1
    else
        local name=${1##*/}      # /dev/disk/by-id/scsi-xxxx -> scsi-xxx
        local disk_name=$(grep -w "^${name}" ${VAR_DIR}/recovery/diskbyid_mappings | awk '{print $2}')
        if [[ -z "$disk_name" ]]; then
            echo $1
        else
            echo "$disk_name"
        fi
    fi
}

# Get the size in bytes of a disk/partition.
# For disks, use "sda" as argument.
# For partitions, use "sda/sda1" as argument.
get_disk_size() {
    local disk_name=$1
    # When a partition is specified (e.g. sda/sda1)
    # then it has to read /sys/block/sda/sda1/size in the old code below.
    # In contrast the get_block_size() function below is different
    # because it is non-sense asking for block size of a partition,
    # so that the get_block_size() function below is stripping everything
    # in front of the blockdev basename (e.g. /some/path/sda -> sda)
    # cf. https://github.com/rear/rear/pull/1885#discussion_r207900308

    # Preferably use blockdev, see https://github.com/rear/rear/issues/1884
    if has_binary blockdev; then
        # ${disk_name##*/} translates 'sda/sda1' into 'sda1' and 'sda' into 'sda'
        blockdev --getsize64 /dev/${disk_name##*/} && return
        # If blockdev fails do not error out but fall through to the old code below
        # because blockdev fails e.g. for a CDROM device when no DVD or ISO is attached to
        # cf. https://github.com/rear/rear/pull/1885#issuecomment-410676283
        # and https://github.com/rear/rear/pull/1885#issuecomment-410697398
    fi

    # Linux always considers sectors to be 512 bytes long. See the note in the
    # kernel source, specifically, include/linux/types.h regarding the sector_t
    # type for details.
    local block_size=512

    retry_command test -r /sys/block/$disk_name/size || Error "Could not determine size of disk $disk_name"

    local nr_blocks=$( < /sys/block/$disk_name/size)
    local disk_size=$(( nr_blocks * block_size ))

    ### Make sure we always return a number
    echo $(( disk_size ))
}

# Get the block size of a disk.
get_block_size() {
    local disk_name="${1##*/}" # /some/path/sda -> sda

    # Preferably use blockdev, see https://github.com/rear/rear/issues/1884
    if has_binary blockdev; then
        blockdev --getss /dev/$disk_name && return
        # If blockdev fails do not error out but fall through to the old code below
        # because blockdev fails e.g. for a CDROM device when no DVD or ISO is attached to
        # cf. https://github.com/rear/rear/pull/1885#issuecomment-410676283
        # and https://github.com/rear/rear/pull/1885#issuecomment-410697398
    fi

    # Only newer kernels have an interface to get the block size
    if [ -r /sys/block/$disk_name/queue/logical_block_size ] ; then
        echo $( < /sys/block/$disk_name/queue/logical_block_size)
    else
        echo "512"
    fi
}

# Get the number of cylinders of a DASD.
# The number of cylinders has the advantage of being fixed - size depends on formatting
# and number of cylinders is valid even for unformatted DASDs, size is not.
get_dasd_cylinders() {
    local disk_name="${1##*/}" # /some/path/dasda -> dasda
    local dasd_cyls

    dasd_cyls=$(dasdview -i /dev/$disk_name | grep cylinders | cut -d ':' -f2 | awk '{print $4}')
    ### Make sure we always return a number
    echo $(( dasd_cyls ))
}

# Sometimes we know what the new device for the original device should be in a more reliable way
# than by looking at disk sizes. THis information is called "mapping hints". Let's pass them
# to the mapping code using the DISK_MAPPING_HINTS array. Each element of the array has the form
# "/dev/source /dev/target" (space-separated).

# Output the mapping hint for the original device.
function get_mapping_hint () {
    local device="$1"
    local hint mapping_hint_source mapping_hint_target

    for hint in "${DISK_MAPPING_HINTS[@]}"; do
        mapping_hint_source=${hint%% *}
        mapping_hint_target=${hint##* }
        if [ "${device}" == "${mapping_hint_source}" ] ; then
            echo "$mapping_hint_target"
            return 0
        fi
    done
    return 1
}

# Determine if there is a mapping hint for the original device.
function has_mapping_hint () {
    local device="$1"

    get_mapping_hint "$device" > /dev/null
}

# Get the UUID of a device.
# Device is something like /dev/sda1.
blkid_uuid_of_device() {
    local device=$1
    local uuid=""
    for LINE in $(blkid $device  2>/dev/null)
    do
        uuid=$( echo "$LINE" | grep "^UUID=" | cut -d= -f2 | sed -e 's/"//g')
        [[ ! -z "$uuid" ]] && break
    done
    echo "$uuid"
}

# Get the LABEL of a device.
# Device is something like /dev/sda1.
blkid_label_of_device() {
    local device=$1
    local label=""
    for LINE in $(blkid $device  2>/dev/null)
    do
        label=$( echo "$LINE" | grep "^LABEL=" | cut -d= -f2 | sed -e 's/"//g' | sed -e 's/ /\\\\b/g')  # replace all " " with "\\b"
        [[ ! -z "$label" ]] && break
    done
    echo "$label"
}

# Returns true if the device is an LVM physical volume
# Returns false otherwise or if the device doesn't exist
is_disk_a_pv() {
    disk=$1

    # Using awk, select the 'lvmdev' line for which $disk is the device (column 3),
    # cf. https://github.com/rear/rear/pull/1897
    # If exit == 1, then there is such line (so $disk is a PV),
    # otherwise exit with default value '0', which falls through to 'return 1' below.
    awk "\$1 == \"lvmdev\" && \$3 == \"${disk}\" { exit 1 }" "$LAYOUT_FILE" >/dev/null || return 0
    return 1
}

# Check whether disk is suitable for being added to layout
# Can be used to skip obviously unsuitable/broken devices
# (missing device node, zero size, device can't be opened).
# Should not be used to skip potential mapping targets before layout restoration
# - an invalid disk may become valid later, for example if it is a DASD that needs
# low-level formatting (see 090_include_dasd_code.sh and 360_generate_dasd_format_code.sh),
# unformatted DASDs show zero size.
# Returns 0 if the device is ok
# Returns nonzero code if it should be skipped, and a text describing the error
# on stdout
# usage example:
# local err
# if ! err=$(is_disk_valid /dev/sda); then

function is_disk_valid {
    local disk="$1"
    local size

    if ! test -b "$disk" ; then
        echo "$disk is not a block device"
        return 1
    fi
    # capture stdout in a variable and redirect stderr to stdout - the error message
    # will be our output
    if { size=$(blockdev --getsize64 "$disk") ; } 2>&1 ; then
        if ! test "$size" -gt 0 2>/dev/null ; then
            echo "$disk has invalid size $size"
            return 1
        fi
        return 0
    else
        return 1
    fi
}

function is_multipath_used {
    # Return 'false' if there is no multipath command:
    type multipath &>/dev/null || return 1
    # 'multipath -l' is the only simple and reliably working command
    # to find out in general whether or not multipath is used at all.
    # But 'multipath -l' scans all devices and the time it takes is proportional
    # to their number so that time would become rather long (seconds up to minutes)
    # if 'multipath -l' was called for each one of hundreds or thousands of devices.
    # So we call 'multipath -l' only once and remember the result
    # in a global variable and then only use that global variable
    # so we can call is_multipath_used very many times as often as needed.
    is_true $MULTIPATH_IS_USED && return 0
    is_false $MULTIPATH_IS_USED && return 1
    # When MULTIPATH_IS_USED has neither a true nor false value set it and return accordingly.
    # Because "multipath -l" always returns zero exit code we check if it has real output via grep -q '[[:alnum:]]'
    # so that no "multipath -l" output could clutter the log (the "multipath -l" output is irrelevant here)
    # in contrast to e.g. test "$( multipath -l )" that would falsely succeed with blank output
    # and the output would appear in the log in 'set -x' debugscript mode:
    if multipath -l | grep -q '[[:alnum:]]' ; then
        MULTIPATH_IS_USED='yes'
        return 0
    else
        MULTIPATH_IS_USED='no'
        return 1
    fi
}

function is_multipath_path {
    # Return 'false' if there is no device as argument:
    test "$1" || return 1
    # Return 'false' if multipath is not used, see https://github.com/rear/rear/issues/2298
    is_multipath_used || return 1
    # Check if a block device should be a path in a multipath device:
    multipath -c /dev/$1 &>/dev/null
}

# retry_command () is binded with REAR_SLEEP_DELAY and REAR_MAX_RETRIES.
# This function will do maximum of REAR_MAX_RETRIES command execution
# and will sleep REAR_SLEEP_DELAY after each unsuccessful command execution.
# It outputs command stdout if succeeded or returns 1 on failure.
retry_command ()
{
    local retry=0

    until command_stdout=$(eval "$@"); do
        sleep $REAR_SLEEP_DELAY

        let retry++

        if (( retry >= REAR_MAX_RETRIES )) ; then
            Log "retry_command '$*' failed"
            return 1
        fi
    done
    # Have no additional trailing newline for the command stdout:
    echo -n "$command_stdout"
}

# UdevSymlinkName (device) return all the udev symlink created by udev to the device.
# example:
# UdevSymlinkName /dev/sda1
#  /dev/disk/by-id/ata-SAMSUNG_MZNLN512HMJP-000L7_S2XANX0H603095-part1 /dev/disk/by-id/wwn-0x5002538d00000000-part1 /dev/disk/by-label/boot /dev/disk/by-partuuid/7d51513d-01 /dev/disk/by-path/pci-0000:00:17.0-ata-1-part1 /dev/disk/by-uuid/b3c0fd92-28cf-4591-b4f5-1a32913f4319
function UdevSymlinkName() {
    unset device
    device="$1"

    # Exit with Error if no argument is provided to UdevSymlinkName
    contains_visible_char "$device" || Error "Empty string passed to UdevSymlinkName()"

    # udevinfo is deprecated by udevadm (SLES 10 still uses udevinfo)
    type -p udevinfo >/dev/null && UdevSymlinkName="udevinfo -r / -q symlink -n"
    type -p udevadm >/dev/null && UdevSymlinkName="udevadm info --root --query=symlink --name"

    if test -z "$UdevSymlinkName" ; then
        LogPrint "Could not find udevinfo nor udevadm. UdevSymlinkName($device) failed."
        return 1
    fi

    $UdevSymlinkName $device
}

# UdevQueryName (device) return all the real device name from udev symlink.
# example:
# UdevQueryName /dev/disk/by-id/wwn-0x5002538d00000000-part1
#  sda1
# WARNING: like udevadm, this function return device name (sda1) not absolute PATH (/dev/sda1)
function UdevQueryName() {
    unset device_link
    device_link="$1"

    # Exit with Error if no argument is provided to UdevSymlinkName
    contains_visible_char "$device_link" || Error "Empty string passed to UdevQueryName()"

    # be careful udevinfo is old, now we have udevadm
    # udevinfo -r -q name -n /dev/disk/by-id/scsi-360060e8015268c000001268c000065c0-part4
    # udevadm info --query=name --name /dev/disk/by-id/dm-name-vg_fedora-lv_root
    type -p udevinfo >/dev/null && UdevQueryName="udevinfo -r -q name -n"
    type -p udevadm >/dev/null && UdevQueryName="udevadm info --query=name --name"

    if test -z "$UdevQueryName" ; then
        LogPrint "Could not find udevinfo nor udevadm. UdevQueryName($device_link) failed."
        return 1
    fi

    $UdevQueryName $device_link
}

# Guess the part device name from a device, based on the OS distro Level.
# See https://github.com/rear/rear/commit/14c062627e15d3be799b1a8bd220634a0aa032b9
# which belongs to https://github.com/rear/rear/pull/1450
function get_part_device_name_format() {
    if [ -z "$1" ] ; then
        BugError "get_part_device_name_format function called without argument (device)"
    else
        device_name="$1"
    fi

    part_name="$device_name"

    case "$device_name" in
        (*mmcblk[0-9]*|*nvme[0-9]*n[1-9]*|*rd[/!]c[0-9]*d[0-9]*|*cciss[/!]c[0-9]*d[0-9]*|*ida[/!]c[0-9]*d[0-9]*|*amiraid[/!]ar[0-9]*|*emd[/!][0-9]*|*ataraid[/!]d[0-9]*|*carmel[/!][0-9]*)
            part_name="${device_name}p" # append p between main device and partitions
            ;;
        (*mapper[/!]*)
            # Every Linux distribution / version has their own rule to name the multipthed partition device.
            #
            # Suse:
            #     Version <12 : always <device>_part<part_num> (same with/without user_friendly_names)
            #     Version >=12 : always <device>-part<part_num> (same with/without user_friendly_names)
            #     Question still open for sles10 ...
            # RedHat:
            #     Version <7 : always <device>p<part_num> (same with/without user_friendly_names)
            #     Version >=7 : if user_friendly_names (default) <device><part_num> else <device>p<part_num>
            # Debian:
            #     if user_firendly_names (default) <device>-part<part_num>
            #     if NOT user_firendly_names <device>p<part_num>
            #

            # First we need to know if user_friendly_names is activated (for Fedora/RedHat and Debian/ubuntu)
            if multipathd ; then
                # check if multipath if using the "user_friendly_names" by default in the current configuration.
                user_friendly_names=$(echo "show config" | multipathd -k | awk '/user_friendly_names/ { gsub("\"","") ; print $2 }' | head -n 1 )
            fi

            case $OS_MASTER_VENDOR in

                (SUSE)
                    # No need to check if user_friendly_names is activated or not as Suse always apply the same naming convention.

                    # SUSE Linux SLE12 put a "-part" between [mpath device name] and [part number].
                    # For example /dev/mapper/3600507680c82004cf8000000000000d8-part1.
                    # But SLES11 uses a "_part" instead. (Let's assume it is the same for SLES10 )
                    if (( $OS_MASTER_VERSION < 12 )) ; then
                        # For SUSE before version 12
                        part_name="${device_name}_part" # append _part between main device and partitions
                    else
                        # For SUSE 12 or above
                        part_name="${device_name}-part" # append -part between main device and partitions
                    fi
                ;;

                (Fedora)
                    # RHEL 7 and above add a "p" after the device name when the device name ends
                    # by a digit, see https://access.redhat.com/solutions/2354631.
                    if (( $OS_MASTER_VERSION < 7 )) || [[ ${device_name: -1} =~ [0-9] ]]; then
                        part_name="${device_name}p"
                    fi
                ;;

                (Debian)
                    if is_false "$user_friendly_names" ; then
                        # Exceptional case for Debian/ubuntu
                        # When user_friendly_names is disable, debian based system will name partition
                        # [mpath device UUID/WWID] + p + [part number]
                        part_name="${device_name}p"
                    else
                        # Default case (user_friendly_name enable)
                        # Ubuntu 16.04 (need to check for other version) named muiltipathed partitions with
                        # [mpath device name] + "-part" + [part number]
                        # for example : /dev/mapper/mpatha-part1
                        part_name="${device_name}-part" # append -part between main device and partitions
                    fi
                ;;

                (*)
                    # For all the other case, use /dev/mapper/mpatha1 type
                    part_name="$device_name"
                ;;
            esac
        ;;
    esac

    echo "$part_name"
}

# The is_completely_identical_layout_mapping function checks
# if there is a completely identical mapping in the mapping file
# (usually $MAPPING_FILE is /var/lib/rear/layout/disk_mappings)
# which is used to avoid that files (in particular restored files)
# may get needlessly touched and modified for identical mappings
# see https://github.com/rear/rear/issues/1847
function is_completely_identical_layout_mapping() {
    # MAPPING_FILE is set in layout/prepare/default/300_map_disks.sh
    # only if MIGRATION_MODE is true.
    # When $MAPPING_FILE is empty the below command
    #   grep -v '^#' "$MAPPING_FILE"
    # would hang up endlessly without user notification
    # because that command would become
    #   grep -v '^#'
    # which reads from stdin (i.e. from the user's keyboard).
    # A non-existent mapping file is considered to be a completely identical mapping
    # (i.e. 'no mapping' means 'do not change anything' which is the identity map).
    test -f "$MAPPING_FILE" || return 0
    # Only non-commented and syntactically valid lines in the mapping file count
    # so that also an empty mapping file or when there is not at least one valid mapping
    # are considered to be completely identical mappings
    # (i.e. 'no valid mapping' means 'do not change anything' which is the identity map):
    while read source target junk ; do
        # Skip lines that have wrong syntax:
        test "$source" -a "$target" || continue
        test "$source" != "$target" && return 1
    done < <( grep -v '^#' "$MAPPING_FILE" )
    Log "Completely identical layout mapping in $MAPPING_FILE"
    return 0
}

# apply_layout_mappings function migrate disk device references
# from an old system and replace them with new ones (from current system).
# The relationship between OLD and NEW device is provided by the mapping file
# (usually $MAPPING_FILE is /var/lib/rear/layout/disk_mappings).
function apply_layout_mappings() {
    local file_to_migrate="$1"

    # Exit if MIGRATION_MODE is not true.
    is_true "$MIGRATION_MODE" || return 0

    # apply_layout_mappings needs one argument:
    test "$file_to_migrate" || BugError "apply_layout_mappings function called without argument (file_to_migrate)."

    # Only apply layout mapping on non-empty file:
    test -s "$file_to_migrate" || return 0

    # Do not apply layout mappings when there is a completely identical mapping in the mapping file.
    # This test is run for each call of the apply_layout_mappings function because
    # in MIGRATION_MODE there are several user dialogs during "rear recover" where
    # the user can run the ReaR shell and edit the mapping file as he likes:
    is_completely_identical_layout_mapping && return 0

    # Generate unique words (where unique means that those generated words cannot already exist in file_to_migrate)
    # as replacement placeholders to correctly handle circular replacements e.g. for "sda -> sdb and sdb -> sda"
    # in the mapping file those generated unique words would be _REAR0_ for sda and _REAR1_ for sdb.
    # The replacement strategy is:
    # Step 0:
    # For each original device in the mapping file generate a unique word (the "replacement").
    # Step 1:
    # In file_to_migrate replace (temporarily) all original devices with their matching unique word.
    # E.g. "disk sda and disk sdb" would become "disk _REAR0_ and disk _REAR1_" temporarily in file_to_migrate.
    # Step 2:
    # In file_to_migrate re-replace all unique replacement words with the matching target device of the source device.
    # E.g. for "sda -> sdb and sdb -> sda" in the mapping file and the unique words _REAR0_ for sda and _REAR1_ for sdb
    # "disk _REAR0_ and disk _REAR1_" would become "disk sdb and disk sda" in the final file_to_migrate
    # so that the circular replacement "sda -> sdb and sdb -> sda" is done in file_to_migrate.
    # Step 3:
    # In file_to_migrate verify that there are none of those temporary replacement words from step 1 left
    # to ensure the replacement was done correctly and completely.

    # Replacement_file initialization.
    replacement_file="$TMP_DIR/replacement_file"
    : > "$replacement_file"

    function add_replacement() {
        # We temporarily map all devices in the mapping to new names _REAR[0-9]+_
        echo "$1 _REAR${replacement_count}_" >> "$replacement_file"
        let replacement_count++
    }

    function has_replacement() {
        grep -q "^$1 " "$replacement_file"
    }

    function get_replacement() {
        local item replacement junk
        read item replacement junk < <( grep "^$1 " $replacement_file )
        test "$replacement" && echo "$replacement" || return 1
    }

    # Step 0:
    # For each original device in the mapping file generate a unique word (the "replacement").
    # E.g. when the mapping file content is
    #   /dev/sda /dev/sdb
    #   /dev/sdb /dev/sda
    #   /dev/sdd /dev/sdc
    # the replacement file will contain
    #   /dev/sda _REAR0_
    #   /dev/sdb _REAR1_
    #   /dev/sdd _REAR2_
    #   /dev/sdc _REAR3_
    replacement_count=0
    while read source target junk ; do
        # Skip lines that have wrong syntax:
        test "$source" -a "$target" || continue
        has_replacement "$source" || add_replacement "$source"
        has_replacement "$target" || add_replacement "$target"
    done < <( grep -v '^#' "$MAPPING_FILE" )

    # Step 1:
    # Replace all original devices with their replacements.
    # E.g. when the file_to_migrate content is
    #   disk /dev/sda
    #   disk /dev/sdb
    #   disk /dev/sdc
    #   disk /dev/sdd
    # it will get temporarily replaced (with the replacement file content in step 0 above) by
    #   disk _REAR0_
    #   disk _REAR1_
    #   disk _REAR3_
    #   disk _REAR2_
    while read original replacement junk ; do
        # Skip lines that have wrong syntax:
        test "$original" -a "$replacement" || continue
        # Replace partitions with unique replacement words:
        # For example we normalize /dev/cciss/c0d1p2 to something like _REAR5_2
        # (therein the '5' is arbitrary but the '2' is the actual partition number).
        # Due to multipath partition naming complexity, all known partition naming types for example
        # /dev/mapper/mpatha4 /dev/mapper/mpathap4 /dev/mapper/mpatha-part4 /dev/mapper/mpatha_part4
        # are replaced by the same normalized replacement word like _REAR7_4
        # (therein the '7' is arbitrary but the '4' is the actual partition number)
        # that is then in step 2 re-replaced with the right partition naming scheme
        # via the get_part_device_name_format() function,
        # cf. https://github.com/rear/rear/pull/1765
        # Because $original (e.g. /dev/sda1) contains slashes sed '/regexp/' cannot be used
        # so sed '\|regexp|' is used (under the assumption that no | characters are in $original):
        sed -i -r "\|$original|s|$original(p)*([-_]part)*([0-9]+)|$replacement\3|g" "$file_to_migrate"
        # Replace whole devices with unique replacement words:
        # Note that / is a word boundary, so is matched by \<, hence the extra /
        sed -i -r "\|$original|s|/\<${original#/}\>|${replacement}|g" "$file_to_migrate"
    done < "$replacement_file"

    # Step 2:
    # Re-replace all unique replacement words with the matching target device of the source device in the mapping file.
    # E.g. when the file_to_migrate content was in step 1 above temporarily changed to
    #   disk _REAR0_
    #   disk _REAR1_
    #   disk _REAR3_
    #   disk _REAR2_
    # it will now get finally re-replaced (with the replacement file and mapping file contents in step 0 above) by
    #   disk /dev/sdb
    #   disk /dev/sda
    #   disk _REAR3_
    #   disk /dev/sdc
    # where the temporary replacement "disk _REAR3_" from step 1 above is left because
    # there is (erroneously) no mapping for /dev/sdc (as source device) in the mapping file (in step 0 above).
    while read source target junk ; do
        # Skip lines that have wrong syntax:
        test "$source" -a "$target" || continue
        # Skip when there is no replacement:
        replacement=$( get_replacement "$source" ) || continue
        # Re-replace whole devices:
        # Use the same sed '\|regexp|s...' syntax as in step 1 above also here to be on the safe side
        # (there are no slashes in $replacement like '_REAR0_' but $target like '/dev/sda' contains slashes):
        sed -i -r "\|$replacement|s|$replacement\>|$target|g" "$file_to_migrate"
        # Re-replace partitions:
        target=$( get_part_device_name_format "$target" )
        sed -i -r "\|$replacement|s|$replacement([0-9]+)|$target\1|g" "$file_to_migrate"
    done < <( grep -v '^#' "$MAPPING_FILE" )

    # Step 3:
    # Verify that there are none of those temporary replacement words from step 1 left in file_to_migrate
    # to ensure the replacement was done correctly and completely (cf. the above example where '_REAR3_' is left).
    apply_layout_mappings_succeeded="yes"
    while read original replacement junk ; do
        # Skip lines that have wrong syntax:
        test "$original" -a "$replacement" || continue
        # Only treat leftover temporary replacement words as an error
        # if they are in a non-comment line (comments have '#' as first non-space character)
        # cf. https://github.com/rear/rear/issues/2183
        if grep -v '^[[:space:]]*#' "$file_to_migrate" | grep -q "$replacement" ; then
            apply_layout_mappings_succeeded="no"
            LogPrintError "Failed to apply layout mappings to $file_to_migrate for $original (probably no mapping for $original in $MAPPING_FILE)"
        fi
    done < "$replacement_file"
    # It is the responsibility of the caller of this apply_layout_mappings function what to do when it failed
    # (e.g. error out, retry, show a user dialog, or whatever is appropriate in the caller's environment):
    is_true $apply_layout_mappings_succeeded && return 0 || return 1
}

has_binary parted || Error "Cannot find 'parted' command"

FEATURE_PARTED_RESIZEPART=y
FEATURE_PARTED_RESIZE=n

if parted --help | awk '$1 == "resizepart" { exit 1 }' ; then
    # No 'resizepart', check for 'resize'
    FEATURE_PARTED_RESIZEPART=n
    if ! parted --help | awk '$1 == "resize" { exit 1 }' ; then
        FEATURE_PARTED_RESIZE=y
        if ! parted --help | awk '$1 == "resize" && $3 == "START" { exit 1 }' ; then
            # 'parted resize NUM START END' tries resizing the file system,
            # which is known to fail, as shown below (output from parted):
            #
            # # parted -s -m /dev/sdc resize 3 1074790400B 2149580799B
            # WARNING: you are attempting to use parted to operate on (resize) a file system.
            # parted's file system manipulation code is not as robust as what you'll find in
            # dedicated, file-system-specific packages like e2fsprogs.  We recommend
            # you use parted only to manipulate partition tables, whenever possible.
            # Support for performing most operations on most types of file systems
            # will be removed in an upcoming release.
            # No Implementation: Support for opening ext4 file systems is not implemented yet.
            FEATURE_PARTED_RESIZE=n
        fi
    fi
fi

# Keeps track of the current disk being processed
# e.g. /dev/sdb
current_disk=""

# Keeps track of last partition created for the current disk
# e.g. last_partition_number=1
last_partition_number=0

# Keeps track of dummy partitions created and to be removed (due to parted limitation) for the current disk
# Contains a list of partition numbers
# e.g. dummy_partitions_to_delete=( 1 2 4 5 )
dummy_partitions_to_delete=()

# Keeps track of partitions to resize to original size for the current disk
# Contains a list of partition tuples (number, end_in_bytes)
# e.g. partitions_to_resize=( 3 2096127 6 8388607 )
partitions_to_resize=()

# Keeps track of the label for the current disk
# e.g. disk_label="gpt"
disk_label=""

#
# create_disk_label(disk, label)
#
# Sets up the disk label. Must be called before calling create_disk_partition().
#
create_disk_label() {
    local disk="$1" label="$2"

    if [[ "$current_disk" ]] && [[ "$current_disk" != "$disk" ]] ; then
        BugError "Current disk has changed from '$current_disk' to '$disk' without calling delete_dummy_partitions_and_resize_real_ones() first."
    fi
    current_disk="$disk"

    if [[ "$disk_label" ]] && [[ "$disk_label" != "$label" ]] ; then
        BugError "Disk '$disk': disk label is being assigned multiple times for the same disk."
    fi
    disk_label="$label"

    LogPrint "Disk '$disk': creating '$label' partition table"
    parted -s $disk mklabel $label
    my_udevsettle
}

#
# create_disk_partition(disk, name, partnumber, partstart, [partend])
#
# Creates a partition. When changing disk, user must call delete_dummy_partitions_and_resize_real_ones().
#
create_disk_partition() {
    local disk="$1" name="$2" number=$3 startB=$4 endB=$5

    #
    # FIXME? This code assumes that "parted" is capable of handling sizes in
    # Bytes. parted supports partitions in Bytes since ages.
    #

    if [[ "$current_disk" ]] && [[ "$current_disk" != "$disk" ]] ; then
        BugError "Current disk has changed from '$current_disk' to '$disk' without calling delete_dummy_partitions_and_resize_real_ones() first."
    fi
    current_disk="$disk"

    if [[ ! "$disk_label" ]] ; then
        BugError "Disk '$disk': disk label is unknown."
    fi

    # The duplicated quoting "'$name'" is there because
    # parted's internal parser needs single quotes for values with blanks.
    # In particular a GPT partition name that can contain spaces
    # like 'EFI System Partition' cf. https://github.com/rear/rear/issues/1563
    # so that when calling parted on command line it must be done like
    #    parted -s /dev/sdb unit MiB mkpart "'partition name'" 12 34
    # where the outer quoting "..." is for bash so that
    # the inner quoting '...' is preserved for parted's internal parser:
    [ "$disk_label" == "msdos" ] || name="'$name'"

    if [[ $number -le last_partition_number ]] ; then
        Error "Disk '$disk': trying to create partition number $number but last created partition was number $last_partition_number"
    fi

    if [[ $(( $number - $last_partition_number - 1 )) -eq 0 ]] ; then
        LogPrint "Disk '$disk': creating partition number $number with name '$name'"
        # FIXME: I <jsmeix@suse.de> think one cannot silently set the end of a partition to 100%
        # if there is no partition end value, I think in this case "rear recover" should error out:
        if [[ ! $endB ]] ; then
            parted -s $disk mkpart "$name" "${startB}B" 100%
        else
            parted -s $disk mkpart "$name" "${startB}B" "${endB}B"
        fi
        my_udevsettle
        last_partition_number=$number
        return 0
    fi

    if is_false $FEATURE_PARTED_RESIZEPART && is_false $FEATURE_PARTED_RESIZE ; then
        Error "Disk '$disk': trying to create partition number $number which isn't consecutive with previous partition but 'parted' doesn't support this feature"
    fi

    # "parted" is only capable of creating partitions consecutively. Since
    # there is a gap between the previous partition and this partition, dummy
    # partitions must be created, and later will be removed once disks are
    # fully partitioned (this is done in
    # delete_dummy_partitions_and_resize_real_ones()).
    #
    # Once the dummy partitions are removed, the real partition needs to be
    # resized to original size.
    # Unfortunately, "parted" is only capable of resizing a partition toward
    # its end, not the beginning, so the only way to create dummy partitions
    # appropriately is to use some space at the end of the real partition.
    #
    # Example: the GPT disk contains only 1 partition numbered 3 named PART
    #
    # # parted -m -s /dev/sda unit B print
    # /dev/sda:21474836480B:scsi:512:512:gpt:QEMU QEMU HARDDISK:;
    # 3:1048576B:2097151B:1048576B::PART:;
    #
    # We need to create these partitions to recreate the exact same partitions:
    #
    # # parted -m -s /dev/sda unit B print
    # /dev/sda:21474836480B:scsi:512:512:gpt:QEMU QEMU HARDDISK:;
    # 3:1048576B:2096127B:1047552B::PART:;
    # 2:2096128B:2096639B:512B::dummy2:;
    # 1:2096640B:2097151B:512B::dummy1:;
    #
    # Then delete dummy partition 1 and 2 and resize 3 to its original end.
    #
    # For 'msdos' disks, only non-logical partitions are affected.

    if [[ ! $endB ]] ; then
        # We need to compute '$endB' based on disk size. To do so, create a
        # partition then get its end
        LogPrint "Disk '$disk': creating a temporary partition to find out the end of the disk"
        parted -s -m $disk mkpart "$name" ${startB}B 100%
        local num
        read num endB <<< $( parted -s -m $disk unit B print | tail -1 | awk -F ':' '{ print $1, $3 }' | sed 's/\([0-9]*\)B$/\1/' )
        LogPrint "Disk '$disk': last allocatable byte on disk is '$endB'"
        LogPrint "Disk '$disk': deleting the temporary partition number $num"
        parted -s -m $disk rm $num
    fi

    partitions_to_resize+=( $number $endB )

    local -i logical_sector_size=$( parted -m -s $disk unit B print | awk -F ':' "\$1 == \"$disk\" { print \$4 }" )

    if [[ "$disk_label" != "msdos" ]] || [[ "$name" != "logical" ]] ; then
        local -i i=$last_partition_number+1
        while [[ $i -lt $number ]] ; do
            local partname="dummy$i"
            [[ "$disk_label" != "msdos" ]] || partname="primary"
            dummy_partitions_to_delete+=( $i )
            LogPrint "Disk '$disk': creating dummy partition number $i with name '$partname' (will be deleted later)"
            parted -s -m $disk mkpart "$partname" "${endB}B" "${endB}B"
            my_udevsettle
            let endB-=$logical_sector_size
            if [[ $endB -lt $startB ]] ; then
                # Not enough space left for real partition: this happens if the
                # partition to create is very small and we cannot create dummy
                # temporary partitions!
                # e.g. Partition table starts at partition number 2 and partition 2
                # has only 1 cylinder
                Error "Disk '$disk': Cannot create partition number $number, the partition is too small to create dummy partitions to work around parted's limitation (parted can only create consecutive partitions)"
            fi
            let i++
        done
    fi

    LogPrint "Disk '$disk': creating partition number $number with name '$name'"
    parted -s $disk mkpart "$name" "${startB}B" "${endB}B"
    my_udevsettle

    last_partition_number=$number
}

#
# delete_dummy_partitions_and_resize_real_ones()
#
# When current disk has non-consecutive partitions, delete temporary partitions
# that have been created and resize the temporary shrunk partitions to their
# expected size.
#
delete_dummy_partitions_and_resize_real_ones() {
    # If parted doesn't support resizing, this is a no-op function
    # (dummy_partitions_to_delete will be empty).
    if [[ ${#dummy_partitions_to_delete[@]} -eq 0 ]] ; then
        partitions_to_resize=()
        current_disk=""
        disk_label=""
        last_partition_number=0
        return 0
    fi

    # Delete dummy partitions
    local -i num
    for num in "${dummy_partitions_to_delete[@]}" ; do
        LogPrint "Disk '$current_disk': deleting dummy partition number $num"
        parted -s -m $current_disk rm $num
    done
    dummy_partitions_to_delete=()
    my_udevsettle

    # Resize previously shrunk partitions (to make place for dummy
    # partitions) to expected size
    local -i endB
    while read num endB ; do
        LogPrint "Disk '$current_disk': resizing partition number $num to original size"
        if is_true $FEATURE_PARTED_RESIZEPART ; then
            parted -s -m $current_disk resizepart $num "${endB}B"
        else
            parted -s -m $current_disk resize $num "${endB}B"
        fi
    done <<< "$(printf "%d %d\n" "${partitions_to_resize[@]}")"
    partitions_to_resize=()
    my_udevsettle

    current_disk=""
    disk_label=""
    last_partition_number=0
}

# vim: set et ts=4 sw=4: