#!/bin/bash

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

## AI-Assisted

set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose

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

error() {
   printf '%s\n' "ERROR: $*" >&2
   exit 2
}

print_usage() {
  cat <<EOF
Emit a reproducibility buildinfo file for a built image. This is a small,
signed Deb822 file modeled on Debian '.buildinfo' conventions. It records the
parameters needed to reproduce the image so a verifier can rebuild it
trivially. The build metadata placed in the file is read from the environment.

This script intentionally does not record checksums or sign files;
'dm-prepare-release' does both already.

Usage:
  dm-reproducible-buildinfo --target TARGET --image IMAGE_PATH [--output FILE]

Exit codes: 0 for success, 2 for a usage/environment error.
EOF
}

target=""
image=""
output=""
do_force="false"

while [ "$#" -gt 0 ]; do
   case "${1:-}" in
      --target)
         [ "$#" -ge 2 ] || error "--target requires a value."
         target="$2"
         shift 2
         ;;
      --image)
         [ "$#" -ge 2 ] || error "--image requires a value."
         image="$2"
         shift 2
         ;;
      --output)
         [ "$#" -ge 2 ] || error "--output requires a value."
         output="$2"
         shift 2
         ;;
      --force)
         do_force='true'
         shift
         ;;
      -h|--help)
         print_usage
         exit 0
         ;;
      *)
         error "unknown argument: '$1'."
         ;;
   esac
done

[ -n "${target}" ] || error "--target is required."
[ -n "${image}" ]  || error "--image is required."
if [ "${do_force}" = 'true' ]; then
   [ -f "${image}" ] || printf '%s\n' "INFO: --force: '${image}' does not exist, but emitting the record anyway." >&2
else
   [ -f "${image}" ] || error "image does not exist: '${image}'"
fi
[ -n "${output}" ] || output="${image}.dm-buildinfo"

val() {
   if ! check_variable_name "${1:-}"; then
      printf '%s' "unknown"
      return 0
   fi

   local v="${!1}"
   printf '%s' "${v:-unknown}"
}

## The source state recorded by help-steps/sign-and-tag BEFORE it amended
## anything. It has to come from there: sign-tag-head rewrites the parent's HEAD
## and every submodule's HEAD, and those commits exist only inside that build, so
## reading them here would publish a signed record pointing at commits nobody can
## fetch. It also carries the submodule SHAs the build actually used, which CI's
## fork-branch checkout resolves from moving branch tips and records nowhere else.
##
## TODO: See if we can get rid of dm-source-state/source_state_file. It should
## only be useful for builds where sign-and-tag runs, and sign-and-tag should
## only ever run in exceptional cases. There is no need for CI-built images to
## be reproducible other than to test reproducibility itself in CI, in which
## case we can probably build from the same sign-and-tag'd repo twice to
## verify that we get the same result and thus still not need dm-source-state.
source_state_file="${dm_source_state_file:-}"
if [ -z "${source_state_file}" ] && [ -n "${binary_build_folder_dist:-}" ]; then
   source_state_file="${binary_build_folder_dist}/dm-source-state"
fi

state_unrecorded() {
   printf '%s\n' "Source-Commit: unrecorded ($1)" "Submodule-State: unrecorded"
}

source_state_block=""
if [ -z "${source_state_file}" ]; then
   source_state_block="$(state_unrecorded "neither dm_source_state_file nor binary_build_folder_dist is set")"
elif [ ! -f "${source_state_file}" ] || [ ! -r "${source_state_file}" ]; then
   source_state_block="$(state_unrecorded "help-steps/sign-and-tag did not run, or did not write '${source_state_file}'")"
else
   source_state_candidate="$(cat -- "${source_state_file}")"
   state_reject=""
   case "${source_state_candidate}" in
      "")
         state_reject="${source_state_file} is empty"
         ;;
      *$'\n\n'*)
         ## TODO: Should we reject a single newline at the start of the file
         ## too?
         state_reject="${source_state_file} contains a blank line, which would terminate this Deb822 record"
         ;;
   esac
   if [ -z "${state_reject}" ]; then
      case "${source_state_candidate}" in
         "Source-Commit: "*)
            true
            ;;
         *)
            state_reject="${source_state_file} does not start with a Source-Commit field"
            ;;
      esac
   fi
   if [ -z "${state_reject}" ]; then
      case "${source_state_candidate}" in
         *"Submodule-State:"*)
            ## FIXME: This can be fooled if 'Submodule-State:' occurs anywhere
            ## in a key's value.
            true
            ;;
         *)
            state_reject="${source_state_file} carries no Submodule-State field"
            ;;
      esac
   fi
   if [ -n "${state_reject}" ]; then
      source_state_block="$(state_unrecorded "${state_reject}")"
   else
      source_state_block="${source_state_candidate}"
   fi
fi

## Source-Version and Source-Commit name DIFFERENT commits by design. Do not
## assert they agree.
##
## A previous version of this file required them to be equal, and that check
## fails every signed build:
##   - sign-and-tag records Source-Commit from 'git rev-parse HEAD' BEFORE it
##     signs, deliberately -- see its own comment. It then runs sign-tag-head,
##     which rewrites the parent's HEAD via 'commit --amend -S'.
##   - 'dist_build_version' is derived by 'git describe' AFTER that amend, so its
##     '-g<sha>' suffix names the amended commit.
## The pre-sign commit is the one a rebuilder can actually fetch; the amended one
## exists only inside that build. Recording the fetchable commit while the
## version describes the ephemeral one is the intended behaviour, so equality is
## the wrong invariant -- it blocked CI on a correct record.
##
## The underlying hazard is real and NOT covered here: 'dist_build_version' is
## auto-detected only when UNSET, so a value inherited from a reused workspace
## silently wins over the actual tree (observed: Source-Commit 02096cd4... beside
## Source-Version ...-ge65ff458...). Catching that needs a comparison against the
## tree's CURRENT HEAD, which this script cannot obtain reliably -- it runs both
## in-tree during a build and from an installed /usr/bin path, and guessing a
## repository from the working directory is how the last false positive happened.
## Belongs where dist_build_version is set (help-steps/variables), not here.
##
## TODO: Since we're going to try to get rid of sign-and-tag in the context of
## standard reproducible builds, and we're going to try to avoid needing to
## record repo information before a sign-and-tag run, maybe we can go back to
## asserting Source-Version and Source-Commit agree.

## An image built with --reproducible-dist-build-version carries a version with
## the describe suffix stripped, so it is not the artifact a normal build
## of this commit would produce. Note this in the record so no one confuses
## a commit-built image with a release image.
version_normalized_field=""
if [ "${dist_build_version_reproducible:-}" = "true" ]; then
   version_normalized_field="
Version-Normalized: true"
   if [ -n "${dist_build_version_unnormalized:-}" ]; then
      version_normalized_field="${version_normalized_field}
Source-Version-Unnormalized: ${dist_build_version_unnormalized}"
   fi
fi

printf '%s\n' "\
Format: 1.0
Buildinfo-Type: derivative-image
Source-Repo: $(val project_clearnet)
${source_state_block}
Source-Version: $(val dist_build_version)${version_normalized_field}
Flavor: $(val dist_build_flavor)
Target: ${target}
Build-Type: $(val dist_build_type)
Architecture: $(val dist_build_target_arch)
Freedom: $(val build_freedom_only)
Debian-Suite: $(val dist_build_apt_stable_release)
APT-Snapshot: $(val dist_build_apt_sources_mirror)
Source-Date-Epoch: $(val SOURCE_DATE_EPOCH)
Image-File: $(basename -- "${image}")
" > "${output}"

printf '%s\n' "INFO: wrote buildinfo -> ${output}" >&2
printf '%s\n' "INFO: register '${output}' with dm-prepare-release so it is signed alongside the image." >&2
