Skip to content

Instantly share code, notes, and snippets.

@n0ctu
Created May 10, 2026 09:59
Show Gist options
  • Select an option

  • Save n0ctu/ec735b103f9c993d93c90154d1e665c2 to your computer and use it in GitHub Desktop.

Select an option

Save n0ctu/ec735b103f9c993d93c90154d1e665c2 to your computer and use it in GitHub Desktop.
Identify corrupted files in NFTS images using a gddrescue map-file
#!/usr/bin/env bash
#
# find_affected_files.sh
#
# Identifies files affected by bad sectors in a ddrescue rescue.
# Reads the ddrescue mapfile, locates each bad region within the image's
# partitions, and uses ntfscluster (for NTFS) to map sectors to file paths.
#
# Usage: sudo ./find_affected_files.sh
#
# Requires: parted, ntfs-3g (provides ntfscluster), awk, bc
set -euo pipefail
# ---- Configuration -----------------------------------------------------------
IMAGE="/run/media/n0c/HDD07/sdb.img"
MAPFILE="/home/n0c/temp/sdb_restore.log"
OUTPUT="/home/n0c/temp/affected_files.log"
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
# ---- Sanity checks -----------------------------------------------------------
if [[ $EUID -ne 0 ]]; then
echo "ERROR: must be run as root (uses losetup and reads block devices)" >&2
exit 1
fi
for cmd in parted ntfscluster losetup awk; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "ERROR: required command not found: $cmd" >&2
echo " Install with: sudo apt install parted ntfs-3g util-linux" >&2
exit 1
fi
done
[[ -f "$IMAGE" ]] || { echo "ERROR: image not found: $IMAGE" >&2; exit 1; }
[[ -f "$MAPFILE" ]] || { echo "ERROR: mapfile not found: $MAPFILE" >&2; exit 1; }
echo "Image: $IMAGE"
echo "Mapfile: $MAPFILE"
echo "Output: $OUTPUT"
echo
# ---- Parse partition layout from the image -----------------------------------
echo "=== Partition layout ==="
# parted -m gives machine-readable output: BYTE units, colon-separated
# Format: NUMBER:START:END:SIZE:FILESYSTEM:NAME:FLAGS
PARTED_OUT="$(parted -m -s "$IMAGE" unit B print 2>/dev/null || true)"
echo "$PARTED_OUT"
echo
# Extract partition entries (skip header lines starting with BYT: or the image path)
# Build an array: "num start end fstype"
declare -a PART_NUM PART_START PART_END PART_FSTYPE
while IFS=: read -r num start end size fstype name flags; do
[[ -z "$num" || "$num" == "BYT" ]] && continue
[[ "$num" =~ ^[0-9]+$ ]] || continue
# Strip trailing 'B' from byte values
start="${start%B}"
end="${end%B}"
PART_NUM+=("$num")
PART_START+=("$start")
PART_END+=("$end")
PART_FSTYPE+=("$fstype")
done < <(echo "$PARTED_OUT")
if [[ ${#PART_NUM[@]} -eq 0 ]]; then
echo "ERROR: no partitions detected in image" >&2
exit 1
fi
# ---- Set up loop device for the image ----------------------------------------
echo "=== Setting up loop device ==="
LOOPDEV="$(losetup --find --show --partscan --read-only "$IMAGE")"
echo "Image attached as $LOOPDEV"
echo "Partitions:"
ls "${LOOPDEV}"p* 2>/dev/null || true
echo
cleanup_loop() { losetup -d "$LOOPDEV" 2>/dev/null || true; }
trap 'cleanup_loop; rm -rf "$WORKDIR"' EXIT
# ---- Extract bad regions from the mapfile ------------------------------------
# Mapfile data lines: <offset_hex> <size_hex> <status>
# Status '-' = bad-sector. We also include '/' (non-trimmed) and '*' (non-scraped)
# in case any remain, though after a completed run these are usually all '-' or '+'.
BAD_REGIONS="$WORKDIR/bad_regions.txt"
awk '
/^[[:space:]]*#/ { next }
NF >= 3 && $1 ~ /^0x/ && ($3 == "-" || $3 == "/" || $3 == "*") {
# Convert hex to decimal
offset = strtonum($1)
size = strtonum($2)
printf "%d %d %s\n", offset, size, $3
}
' "$MAPFILE" > "$BAD_REGIONS"
BAD_COUNT="$(wc -l < "$BAD_REGIONS")"
echo "=== Bad regions in mapfile: $BAD_COUNT ==="
if [[ "$BAD_COUNT" -eq 0 ]]; then
echo "No bad regions found. Nothing to map."
exit 0
fi
echo
# ---- For each bad region, find owning partition + map to file ----------------
> "$OUTPUT"
{
echo "# Files affected by bad sectors"
echo "# Image: $IMAGE"
echo "# Mapfile: $MAPFILE"
echo "# Generated: $(date -Is)"
echo "#"
} >> "$OUTPUT"
declare -A SEEN_FILES # dedup: path -> 1
UNMAPPED=0
OUTSIDE_PART=0
NON_NTFS=0
# Process each bad region
while read -r offset size status; do
end_offset=$(( offset + size - 1 ))
# Find the partition containing this offset
matched_part=""
matched_idx=-1
for i in "${!PART_NUM[@]}"; do
ps="${PART_START[$i]}"
pe="${PART_END[$i]}"
if (( offset >= ps && offset <= pe )); then
matched_part="${PART_NUM[$i]}"
matched_idx=$i
break
fi
done
if [[ -z "$matched_part" ]]; then
OUTSIDE_PART=$(( OUTSIDE_PART + 1 ))
continue
fi
fstype="${PART_FSTYPE[$matched_idx]}"
part_dev="${LOOPDEV}p${matched_part}"
if [[ "$fstype" != "ntfs" ]]; then
NON_NTFS=$(( NON_NTFS + 1 ))
continue
fi
[[ -b "$part_dev" ]] || { echo "WARN: partition device missing: $part_dev" >&2; continue; }
# Offset within the partition (in bytes), then convert to a 512-byte sector
part_start="${PART_START[$matched_idx]}"
rel_offset=$(( offset - part_start ))
sector=$(( rel_offset / 512 ))
# ntfscluster --sector reports inode + filename for the given sector.
# We sample the first sector of the bad region; bad regions are typically
# small (one or a few sectors) and the file mapping won't change within them.
if result="$(ntfscluster --sector "$sector" "$part_dev" 2>/dev/null)"; then
# Extract any "Inode X /path/to/file" lines
while IFS= read -r line; do
# Match lines like: "Inode 12345 /Users/foo/bar.txt"
if [[ "$line" =~ ^Inode\ [0-9]+\ (.+)$ ]]; then
path="${BASH_REMATCH[1]}"
if [[ -z "${SEEN_FILES[$path]:-}" ]]; then
SEEN_FILES["$path"]=1
echo "$path" >> "$OUTPUT"
fi
fi
done <<< "$result"
else
UNMAPPED=$(( UNMAPPED + 1 ))
fi
done < "$BAD_REGIONS"
# ---- Summary -----------------------------------------------------------------
UNIQUE_COUNT="${#SEEN_FILES[@]}"
echo
echo "=== Summary ==="
echo "Bad regions processed: $BAD_COUNT"
echo "Outside any partition: $OUTSIDE_PART (likely free space / partition gaps)"
echo "On non-NTFS partitions: $NON_NTFS (e.g., EFI, recovery)"
echo "Could not be mapped to file: $UNMAPPED (likely free space within partition)"
echo "Unique affected files: $UNIQUE_COUNT"
echo
echo "Result written to: $OUTPUT"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment