#!/bin/bash

# Prints SFO information of either a PS4 PKG or param.sfo file.
# Optionally searches for (and replaces) a specific string.
# Made with info from https://www.psdevwiki.com/ps4/Param.sfo

show_usage() {
  echo "Usage: ${0##*/} [-hw] file [search] [replace]" >&2
}

show_help() {
  show_usage 2>&1
  echo "
  Prints SFO information of either a PS4 PKG or param.sfo file.
  Providing a search string will output the value of that specific key only.
  Providing a replacement string will write it to the file (needs option -w).

  Options:
    -h  Display this help info
    -w  Enable write mode"
  exit
}

if [[ $@ == "" ]]; then
  show_usage
  exit 1
fi

# Parse command line options
while [[ $1 == "-"?* ]]; do
  for ((i=1;i<${#1};i++)); do
    case ${1:$i:1} in
      h) show_help ;;
      w) option_write_enabled=true ;;
    esac
  done
  shift
done

file=$1
[[ -f "$file" ]] || { echo "Not a file: $file" >&2; exit 1; }
search=${2,,}
replace=$3
if [[ $replace != "" && $option_write_enabled != true ]]; then
  echo "Use option -w to enable write mode." >&2
  exit 1
fi

getbytes() {
  xxd -u -p -s $(($1+pkgOffset)) -l "$2" "$file"
}

# Finds the param.sfo's offset inside a PKG file
# https://www.psdevwiki.com/ps4/Package_Files
get_pkg_offset() {
  local pkg_file_count pkg_table_offset full_table i current_datablock id
  pkg_file_count=$((0x$(getbytes 0x00C 4)))
  pkg_table_offset=0x$(getbytes 0x018 4)
  full_table=$(getbytes $pkg_table_offset $((32*pkg_file_count)))
  full_table=${full_table//$'\n'} # xxd adds newlines which we must remove
  for ((i=0;i<pkg_file_count;i++)); do
    current_datablock=${full_table:$i*64:64}
    id=${current_datablock:0:8}
    if [[ $id == 00001000 ]]; then # param.sfo ID
      pkgOffset=0x${current_datablock:32:8}
      return
    fi
  done
}

# Header
# https://www.psdevwiki.com/ps4/Param.sfo#Header_SFO
magic=$(getbytes 0x00 0x04)
if [[ $magic == 7F434E54 ]]; then # PKG file
  get_pkg_offset
  if [[ $(getbytes 0 4) != 00505346 ]]; then
    echo "Error: param.sfo magic not found inside PKG \"$file\"" >&2
    exit 1
  fi
elif [[ $magic == 00505346 ]]; then # param.sfo file
  pkgOffset=0
else
  echo "$file: Not a PKG neither a param.sfo file!" >&2
  exit 1
fi
header=$(getbytes 0x00 20)
keyTableOffset=${header:16:8}
keyTableOffset=0x${keyTableOffset:6:2}${keyTableOffset:4:2}${keyTableOffset:2:2}${keyTableOffset:0:2}
dataTableOffset=${header:24:8}
dataTableOffset=0x${dataTableOffset:6:2}${dataTableOffset:4:2}${dataTableOffset:2:2}${dataTableOffset:0:2}
indexTableEntries=${header:32:8}
indexTableEntries=$((0x${indexTableEntries:6:2}${indexTableEntries:4:2}${indexTableEntries:2:2}${indexTableEntries:0:2}))

# Get nullstring-delimited keys
# https://www.psdevwiki.com/ps4/Param.sfo#Key_table
i=0
while read -r -d $'\0' line; do
  key[$i]="$line"
  ((i++))
  [[ $i -gt $indexTableEntries ]] && break
done  < <(getbytes "$keyTableOffset" 5000 | xxd -r -p)

# Get full index table
# https://www.psdevwiki.com/ps4/Param.sfo#Index_table
indexTable=$(getbytes 0x14 $((0x10*indexTableEntries)))
indexTable=${indexTable//$'\n'} # xxd adds newlines which we must remove

# Get full data table
dataTable=$(getbytes "$dataTableOffset" 5000)
dataTable=${dataTable//$'\n'}

# Search data table data
for ((i=0;i<indexTableEntries;i++)); do
  if [[ $search == "" ]]; then
    echo -n "${key[$i]}="
  elif [[ $search != "${key[$i],,}" ]]; then
    continue
  fi
  param_fmt=${indexTable:$((i*32+4)):4}
  paramLen=${indexTable:$((i*32+8)):8}
  paramLen=0x${paramLen:6:2}${paramLen:4:2}${paramLen:2:2}${paramLen:0:2}
  paramMaxLen=${indexTable:$((i*32+16)):8}
  paramMaxLen=0x${paramMaxLen:6:2}${paramMaxLen:4:2}${paramMaxLen:2:2}${paramMaxLen:0:2}
  dataOffset=${indexTable:$((i*32+24)):8}
  dataOffset=0x${dataOffset:6:2}${dataOffset:4:2}${dataOffset:2:2}${dataOffset:0:2}

  # Write data to file
  if [[ $search == "${key[$i],,}" && $option_write_enabled == true ]]; then
    case $param_fmt in
      0402) # Replace UTF-8 string
        # Check new string for valid length
        if [[ ${#replace} -ge $paramMaxLen ]]; then
          echo "Error: Replacement string \"$replace\" too large ($((paramMaxLen-1)) characters allowed)." >&2
          exit 1
        fi
        # Fill space with zeros
        printf "%0$((paramMaxLen*2))d" | xxd -r -p -seek \
          $((pkgOffset+dataTableOffset+dataOffset)) - "$file"
        # Write new string to file
        printf "$replace" | xxd -o $((pkgOffset+dataTableOffset+dataOffset)) \
          | xxd -r - "$file"
        # Write new paramLen to file
        newlen=$(printf "%08x" $((${#replace}+1)))
        newlen=${newlen:6:2}${newlen:4:2}${newlen:2:2}${newlen:0:2}
        printf "%x: %s" $((pkgOffset+0x14+i*16+4)) "$newlen" \
          | xxd -r - "$file"
        ;;
      0404) # Replace integer
        # Test if new value is an integer
        ((replace)) 2> /dev/null
        if [[ $? == 1 ]]; then
          echo "Error: Not an integer: \"$replace\"." >&2
          exit 1
        fi
        # Check new integer for valid size
        if [[ $replace == 0x* ]]; then
          newint=$(printf "%08d" "${replace#0x}")
        else
          newint=$(printf "%08x" "$replace" 2> /dev/null)
        fi
        if [[ ${#newint} -gt $((paramMaxLen*2)) ]]; then
          echo "Error: Replacement integer \"$replace\" larger than limit ($((0xFFFFFFFF)))." >&2
          exit 1
        fi
        # Fill space with zeros
        printf "%x: %04d" $((pkgOffset+dataTableOffset+dataOffset)) \
          | xxd -r - "$file"
        # Write new integer to file
        if [[ $replace != 0x* ]]; then
          newint=${newint:6:2}${newint:4:2}${newint:2:2}${newint:0:2}
        fi
        printf "%x: %s" $((pkgOffset+dataTableOffset+dataOffset)) "$newint" \
          | xxd -r - "$file"
        ;;
      *) echo "Cannot replace this type of data: $param_fmt."; exit 1 ;;
    esac
    exit
  fi

  # Read data
  data=${dataTable:$((dataOffset*2)):$((paramLen*2))}
  case $param_fmt in
    0400) # UTF-8 special mode string
      echo "[SPECIAL MODE STRING]"
      ;;
    0402) # UTF-8 string
      echo "${data%00}" | xxd -r -p
      echo
      ;;
    0404) # Integer
      data=0x${data:6:2}${data:4:2}${data:2:2}${data:0:2}
      echo "$data"
      ;;
    *) echo "[UNKNOWN DATA TYPE]"
  esac

  [[ $search == "${key[$i],,}" ]] && exit
done
