Skip to content

Instantly share code, notes, and snippets.

@apfelchips
Last active August 13, 2025 02:05
Show Gist options
  • Save apfelchips/5336fd75a18bd9137266abd356f31c6f to your computer and use it in GitHub Desktop.
Save apfelchips/5336fd75a18bd9137266abd356f31c6f to your computer and use it in GitHub Desktop.
rename IPA files by parsing the contained info.plist
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# src: https://gist.github.com/apfelchips/5336fd75a18bd9137266abd356f31c6f
# required commands: xmllint unzip plutil
# plutil is provided by the gnustep-base-runtime package on debian / ubuntu
RENAME=1
VERBOSE=0
# realpath polyfill
abspath(){
[ ${1:+x} ] || return 1
p=$1
until [ _"${p%/}" = _"$p" ]
do
p=${p%/}
done
[ -e "$p" ] && p=$1
[ -d "$1" ] && p=$p/
set 10 "$(pwd)" "${OLDPWD:-}"
PWD=
CDPATH="" cd -P "$2" && while [ "$1" -gt 0 ]
do
set "$1" "$2" "$3" "${p%/*}"
[ _"$p" = _"$4" ] || {
CDPATH="" cd -P "${4:-/}" || break
p=${p##*/}
}
[ ! -L "$p" ] && p=${PWD%/}${p:+/}$p && set "$@" "${p:-/}" && break
set $(($1-1)) "$2" "$3" "$p"
p=$(ls -dl "$p") || break
p=${p#*" $4 -> "}
done 2> /dev/null
cd "$2" && OLDPWD=$3 && [ ${5+x} ] && printf '%s\n' "$5"
}
analyze_macho(){
# https://github.com/golang/go/blob/master/src/debug/macho/macho.go#L33
# https://en.wikipedia.org/wiki/Mach-O#Multi-architecture_binaries
IPA_FILE_PATH="${1}"
INFO_FILE_IN_IPA_PATH="${2}"
BINARY=$(set +x; unzip -p "${IPA_FILE_PATH}" "${EXECUTABLE_FILE_IN_IPA_PATH}" | head -c 512 || if [[ $? -eq 141 ]]; then true; else exit $?; fi)
# Magic numbers for Mach-O headers
MH_MAGIC="feedface" # 32-bit
MH_CIGAM="cefaedfe" # 32-bit (reversed endian)
MH_MAGIC_64="feedfacf" # 64-bit
MH_CIGAM_64="cffaedfe" # 64-bit (reversed endian)
FAT_MAGIC="cafebabe" # Universal binary
FAT_CIGAM="bebafeca" # Universal binary (reversed endian)
FAT_MAGIC_64="cafebabf" # Universal binary (64-bit)
FAT_CIGAM_64="bfbafeca" # Universal binary (64-bit, reversed endian)
# LC_ENCRYPTION_INFO = 0x21, LC_ENCRYPTION_INFO_64 = 0x2C
LC_ENCRYPTION_INFO_HEX="21000000"
LC_ENCRYPTION_INFO_64_HEX="2c000000"
# Determine binary type
BINARY_MAGIC=$(xxd -l 4 -p - <<< $BINARY)
BINARY_TYPE="Unknown"
case "$BINARY_MAGIC" in
"$MH_MAGIC")
BINARY_TYPE="32-bit Mach-O"
ENDIAN="big"
;;
"$MH_CIGAM")
BINARY_TYPE="32-bit Mach-O (reversed endian)"
ENDIAN="little"
;;
"$MH_MAGIC_64")
BINARY_TYPE="64-bit Mach-O"
ENDIAN="big"
;;
"$MH_CIGAM_64")
BINARY_TYPE="64-bit Mach-O (reversed endian)"
ENDIAN="little"
;;
"$FAT_MAGIC" | "$FAT_CIGAM" | "$FAT_MAGIC_64" | "$FAT_CIGAM_64")
BINARY_TYPE="Universal (fat) binary"
;;
*)
echo "Unknown binary format: $BINARY_MAGIC"
exit 1
;;
esac
echo "Binary type: $BINARY_TYPE"
}
while getopts "dv" opt; do
case ${opt} in
d)
RENAME=0
;;
v)
VERBOSE=1
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
shift $((OPTIND -1))
FAILED_ITEMS=()
INPUT_ITEMS=""
if [ -p /dev/stdin ]; then
INPUT_ITEMS="$(cat /dev/stdin)"
else
INPUT_ITEMS=$(IFS="$' \t\n'"; for FILE in "$@"; do echo "${FILE}"; done)
fi
for IPA_FILE in ${INPUT_ITEMS}; do
if [ ! -f "${IPA_FILE}" ] || [ -L "${IPA_FILE}" ]; then
FAILED_ITEMS+=("${IPA_FILE}")
continue
fi
IPA_FILE_PATH=$(abspath "${IPA_FILE}")
IPA_FILE_NAME=$(basename "${IPA_FILE_PATH}")
if ! file --mime-type -b "${IPA_FILE_PATH}" | grep -iq -e '^application/x-ios-app$' -e '^application/zip$'; then
echo "${IPA_FILE} is not a valid iOS App Store Package"
FAILED_ITEMS+=("${IPA_FILE_PATH}")
continue
fi
echo "File: ${IPA_FILE_PATH}"
INFO_FILE_IN_IPA_PATH=$(unzip -Z1 "${IPA_FILE_PATH}" | grep -i -E "^Payload/[^/]*\.app/Info\.plist$" || true)
if [ -z "${INFO_FILE_IN_IPA_PATH}" ]; then
FAILED_ITEMS+=("${IPA_FILE_PATH}")
continue
fi
INFO_FILE_CONTENT=$(unzip -p "${IPA_FILE_PATH}" "${INFO_FILE_IN_IPA_PATH}" | plutil -convert xml1 -o - -- -)
BUNDLE_ID=$(xmllint --xpath "/plist/dict/key[text()='CFBundleIdentifier']/following-sibling::string[1]/text()" - <<< $INFO_FILE_CONTENT 2>/dev/null)
BUNDLE_NAME=$(xmllint --xpath "/plist/dict/key[text()='CFBundleName']/following-sibling::string[1]/text()" - <<< $INFO_FILE_CONTENT 2>/dev/null || true)
EXECUTABLE_NAME=$(xmllint --xpath "/plist/dict/key[text()='CFBundleExecutable']/following-sibling::string[1]/text()" - <<< $INFO_FILE_CONTENT 2>/dev/null)
EXECUTABLE_FILE_IN_IPA_PATH=$(unzip -Z1 "${IPA_FILE_PATH}" | grep -i -E "^Payload/[^/]*\.app/${EXECUTABLE_NAME}\$")
DEVICE_FAMILY=()
DEVICE_FAMILY_COUNT=$(xmllint --xpath "count(/plist/dict/key[.='UIDeviceFamily']/following-sibling::array[1]/integer)" - <<< $INFO_FILE_CONTENT || echo 0)
counter=1
while [ "$counter" -le "${DEVICE_FAMILY_COUNT}" ]; do
DEVICE_ID=$(xmllint --xpath "(/plist/dict/key[.='UIDeviceFamily']/following-sibling::array[1]/integer)[${counter}]/text()" - <<< $INFO_FILE_CONTENT)
case "${DEVICE_ID}" in
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/iPhoneOSKeys.html#//apple_ref/doc/uid/TP40009252-SW11
1) DEVICE_FAMILY+=("iPhoneOS");;
2) DEVICE_FAMILY+=("iPadOS");;
3) DEVICE_FAMILY+=("tvOS");;
4) DEVICE_FAMILY+=("watchOS");;
5) DEVICE_FAMILY+=("HomePodOS");;
6) DEVICE_FAMILY+=("macOS");;
*)
esac
((counter++))
done
DEVICE_FAMILY=$(IFS=+ ; echo "${DEVICE_FAMILY[*]:-'iOS'}")
LONG_VERSION_STRING=$(xmllint --xpath "/plist/dict/key[text()='CFBundleVersion']/following-sibling::string[1]/text()" - <<< $INFO_FILE_CONTENT 2>/dev/null || true)
SHORT_VERSION_STRING=$(xmllint --xpath "/plist/dict/key[text()='CFBundleShortVersionString']/following-sibling::string[1]/text()" - <<< $INFO_FILE_CONTENT 2>/dev/null || true)
VERSION_STRING=${SHORT_VERSION_STRING:-"${LONG_VERSION_STRING}"}
MIN_OS_VERSION=$(xmllint --xpath "/plist/dict/key[text()='MinimumOSVersion']/following-sibling::string[1]/text()" - <<< $INFO_FILE_CONTENT 2>/dev/null || true)
CLUTCH_VERSION=$( echo "${IPA_FILE}" | awk 'match(tolower($0), /clutch-\d+\.\d+\.\d+/) {print substr($0, RSTART, RLENGTH)}' | cut -d'-' -f2 || true)
CRC32=$(crc32 "${IPA_FILE}" | awk '{print $1}')
NEW_NAME=$(printf "${BUNDLE_ID}_v${VERSION_STRING:-'UNKNOWN'}_${BUNDLE_NAME:-'UNKNOWN'}_${DEVICE_FAMILY:-UNKNOWN}${MIN_OS_VERSION:-'0'}_${CRC32}.ipa" | tr '\n' '-' | tr -d '\r' | tr ' ' '-' | tr -dc '[:alnum:]()._@+-')
cat <<EOF
CFBundleName: ${BUNDLE_NAME:-'N/A'}
CFBundleIdentifier: ${BUNDLE_ID:-"N/A"}
CFBundleVersion: ${LONG_VERSION_STRING:-"N/A"}
CFBundleShortVersionString: ${SHORT_VERSION_STRING:-"N/A"}
UIDeviceFamily: ${DEVICE_FAMILY-"N/A"}
MinimumOSVersion: ${MIN_OS_VERSION:-"N/A"}
crc32: ${CRC32}
Clutch Version: ${CLUTCH_VERSION:-"N/A"}
EOF
if [ $VERBOSE -ge 1 ]; then
xmllint --format - <<< $INFO_FILE_CONTENT
fi
analyze_macho "${IPA_FILE_PATH}" "${EXECUTABLE_FILE_IN_IPA_PATH}"
if [ "${IPA_FILE_NAME}" != "${NEW_NAME}" ]; then
echo "New Name: ${IPA_FILE_NAME} --> ${NEW_NAME}"
if ((RENAME)); then
IPA_DIRNAME=$(dirname "${IPA_FILE_PATH}")
mv -n "${IPA_FILE_PATH}" "${IPA_DIRNAME}/${NEW_NAME}"
fi
else
echo "Keeping current name: ${IPA_FILE_NAME}"
fi
echo "---"
done
if [ "${#FAILED_ITEMS[@]}" -ne 0 ]; then
echo "FAILED ITEMS:"
for ERROR in "${FAILED_ITEMS[@]}"; do
echo "${ERROR}"
done
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment