Skip to content

Instantly share code, notes, and snippets.

@oficsu
Last active October 1, 2025 09:54
Show Gist options
  • Select an option

  • Save oficsu/3a8cfccfda450a5ff610b77a459b0353 to your computer and use it in GitHub Desktop.

Select an option

Save oficsu/3a8cfccfda450a5ff610b77a459b0353 to your computer and use it in GitHub Desktop.
Reassemble selected partitions into a new virtual disk and derive GPT partition table properties from the old one
#!/bin/bash
set -eu
# MIT License
#
# Copyright (c) 2025 Ofee Oficsu
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
SECTOR_SIZE=512
SECTOR_ALIGNMENT=2048
GPT_SIZE=$(($SECTOR_SIZE * 2048))
VERBOSE=false
VDRIVE_NAME=virtual-disk
DISK_GUID_SOURCE=
LOOPS=()
BLOCKS=()
BLOCK_MODES=()
TEMP="$(mktemp -d)"
DMSETUP_CONFIG="$TEMP/dmsetup-config.txt"
info-message() {
echo "$@" 1>&2
}
error() {
info-message "error: reassemble-gpt-drive: $@"
exit 1
}
separate-log-group() {
info-message
if $VERBOSE; then
info-message
fi
}
create-gpt-header() {
local file="$1"
dd if=/dev/zero of="$file" bs=1 count="$GPT_SIZE" status=none
}
get-size() {
declare -n __out_var__="$1"
local file="$2"
if [ -b "$file" ]; then
__out_var__="$(blockdev --getsize64 "$file")"
elif [ -f "$file" ]; then
__out_var__="$(stat --printf="%s" "$file")"
elif [ -e "$file" ]; then
error "get-size: unexpected file type: $file"
else
error "get-size: no such file: $file"
fi
}
add-loop() {
local mode="$1"
local file="$2"
if ! [ -e "$file" ]; then
error "add-loop: no such file: $file"
fi
local losetup_opts=()
if [ "$mode" = rw ]; then
: it is a default
elif [ "$mode" = ro ]; then
losetup_opts+=(--read-only)
else
error "add-loop: unexpected mode for file '$file'"
fi
LOOPS+=("$(losetup "${losetup_opts[@]}" --show --find "$file")")
}
sync-everything() (
sync
for i in {1..3}; do
echo $i > /proc/sys/vm/drop_caches
done
)
newline() {
local i="$1"
if (($i == 0)); then
echo ''
elif $VERBOSE; then
echo '\n'
else
echo '\033[1K\r'
fi
}
print-progress-info() {
local i="$1"
local count="$2"
shift; shift
local newline="$(newline "$i")"
# start from 1
local i="$(($i+1))"
info-message -ne "$newline[$i/$count]" "$@"
}
add-loops() {
local i=0
local count="${#BLOCKS[@]}"
for ((; i < "$count"; i++)); do
local mode="${BLOCK_MODES[$i]}"
local part="${BLOCKS[$i]}"
print-progress-info "$i" "$count" "creating $mode loop device: $part -> "
add-loop "$mode" "$part"
info-message -n "${LOOPS[-1]}"
done
separate-log-group
}
add-alignment() {
local start="$1"
local size="$2"
echo "$start $size zero"
}
add-linear() {
local backing_device="$1"
local start="$2"
local size="$3"
echo "$start $size linear $backing_device 0"
}
foreach-sector() {
local callback="$1"
shift
local begin=0
local count="${#BLOCKS[@]}"
local i=0
for ((; i < "$count"; i++)); do
local extra=0
local unaligned=$(($begin % $SECTOR_ALIGNMENT))
if (($unaligned)); then
extra=$(($SECTOR_ALIGNMENT - $unaligned))
"$callback" "$i" "$begin" -1 "$extra" "$count" "$@"
begin=$(($begin + $extra))
fi
local block="${BLOCKS[$i]}"
local byte_size=
get-size byte_size "$block"
local size="$(($byte_size / $SECTOR_SIZE))"
local next="$(($begin + $size))"
local end=$(($next-1))
"$callback" "$i" "$begin" "$end" "$size" "$count" "$@"
begin=$next
done
}
build-dmconfig() {
local i="$1"
local begin="$2"
local end="$3"
local size="$4"
local count="$5"
local part="${BLOCKS[$i]}"
local loop="${LOOPS[$i]}"
if (($end == -1)); then
print-progress-info "$i" "$count" "adding $size sectors of padding for the alignment"
add-alignment "$begin" "$size"
return
fi
print-progress-info "$i" "$count" "processing raw partition: $part..."
add-linear "$loop" "$begin" "$size"
}
get-prop() {
local part="$1"
local column="$2"
lsblk -n -p -d -o "$column" "$part"
}
clone-attributes() {
local source_part="$1"
local target_part_num="$2"
local target_device="$3"
# it returns a 64-bit hex value, e.g: 0x8000000000000001
local flags="$(get-prop "$source_part" PARTFLAGS 2>/dev/null)"
# do nothing
if [ -z "$flags" ]; then
return
fi
local setters=()
# for each bit number, push a sgdisk setter to the array
for ((bit_num=0; bit_num != 64 ; ++bit_num)); do
if (( ($flags >> $bit_num) & 1 )); then
setters+=(-A "$target_part_num:set:$bit_num")
fi
done
if (( ${#setters[@]} == 0 )); then
return
fi
sgdisk "${setters[@]}" "$target_device" > /dev/null
}
recreate-part() {
local i="$1"
local begin="$2"
local end="$3"
local size="$4"
local count="$5"
local drive_path="$6"
local part="${BLOCKS[$i]}"
# first and last are gpt header and footer, so skip them
if (($i == 0 || $i == ($count-1))); then
return
fi
# ignore paddings
if (($end == -1)); then
return
fi
print-progress-info "$((i-1))" "$((count-2))" "recreating partition at $begin:$end: $part..."
sgdisk --new $i:$begin:$end --set-alignment=$SECTOR_ALIGNMENT "$drive_path" >/dev/null
if ! [ -b "$part" ]; then
return
fi
local type="$(get-prop "$part" PARTTYPE)"
if [ -n "$type" ]; then
sgdisk --typecode="$i:$type" "$drive_path" >/dev/null
fi
local uuid="$(get-prop "$part" PARTUUID)"
if [ -n "$uuid" ]; then
sgdisk --partition-guid="$i:$uuid" "$drive_path" >/dev/null
fi
local label="$(get-prop "$part" PARTLABEL)"
sgdisk --change-name="$i:$label" "$drive_path" >/dev/null
clone-attributes "$part" "$i" "$drive_path"
}
try-cloning-guid() {
local source="$1"
local target="$2"
if [ -z "$source" ]; then
return
fi
if ! [ -b "$source" ]; then
error "can't copy guid information, it's not a block device: $source"
fi
local guid="$(get-prop "$source" PTUUID)"
if $VERBOSE; then
info-message -e "set guid to $guid\n"
fi
sgdisk --disk-guid="$guid" "$target" >/dev/null
}
vdrive-path() {
echo "/dev/mapper/$VDRIVE_NAME"
}
add-part() {
local mode="$1"
local part="$2"
# if part is just a number
if [ "$part" -gt 0 ] 2>/dev/null; then
local num="$part"
if [ -z "$DISK_GUID_SOURCE" ]; then
error "can't add partition $num, --source device was not specified"
fi
if ! [ -b "$DISK_GUID_SOURCE" ]; then
error "can't add partition $num, --source device is not a block device"
fi
part="$(lsblk "$DISK_GUID_SOURCE" --filter "PARTN == $num" --noheadings -o PATH)"
if [ -z "$part" ]; then
error "can't add partition $num, --source device doesn't contain such partition"
fi
fi
BLOCK_MODES+=("$mode")
BLOCKS+=("$part")
}
cleanup() {
if $VERBOSE; then
info-message -e "\n[ - ] starting cleanup..."
fi
if [ -e "$(vdrive-path)" ]; then
kpartx -d "$(vdrive-path)" || true
dmsetup remove "$VDRIVE_NAME" || true
fi
for loop in "${LOOPS[@]}"; do
losetup --detach "$loop" || true
done
rm -rf "$TEMP"
}
trap cleanup EXIT
while [[ $# -gt 0 ]]; do
case "$1" in
--ro)
add-part ro "$2"
shift; shift;;
--rw)
add-part rw "$2"
shift; shift;;
--name|-n)
VDRIVE_NAME="$2"
shift; shift;;
--verbose|-v)
VERBOSE=true
shift;;
--guid-source|--source|-S)
DISK_GUID_SOURCE="$2"
shift; shift;;
-*|--*)
error "unknown option: $1";;
*)
error "this script doesn't accept positional parameters";;
esac
done
if [ -z "$VDRIVE_NAME" ]; then
error "drive name not specified, use --name <name>"
elif [ -e "$(vdrive-path)" ]; then
error "drive with the name $VDRIVE_NAME already exists under /dev/mapper/"
fi
gpt_header="$TEMP/gpt-header"
gpt_footer="$TEMP/gpt-footer"
BLOCKS=( "$gpt_header" "${BLOCKS[@]}" "$gpt_footer")
BLOCK_MODES=( rw "${BLOCK_MODES[@]}" rw )
create-gpt-header "$gpt_header"
create-gpt-header "$gpt_footer"
sync-everything
add-loops
if $VERBOSE; then
echo 1>&2 "[ - ] losetup -a:"
losetup -a 1>&2
echo 1>&2
fi
# calculate paddings, start and end sectors for partitions
foreach-sector build-dmconfig | column -t > "$DMSETUP_CONFIG"
separate-log-group
if $VERBOSE; then
info-message "[ - ] created the following dmsetup config: "
info-message -e "$(cat "$DMSETUP_CONFIG")\n"
fi
sync-everything
# create virtual drive from the config
dmsetup create "$VDRIVE_NAME" < "$DMSETUP_CONFIG"
# create new gpt table
sgdisk --zap-all "$(vdrive-path)" > /dev/null
foreach-sector recreate-part "$(vdrive-path)"
separate-log-group
try-cloning-guid "$DISK_GUID_SOURCE" "$(vdrive-path)"
# create partition mappings: /dev/mapper/device-name{1..N}
kpartx -a "$(vdrive-path)"
sync-everything
echo "[ - ] drive has been created: $(vdrive-path)"
# now wait for the user to cancel the process, the virtual
# drive will be disassembled by the trap handler upon exit
sleep infinity
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment