#!/bin/bash

## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## AI-Assisted

## Rebuild a FAT (EFI System) partition so its bytes are reproducible regardless
## of the build environment.

set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose
export LC_ALL=C

# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/has.bsh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/has.bsh
# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/log_run_die.sh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/log_run_die.sh

if [ "$#" -ne 1 ]; then
   die 2 "usage: ${0##*/} <fat-partition-device-or-image>"
fi

fat_device="$1"
if [ ! -f "${fat_device}" ] && [ ! -b "${fat_device}" ]; then
   die 2 "not found: ${fat_device}"
fi

if [ -z "${SOURCE_DATE_EPOCH:-}" ]; then
   die 2 "SOURCE_DATE_EPOCH is unset; refusing a non-reproducible run."
fi

for tool in mkfs.fat mcopy mmd mdir; do
   if ! has "${tool}"; then
      die 2 "required tool not found: ${tool} (need dosfstools + mtools)"
   fi
done

## mtools must not probe the device geometry: MTOOLS_SKIP_CHECK makes it accept
## a plain image.
export MTOOLS_SKIP_CHECK=1
export TZ=UTC

## No point in exporting MTOOLSRC=/dev/null. mtools overlays the file pointed
## to by this variable on top of other configuration files, it does not
## suppress the use of other files. Overlaying configuration read from
## /dev/null is a no-op.
##
## TODO: Figure out how to keep on-disk configuration from messing things up.
## Perhaps create a configuration file that resets everything to the defaults
## and then point MTOOLSRC at that?

## Read COUNT bytes at OFFSET as a hex string, reversing the byte order so
## little-endian fields can be used for arithmetic trivially.
boot_bytes_le() {
   local offset="$1" count="$2" i out=""
   for (( i = count - 1; i >= 0; i-- )); do
      out+="$( dd if="${fat_device}" bs=1 skip=$(( offset + i )) count=1 2>/dev/null \
         | od --address-radix=n --format=x1 | tr -d ' \n' )"
   done
   printf '%s' "${out}"
}

## Get the device / image file's size in bytes.
if [ -b "${fat_device}" ]; then
   if ! has blockdev; then
      die 2 "${fat_device} is a block device but 'blockdev' (util-linux) is missing."
   fi
   device_size="$( blockdev --getsize64 "${fat_device}" )"
else
   device_size="$( stat --format='%s' "${fat_device}" )"
fi
if [ "${device_size}" -lt 512 ]; then
   die 1 "${fat_device} is too small to be a FAT partition."
fi

## Refuse anything that is not a FAT32 filesystem. FAT12 and FAT16 would need
## to be explicitly supported by this script, and we don't want to do that yet.
##
## Do NOT trust the file system type field. Determine the filesystem type from
## sector and cluster counts.

die_not_fat32() {
   die 1 "${fat_device} is not FAT32, refusing."
}

root_dir_entry_count=$(( 16#$( boot_bytes_le 17 2 ) ))
if [ "${root_dir_entry_count}" -ne 0 ]; then
   die_not_fat32
fi
fat16_fat_sectors=$(( 16#$( boot_bytes_le 22 2 ) ))
if [ "${fat16_fat_sectors}" -ne 0 ]; then
   die_not_fat32
fi
fat32_fat_sectors=$(( 16#$( boot_bytes_le 36 4 ) ))
fat16_volume_sectors=$(( 16#$( boot_bytes_le 19 2 ) ))
if [ "${fat16_volume_sectors}" -ne 0 ]; then
   die_not_fat32
fi
fat32_volume_sectors=$(( 16#$( boot_bytes_le 32 4 ) ))
reserved_sectors=$(( 16#$( boot_bytes_le 14 2 ) ))
fat_count=$(( 16#$( boot_bytes_le 16 1 ) ))
sector_count=$(( fat32_volume_sectors - ( reserved_sectors + ( fat_count * fat32_fat_sectors ) ) ))
sectors_per_cluster=$(( 16#$( boot_bytes_le 13 1 ) ))
cluster_count=$(( sector_count / sectors_per_cluster ))
if ! [ "${cluster_count}" -ge 65525 ]; then
   die_not_fat32
fi

## If we get this far, we're dealing with a FAT32 filesystem.

bytes_per_sector=$(( 16#$( boot_bytes_le 11 2 ) ))
## 'mkfs.fat -i' wants the serial number big-endian.
volume_serial="$( boot_bytes_le 67 4 )"
## The volume label is space-padded.
volume_label="$( dd if="${fat_device}" bs=1 skip=71 count=11 2>/dev/null | tr -d '\000' | sed 's/ *$//' )"

if [ "${bytes_per_sector}" -eq 0 ]; then
   die 1 "${fat_device} has an unreadable FAT boot sector."
fi

staging="$( mktemp --directory )"
fresh_image="$( mktemp )"
# shellcheck disable=SC2317  # reached only via the EXIT trap
cleanup() {
   safe-rm --recursive --force -- "${staging}" "${fresh_image}"
}
trap cleanup EXIT

## Extract the existing FS contents.
mcopy -i "${fat_device}" -s "::/." "${staging}/" 2>/dev/null

## Build the replacement FAT in a fresh zero-filled image of the SAME size, so
## free space is deterministic rather than whatever stale bytes the partition
## held. Geometry, serial and label are carried over verbatim.
truncate --size="${device_size}" -- "${fresh_image}"
label_args=()
if [ -n "${volume_label}" ]; then
   label_args=( -n "${volume_label}" )
fi
mkfs.fat -F 32 \
   -S "${bytes_per_sector}" -s "${sectors_per_cluster}" \
   -R "${reserved_sectors}" -f "${fat_count}" \
   -i "${volume_serial}" "${label_args[@]}" \
   "${fresh_image}" >/dev/null

## Recreate directories first, then files, each set in LC_ALL=C sorted order, so
## cluster allocation is a pure function of the (identical) content. Every mtime
## is pinned to SOURCE_DATE_EPOCH; mcopy -m preserves it into the FAT directory
## entry.
##
## Listings captured, NOT fed via '< <( find ... )': a process substitution hides
## a cd/find failure from errexit, and the loop would then rebuild an EMPTY image
## that dd overwrites the ESP with -- the same silent-strip failure mode as the
## extraction listing above. The '[ -n ]' guard keeps an empty set to zero
## iterations (a flat FAT legitimately has no subdirectories).
##
## NUL-delimited: a file/dir name with a leading/trailing space or a newline must
## survive verbatim into mmd/mcopy.
##
## FIXME: We aren't capturing or `[ -n ]` guarding the listing anymore, so we
## need to use a temp file to expose the exit code of cd | find | sort to the
## parent shell, and should use a flag or similar to indicate whether we
## created any dirs or copied any files. We should also DELETE (not rewrite!)
## the comment above that mentions capturing listings.
while IFS= read -r -d '' directory; do
   mmd -i "${fresh_image}" "::${directory#./}"
done < <( cd -- "${staging}" && find . -mindepth 1 -type d -print0 | LC_ALL=C sort -z )

while IFS= read -r -d '' file; do
   touch --date="@${SOURCE_DATE_EPOCH}" -- "${staging}/${file#./}"
   mcopy -i "${fresh_image}" -m "${staging}/${file#./}" "::${file#./}"
done < <( cd -- "${staging}" && find . -mindepth 1 -type f -print0 | LC_ALL=C sort -z )

## Overwrite the partition with the deterministic image. conv=notrunc so a file
## target keeps its size; the image is exactly the partition size regardless.
dd if="${fresh_image}" of="${fat_device}" bs=1M conv=notrunc status=none
exit 0
