#!/bin/bash -eu
#
# Copyright © 2017 Samuel Holland <samuel@sholland.org>
# See LICENSE in the project directory for license terms.
#
# bash is only required for hexadecimal math.
#

# Adjust these as needed
ARCH=${ARCH:-or1k}
BLOCK_SIZE=4096
CROSS_COMPILE=${CROSS_COMPILE:-${ARCH}-linux-musl-}

# Generated, but might need adjustment
case "$ARCH" in
  or1k) ENDIANNESS=big ;;
  *)    ENDIANNESS=little ;;
esac
case "$ARCH" in
  *64*) WORDSIZE=64 ;;
  *)    WORDSIZE=32 ;;
esac
case "$ARCH" in
  riscv*) ARCH=riscv ;;
esac
case "$ARCH" in
  arm|riscv)
        BFDNAME=elf${WORDSIZE}-${ENDIANNESS}${ARCH} ;;
  *)    BFDNAME=elf${WORDSIZE}-${ARCH} ;;
esac

# Command-line parameters
input_file=$1
section_file=$2
symbol_file=$3
output_file=$4

# Internal variables
base=0

# Temporary files, deleted when the script exits
ldscript=$(mktemp)
tmpdir=$(mktemp -d)
tmpfile=$(mktemp)
trap 'rm -fr "$ldscript" "$tmpdir" "$tmpfile"' EXIT

# Usage: elfify <file> <start> <end> <type>
elfify() {
  # Convert raw binary into an ELF section of the appropriate type
  case "$4" in
    data)
      flags=alloc,contents,data,load,readonly
      stype=object
      ;;
    text)
      flags=alloc,code,contents,load,readonly
      stype=function
      ;;
  esac
  case "$ENDIANNESS" in
    big)    reverse="--reverse-bytes $((WORDSIZE / 8))" ;;
    little) reverse= ;;
  esac
  ${CROSS_COMPILE}objcopy -B "$ARCH" -I binary -O "$BFDNAME" \
    --rename-section ".data=.${4},${flags}" $reverse "$1"
  # Remove start/end symbols added by objcopy
  ${CROSS_COMPILE}strip -s "$1"

  printf '  .%s %s : {\n    %s(.%s)\n' "$2" "$2" "$1" "$4" >> "$ldscript"

  # Import symbols for this section. File format: "<address> <name>" lines
  while read -r address name; do
    if [[ "$address" -ge "$2" && "$address" -lt "$3" ]]; then
      ${CROSS_COMPILE}objcopy \
        --add-symbol "${name}=.${4}:$((address - $2)),${stype}" "$1"
    fi
  done < "$symbol_file"

  printf '  }\n' >> "$ldscript"
}

# Usage: hex <number>
hex() {
  if [ "$WORDSIZE" -eq 64 ]; then
    printf '0x%016x' "$*"
  else
    printf '0x%08x' "$*"
  fi
}

# Usage: warn <message>
warn() {
  printf 'warning: %s\n' "$*" >&2
}

printf 'SECTIONS\n{\n' > "$ldscript"

start=-1
type=data
# Section file contains lines of the format "<virtual address> <section type>"
while read -r newstart newtype; do
  # Sanity check the section start address and length
  newstart=$((newstart - base))
  length=$((newstart - start))
  if [ "$length" -le 0 ]; then
    warn "Ignoring negative or zero-length section at $(hex "$newstart")."
    continue
  fi

  # Set the base from the first starting address
  if [ "$start" -lt 0 ]; then
    base=$newstart
    start=0
    type=$newtype
    continue
  fi

  # Sanity check the section type -- new types need flags in elfify function
  if [ "$newtype" != 'data' ] && [ "$newtype" != 'text' ]; then
    warn "Handling unknown type '$newtype' at $(hex "$start") as 'data'."
    newtype=data
  fi

  # Handle the section beginning at $start and ending before $newstart
  if [ "$start" -gt 0 ]; then
    bs=$start
    skip=1
  else
    bs=$BLOCK_SIZE
    skip=0
  fi
  of=$tmpdir/$(hex "$start").$type
  dd bs="$newstart" count=1 if="$input_file" of="$tmpfile" 2> /dev/null
  dd bs="$bs" if="$tmpfile" of="$of" skip="$skip" 2> /dev/null
  elfify "$of" "$(hex "$((base + start))")" "$((base + newstart))" "$type"

  start=$newstart
  type=$newtype
done < "$section_file"

if [ "$start" -lt 0 ]; then
  warn "No sections were provided! Addresses will be wrong."
fi

# Handle remaining part of the input file (after last section start)
if [ "$start" -gt 0 ]; then
  bs=$start
  skip=1
else
  bs=$BLOCK_SIZE
  skip=0
fi
of=$tmpdir/$(hex "$start").$type
dd bs="$bs" if="$input_file" of="$of" skip="$skip" 2> /dev/null
# The end address doesn't matter as long as it's past the end of the file
elfify "$of" "$(hex "$((base + start))")" "$((2 ** WORDSIZE))" "$type"

printf '}\n' >> "$ldscript"

# Merge all section files in the correct order
${CROSS_COMPILE}ld -e 0 -o "$output_file" -r -T "$ldscript"
