#!/bin/sh
# decode.sh -- Busybox-compatible archive decoder
# Extracts files from archives created by the Rust archiver (encrypted_archive).
# Uses only: dd, xxd/od, openssl, gunzip, sha256sum
#
# Usage: decode.sh <archive_file> <output_dir>

set -e

export LC_ALL=C

# -------------------------------------------------------
# Usage check
# -------------------------------------------------------
if [ $# -ne 2 ]; then
  printf 'Usage: %s <archive_file> <output_dir>\n' "$0" >&2
  exit 1
fi

ARCHIVE="$1"
OUTPUT_DIR="$2"

if [ ! -f "$ARCHIVE" ]; then
  printf 'Error: archive file not found: %s\n' "$ARCHIVE" >&2
  exit 1
fi

mkdir -p "$OUTPUT_DIR"

# -------------------------------------------------------
# Hardcoded 32-byte AES-256 key (matches src/key.rs)
# -------------------------------------------------------
KEY_HEX="7a35c1d94fe82b6a910df358bc74a61e428fd063e5179b2cfa8406cd3e79b550"

# -------------------------------------------------------
# Prerequisite checks
# -------------------------------------------------------
for tool in dd openssl sha256sum; do
  if ! command -v "$tool" >/dev/null 2>&1; then
    printf 'Error: required tool not found: %s\n' "$tool" >&2
    exit 1
  fi
done

# -------------------------------------------------------
# Detect xxd vs od fallback
# -------------------------------------------------------
if command -v xxd >/dev/null 2>&1; then
  HAS_XXD=1
else
  HAS_XXD=0
fi

# -------------------------------------------------------
# Temporary directory with cleanup trap
# -------------------------------------------------------
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT

# -------------------------------------------------------
# Hex reading functions
# -------------------------------------------------------

# read_hex <file> <offset> <count>
# Returns lowercase hex string of <count> bytes at <offset>
read_hex() {
  if [ "$HAS_XXD" = "1" ]; then
    dd if="$1" bs=1 skip="$2" count="$3" 2>/dev/null | xxd -p | tr -d '\n'
  else
    dd if="$1" bs=1 skip="$2" count="$3" 2>/dev/null | od -A n -t x1 | tr -d ' \n'
  fi
}

# read_le_u16 <file> <offset>
# Reads 2 bytes little-endian, prints decimal
read_le_u16() {
  _hex=$(read_hex "$1" "$2" 2)
  _b0=$(printf '%.2s' "$_hex")
  _b1=$(printf '%.2s' "${_hex#??}")
  printf '%d' "0x${_b1}${_b0}"
}

# read_le_u32 <file> <offset>
# Reads 4 bytes little-endian, prints decimal
read_le_u32() {
  _hex=$(read_hex "$1" "$2" 4)
  _b0=$(printf '%.2s' "$_hex")
  _b1=$(printf '%.2s' "${_hex#??}")
  _rest="${_hex#????}"
  _b2=$(printf '%.2s' "$_rest")
  _b3=$(printf '%.2s' "${_rest#??}")
  printf '%d' "0x${_b3}${_b2}${_b1}${_b0}"
}

# -------------------------------------------------------
# HMAC availability detection
# -------------------------------------------------------
SKIP_HMAC=0
if ! printf 'test' | openssl dgst -sha256 -mac HMAC -macopt hexkey:00 >/dev/null 2>&1; then
  printf 'WARNING: openssl HMAC not supported, skipping HMAC verification\n' >&2
  SKIP_HMAC=1
fi

# -------------------------------------------------------
# XOR obfuscation key (FORMAT.md Section 9.1)
# -------------------------------------------------------
XOR_KEY_HEX="a53c960fe17b4dc8"

# -------------------------------------------------------
# hex_to_bin <hex_string> <output_file>
# Write binary data from a hex string to a file.
# -------------------------------------------------------
hex_to_bin() {
  if [ "$HAS_XXD" = "1" ]; then
    printf '%s' "$1" | xxd -r -p > "$2"
  else
    _htb_hex="$1"
    _htb_i=0
    _htb_len=${#_htb_hex}
    : > "$2"
    while [ "$_htb_i" -lt "$_htb_len" ]; do
      _htb_byte=$(printf '%s' "$_htb_hex" | cut -c$((_htb_i + 1))-$((_htb_i + 2)))
      printf "\\$(printf '%03o' "0x$_htb_byte")" >> "$2"
      _htb_i=$((_htb_i + 2))
    done
  fi
}

# -------------------------------------------------------
# Header parsing with XOR bootstrapping (FORMAT.md Section 9.1, Section 10)
# -------------------------------------------------------
# Read 40-byte header as hex string (80 hex chars)
raw_header_hex=$(read_hex "$ARCHIVE" 0 40)
magic_hex=$(printf '%.8s' "$raw_header_hex")

if [ "$magic_hex" != "00ea7263" ]; then
  # Attempt XOR de-obfuscation
  header_hex=""
  byte_idx=0
  while [ "$byte_idx" -lt 40 ]; do
    hex_pos=$((byte_idx * 2))
    # Extract this byte from raw header (2 hex chars)
    raw_byte=$(printf '%s' "$raw_header_hex" | cut -c$((hex_pos + 1))-$((hex_pos + 2)))
    # Extract key byte (cyclic)
    key_pos=$(( (byte_idx % 8) * 2 ))
    key_byte=$(printf '%s' "$XOR_KEY_HEX" | cut -c$((key_pos + 1))-$((key_pos + 2)))
    # XOR
    xored=$(printf '%02x' "$(( 0x$raw_byte ^ 0x$key_byte ))")
    header_hex="${header_hex}${xored}"
    byte_idx=$((byte_idx + 1))
  done

  # Verify magic after XOR
  magic_hex=$(printf '%.8s' "$header_hex")
  if [ "$magic_hex" != "00ea7263" ]; then
    printf 'Invalid archive: bad magic bytes (got %s)\n' "$magic_hex" >&2
    exit 1
  fi
else
  header_hex="$raw_header_hex"
fi

# Write de-XORed header to temp file for field parsing
hex_to_bin "$header_hex" "$TMPDIR/header.bin"

# Parse header fields from de-XORed temp file
version_hex=$(read_hex "$TMPDIR/header.bin" 4 1)
version=$(printf '%d' "0x${version_hex}")

if [ "$version" -ne 1 ]; then
  printf 'Unsupported version: %d\n' "$version" >&2
  exit 1
fi

flags_hex=$(read_hex "$TMPDIR/header.bin" 5 1)
flags=$(printf '%d' "0x${flags_hex}")

file_count=$(read_le_u16 "$TMPDIR/header.bin" 6)
toc_offset=$(read_le_u32 "$TMPDIR/header.bin" 8)
toc_size=$(read_le_u32 "$TMPDIR/header.bin" 12)
toc_iv_hex=$(read_hex "$TMPDIR/header.bin" 16 16)

printf 'Archive: %d files\n' "$file_count"

# -------------------------------------------------------
# TOC decryption (FORMAT.md Section 9.2)
# -------------------------------------------------------
toc_encrypted=$(( flags & 2 ))

if [ "$toc_encrypted" -ne 0 ]; then
  # Extract encrypted TOC to temp file
  dd if="$ARCHIVE" bs=1 skip="$toc_offset" count="$toc_size" of="$TMPDIR/toc_enc.bin" 2>/dev/null

  # Decrypt TOC
  openssl enc -d -aes-256-cbc -nosalt \
    -K "$KEY_HEX" -iv "$toc_iv_hex" \
    -in "$TMPDIR/toc_enc.bin" -out "$TMPDIR/toc_dec.bin"

  TOC_FILE="$TMPDIR/toc_dec.bin"
  TOC_BASE_OFFSET=0
else
  TOC_FILE="$ARCHIVE"
  TOC_BASE_OFFSET=$toc_offset
fi

# -------------------------------------------------------
# TOC parsing loop (FORMAT.md Section 5)
# -------------------------------------------------------
pos=$TOC_BASE_OFFSET
extracted=0
i=0

while [ "$i" -lt "$file_count" ]; do
  # -- name_length (u16 LE) --
  name_length=$(read_le_u16 "$TOC_FILE" "$pos")
  pos=$((pos + 2))

  # -- filename (raw UTF-8 bytes) --
  filename=$(dd if="$TOC_FILE" bs=1 skip="$pos" count="$name_length" 2>/dev/null)
  pos=$((pos + name_length))

  # -- original_size (u32 LE) --
  original_size=$(read_le_u32 "$TOC_FILE" "$pos")
  pos=$((pos + 4))

  # -- compressed_size (u32 LE) --
  compressed_size=$(read_le_u32 "$TOC_FILE" "$pos")
  pos=$((pos + 4))

  # -- encrypted_size (u32 LE) --
  encrypted_size=$(read_le_u32 "$TOC_FILE" "$pos")
  pos=$((pos + 4))

  # -- data_offset (u32 LE) --
  data_offset=$(read_le_u32 "$TOC_FILE" "$pos")
  pos=$((pos + 4))

  # -- iv (16 bytes as hex) --
  iv_hex=$(read_hex "$TOC_FILE" "$pos" 16)
  pos=$((pos + 16))

  # -- hmac (32 bytes as hex) --
  hmac_hex=$(read_hex "$TOC_FILE" "$pos" 32)
  pos=$((pos + 32))

  # -- sha256 (32 bytes as hex) --
  sha256_hex=$(read_hex "$TOC_FILE" "$pos" 32)
  pos=$((pos + 32))

  # -- compression_flag (1 byte as hex) --
  compression_flag=$(read_hex "$TOC_FILE" "$pos" 1)
  pos=$((pos + 1))

  # -- padding_after (u16 LE) --
  padding_after=$(read_le_u16 "$TOC_FILE" "$pos")
  pos=$((pos + 2))

  # =======================================================
  # Per-file decode pipeline (FORMAT.md Section 10)
  # =======================================================

  # a. Extract ciphertext to temp file
  dd if="$ARCHIVE" bs=1 skip="$data_offset" count="$encrypted_size" of="$TMPDIR/ct.bin" 2>/dev/null

  # b. Verify HMAC (if available)
  #    HMAC input = IV (16 bytes) || ciphertext
  #    IV comes from the parsed TOC entry (iv_hex), not from an archive position
  if [ "$SKIP_HMAC" = "0" ]; then
    # Write IV bytes to temp file from parsed hex
    hex_to_bin "$iv_hex" "$TMPDIR/iv.bin"
    computed_hmac=$( {
      cat "$TMPDIR/iv.bin"
      cat "$TMPDIR/ct.bin"
    } | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${KEY_HEX}" -hex 2>/dev/null | awk '{print $NF}' )

    # Normalize both to lowercase
    computed_hmac=$(printf '%s' "$computed_hmac" | tr 'A-F' 'a-f')
    hmac_hex_lc=$(printf '%s' "$hmac_hex" | tr 'A-F' 'a-f')

    if [ "$computed_hmac" != "$hmac_hex_lc" ]; then
      printf 'HMAC FAILED for %s, skipping\n' "$filename" >&2
      i=$((i + 1))
      continue
    fi
  fi

  # c. Decrypt
  openssl enc -d -aes-256-cbc -nosalt \
    -K "$KEY_HEX" -iv "$iv_hex" \
    -in "$TMPDIR/ct.bin" -out "$TMPDIR/dec.bin"

  # d. Decompress (if compression_flag = "01")
  if [ "$original_size" -eq 0 ]; then
    # Special case: empty file
    : > "$TMPDIR/out.bin"
  elif [ "$compression_flag" = "01" ]; then
    if ! command -v gunzip >/dev/null 2>&1; then
      printf 'Error: gunzip required for compressed file: %s\n' "$filename" >&2
      i=$((i + 1))
      continue
    fi
    gunzip -c "$TMPDIR/dec.bin" > "$TMPDIR/out.bin"
  else
    mv "$TMPDIR/dec.bin" "$TMPDIR/out.bin"
  fi

  # e. Verify SHA-256
  actual_sha=$(sha256sum "$TMPDIR/out.bin" | awk '{print $1}')
  if [ "$actual_sha" != "$sha256_hex" ]; then
    printf 'WARNING: SHA-256 mismatch for %s\n' "$filename" >&2
  fi

  # f. Write output file
  mv "$TMPDIR/out.bin" "$OUTPUT_DIR/$filename"
  printf 'Extracted: %s (%d bytes)\n' "$filename" "$original_size"
  extracted=$((extracted + 1))

  # Clean up temp files for next iteration
  rm -f "$TMPDIR/ct.bin" "$TMPDIR/dec.bin" "$TMPDIR/iv.bin"

  i=$((i + 1))
done

# -------------------------------------------------------
# Final summary
# -------------------------------------------------------
printf 'Done: extracted %d files to %s\n' "$extracted" "$OUTPUT_DIR"
