#!/bin/bash

usage () {
    echo >&2 "usage: ${0##*/} [-hv] [-p google-service] [-i info] service-account-file {mach-o file|uuid} ..."
}

help () {
    usage
    cat >&2 <<EOF

-h      Show this help.
-p      Location of GoogleService-Info.plist.
-i      Location of Info.plist.
-v      Be verbose.

account JSON file containing account information.
mach-o  A path to an executable, dSYM file, library,
        or other Mach-O object.
uuid    A dSYM file's UUID (searches for the file).

Processes one or more Mach-O files for use with the Firebase Crash
Reporter.  dSYM bundles may be specified by full path to the dSYM
companion file (usually found under "DWARF") or by UUID.

For applications and frameworks, please use the full path to the
Mach-O file.  For frameworks, this will be "Blah.framework/Blah".
For applications, this will be "Blah.app/Blah".

Useful environment variables:

  SERVICE_PLIST - path to GoogleService-Info.plist (-p command-line option)
  INFO_PLIST - path to Info.plist (-i command-line option)
  DUMP_SYMS - path to dump_syms executable
  FCR_BUNDLE_ID - CFBundleIdentifier (build version) from Info.plist
  FCR_PROD_VERS - CFBundleShortVersionString from Info.plist
  FIREBASE_API_KEY - API key from GoogleService-Info.plist
  FIREBASE_APP_ID - App ID from GoogleService-Info.plist
  SWIFT_DEMANGLE - path to swift-demangle executable

Setting any of the above prevents this script from searching for the
values.  Specifically, the SERVICE_PLIST and INFO_PLIST files are not
required if FCR_* and FIREBASE_* environment variables are not empty.

EOF
}

KEEP_TEMPORARIES=false          # mostly for debugging (not documented)

while getopts hi:kp:v-: OPT; do
    case ${OPT} in
    h)  help; exit 0;;
    i)  INFO_PLIST="${OPTARG}";;
    k)  KEEP_TEMPORARIES=true;;
    p)  SERVICE_PLIST="${OPTARG}";;
    v)  ((VERBOSE+=1));;
    -)  case "${OPTARG}" in
        help)           help; exit 0;;
        info=*)         INFO_PLIST="${OPTARG#info=}";;
        service=*)      SERVICE_PLIST="${OPTARG#service=}";;
        verbose)        ((VERBOSE+=1));;
        *)              usage; exit 2;;
        esac;;
    ?)  usage; exit 2;;
    esac
done

shift $((OPTIND - 1))

. "$(dirname "$0")/upload-sym-util.bash"

var_check () {
    for VAR; do
        if [[ "${!VAR}" =~ \$\(.*\) ]]; then
            xcwarning "${VAR} (== \"${!VAR}\") appears to have unexpanded variables."
            xcnote "Consider specifying it through an environment variable."
        fi
    done
}

SERVICE_ACCOUNT_FILE="$1"

if [[ ! -f "${SERVICE_ACCOUNT_FILE}" ]]; then
    xcwarning "The first argument does not look like a service account file."
    xcdebug "Will attempt to extract account file from legacy cache."
    unset SERVICE_ACCOUNT_FILE
else
    shift
fi

if (( $# == 0 )); then
    usage
    exit 2
fi

if [[ "${INFO_PLIST}" && -f "${INFO_PLIST%/*}/GoogleService-Info.plist" ]]; then
    : "${SERVICE_PLIST:="${INFO_PLIST%/*}/GoogleService-Info.plist"}"
fi

if [[ "${SERVICE_PLIST}" && -f "${SERVICE_PLIST%/*}/Info.plist" ]]; then
    : "${INFO_PLIST:="${SERVICE_PLIST%/*}/Info.plist"}"
fi

xcdebug "SERVICE_PLIST = ${SERVICE_PLIST:="$(find . -name GoogleService-Info.plist | head -n1)"}"

xcdebug "INFO_PLIST = ${INFO_PLIST:="$(find . -name Info.plist | head -n1)"}"

if [[ -f "${SERVICE_PLIST}" ]]; then
    xcdebug "FIREBASE_API_KEY = ${FIREBASE_API_KEY:="$(/usr/libexec/PlistBuddy -c 'print API_KEY' "${SERVICE_PLIST}")"}"
    xcdebug "FIREBASE_APP_ID = ${FIREBASE_APP_ID:="$(/usr/libexec/PlistBuddy -c 'print GOOGLE_APP_ID' "${SERVICE_PLIST}")"}"
    xcdebug "FCR_BUNDLE_ID = ${FCR_BUNDLE_ID:="$(/usr/libexec/PlistBuddy -c 'print BUNDLE_ID' "${SERVICE_PLIST}")"}"
fi

if [[ -f "${INFO_PLIST}" ]]; then
    xcdebug "FCR_PROD_VERS = ${FCR_PROD_VERS:="$(/usr/libexec/PlistBuddy -c 'print CFBundleShortVersionString' "${INFO_PLIST}" 2>/dev/null)"}"
fi

var_check FCR_PROD_VERS FCR_BUNDLE_ID

ERROR=$'environment variable empty or unset\n\nExplicitly add to environment or set GoogleService-Info.plist (-p)\nand Info.plist (-i) flags to extract values from the files.\n\nTry "'"$0"' -h" for details.'

: "${FIREBASE_API_KEY:?"${ERROR}"}" "${FIREBASE_APP_ID:?"${ERROR}"}"
: "${FCR_PROD_VERS:?"${ERROR}"}" "${FCR_BUNDLE_ID:?"${ERROR}"}"

# Extract key from legacy cache.

if [[ ! "${SERVICE_ACCOUNT_FILE}" ]]; then
    xcwarning "Running extract-keys on desktop."
    EXTRACT_KEYS="$(script_dir)/extract-keys"
    (cd "${HOME}/Desktop"; "${EXTRACT_KEYS}") || exit $?
    SERVICE_ACCOUNT_FILE="${HOME}/Desktop/${FIREBASE_APP_ID}.json"
    xcdebug "Using ${SERVICE_ACCOUNT_FILE} as account file.  Please move this and all other extracted keys to a safe place."
fi

if [[ ! -f "${SERVICE_ACCOUNT_FILE}" ]]; then
    echo >&2 "Unable to find service account file."
    echo >&2
    usage
    exit 2
fi

# usage: extract_symbols_and_upload *dwarf-file* *arch* *exe-file*
#
# Do NOT use the dSYM bundle path.  While it may work on occasion, it
# is not guaranteed to do so; the full path to the DWARF companion
# file will always work.  (Discovered by Kerem Erkan.)
#
# If the executable is empty, use the DWARF companion file as a proxy
# for the executable.
extract_symbols_and_upload () {
    local DWARF_COMPANION="$1" ARCH="$2" EXECUTABLE="$3"

    if [[ ! "${EXECUTABLE}" ]]; then
        xcdebug "No executable; using ${DWARF_COMPANION} as symbol source."

        EXECUTABLE="${DWARF_COMPANION}"
        unset DWARF_COMPANION
    fi

    [[ "${EXECUTABLE}" ]] || return 1

    if [[ -x "${SWIFT_DEMANGLE:=$(xcrun --find swift-demangle 2>/dev/null)}" ]];
    then
        SWIFT_DEMANGLE_COMMAND="${SWIFT_DEMANGLE} -simplified"
    else
        SWIFT_DEMANGLE_COMMAND=/bin/cat
    fi
    fcr_mktemp SYMBOL_FILE

    "${DUMP_SYMS:="$(script_dir)/dump_syms"}" -a "${ARCH}" ${DWARF_COMPANION:+-g "${DWARF_COMPANION}"} "${EXECUTABLE}" | ${SWIFT_DEMANGLE_COMMAND} >|"${SYMBOL_FILE}" || return $?

    fcr_upload_files "${SYMBOL_FILE}" || return $?
}

# usage: is_executable *path*
#
# Check to see if the file is an executable or a dSYM bundle
is_executable () {
    [[ -f "$1" || ( -d "$1" && "${1%/}" == *.dSYM ) ]]
}

# usage: is_uuid *string*
#
# Verify that the argument is a UUID.
is_uuid () {
    [[ "$1" =~ ^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}$ ]]
}

# usage: set_uuids_archs *mach-o-file*
#
# side effect: appends to UUIDS, ARCHS
#
# Extract the uuid and architecture information from the given Mach-O
# file and append the information to the UUIDS and ARCHS arrays.
set_uuids_archs () {
    eval "$(dwarfdump --uuid "$1" | awk '/^UUID:/ { print "UUIDS+=(" $2 "); ARCHS+=" $3 }')"
}

# usage: mdls_to_bash
#
# Convert the output of mdls to a string consumable by bash.  mdls
# outputs string arrays as quoted strings separated by commas, and
# Unicode characters as '\Uxxxx'.
#
# Note: this is sensitive to the current locale.  If the locale is not
# UTF-8, then wide-character warnings will result if the strings
# contain non-ASCII characters.  This is actually a desired behavior,
# because bash has issues with non-Unicode encodings for file names.
# (The macOS default is to have UTF-8 enabled, so this should not be a
# problem for the majority of use cases.)
mdls_to_bash () {
    perl -C -ple 's/,$//; s/\\U(....)/chr hex $1/ge'
}

for EXE; do
    if is_executable "${EXE}"; then
        xcdebug "Assuming ${EXE} is an executable or dSYM bundle."

        # Import architecture UUID information
        UUIDS=() ARCHS=()
        set_uuids_archs "${EXE}"

        for I in "${!UUIDS[@]}"; do
            xcdebug "Found ${UUIDS[$I]} for ${ARCHS[$I]} in ${EXE}"
        done

        if ((${#UUIDS[*]} == 0)); then
            xcwarning "${EXE} exists, but has no architecture information."
            continue
        fi

        if [[ "${EXE}" = *.dSYM ]]; then
            xcdebug "Removing dSYM bundle as executable target."
            unset EXE
        fi

    elif is_uuid "${EXE}"; then
        xcdebug "${EXE} looks like a UUID to me."
        UUIDS=("${EXE}"); unset EXE

    else
        xcwarning "${EXE}: not an executable, bundle, or UUID."
        continue
    fi

    BUNDLES=()

    for UUID in "${UUIDS[@]}"; do
        xcdebug "Searching for ${UUID} ..."

        QUERY_UUID="com_apple_xcode_dsym_uuids == '${UUID}'"
        QUERY_TYPE="kMDItemContentType == 'com.apple.xcode.dsym' || kMDItemContentType == 'com.apple.xcode.archive'"
        QUERY="(${QUERY_UUID}) && (${QUERY_TYPE})"

        if ((VERBOSE > 1)); then
            xcnote "Passing query \"${QUERY}\" to mdfind."
        fi

        MD_FIND_RESULT=()

        eval "$(mdfind "${QUERY}" -0 | xargs -0 perl -le 'print "MD_FIND_RESULT+=(\Q$_\E)" for @ARGV')"

        xcdebug "mdfind returned (${MD_FIND_RESULT[*]})"

        # BUNDLES should contain no duplicates.
        for I in "${!MD_FIND_RESULT[@]}"; do
            for BUNDLE in "${BUNDLES[@]}"; do
                if [[ "${MD_FIND_RESULT[$I]}" == "$BUNDLE" ]]; then
                    unset "MD_FIND_RESULT[$I]"
                fi
            done
        done

        BUNDLES+=("${MD_FIND_RESULT[@]}")
    done

    if [[ ${#BUNDLES[@]} == 0 && ${#ARCHS[@]} == 0 ]]; then
        xcwarning "No executable or bundle found for ${UUIDS[*]}."
        xcnote "Try passing in the executable itself instead of a UUID."
        continue
    fi

    xcdebug "BUNDLES = (${BUNDLES[*]})"

    if [[ ${#BUNDLES[@]} == 0 ]]; then
        xcdebug "No dSYM bundle found."

        # The dSYM has to be on a normal volume (not temporary).  It
        # can, however, be shared among multiple executables.
        if [[ ! "${SCRATCH_BUNDLE}" ]]; then
            SCRATCH_BUNDLE="${HOME}/com.google.BatchUploadScratchFile.dSYM"
            FCR_TEMPORARY_FILES+=("${SCRATCH_BUNDLE}")
        fi

        xcdebug "Creating one in ${SCRATCH_BUNDLE}"

        BUNDLES=("${SCRATCH_BUNDLE}")

        # Create the dSYM bundle.  This may produce an empty dSYM
        # bundle if the executable has no debugging information.
        xcrun dsymutil -o "${BUNDLES[0]}" "${EXE}"; STATUS=$?

        if ((STATUS)); then
            xcwarning "Command dsymutil failed with exit code ${STATUS}."
            continue
        fi

        # Import the dSYM bundle.  There is a momentary delay between
        # creating the bundle and having it indexed; explicitly
        # importing guarantees the mds database is up-to-date when we
        # ask it for information about UUIDs and paths.
        mdimport "${SCRATCH_BUNDLE}"; STATUS=$?

        if ((STATUS)); then
            xcwarning "Command mdimport failed with exit code ${STATUS}."
            continue
        fi
    fi

    SEEN_ARCH=() SEEN_PATH=()

    for BUNDLE in "${BUNDLES[@]}"; do
        typeset -a BNDL_UUIDS BNDL_PATHS # keeps ShellLint happy

        eval "BNDL_UUIDS=$(mdls -raw -name com_apple_xcode_dsym_uuids "${BUNDLE}" | mdls_to_bash)"
        eval "BNDL_PATHS=$(mdls -raw -name com_apple_xcode_dsym_paths "${BUNDLE}" | mdls_to_bash)"

        # Neither of these SHOULD occur, but curious things happen out
        # in the field.
        if ((${#BNDL_UUIDS[@]} != ${#BNDL_PATHS[@]})); then
            xcwarning "${BUNDLE}: Malformed dSYM bundle."
            continue
        elif ((${#BNDL_UUIDS[@]} == 0)); then
            xcwarning "${BUNDLE}: No DWARF information."
            continue
        fi

        # If no executable was specified, then the UUIDS and ARCHS
        # arrays are empty.  Populate them with information from the
        # bundle.
        if [[ ! "${EXE}" ]]; then
            # The final UUIDS setting will be the intersection of the
            # discovered set and the originally specified UUIDS.  This
            # is to prevent uploading potentially private information.
            SOUGHT_UUIDS=("${UUIDS[@]}")

            UUIDS=() ARCHS=()
            for BNDL_PATH in "${BNDL_PATHS[@]}"; do
                set_uuids_archs "${BUNDLE}/${BNDL_PATH}"
            done

            if ((${#SOUGHT_UUIDS[@]})); then
                for I in "${!UUIDS[@]}"; do
                    for UUID in "${SOUGHT_UUIDS[@]}"; do
                        if [[ "${UUIDS[$I]}" == "${UUID}" ]]; then
                            continue 2
                        fi
                    done

                    # This is not the DWARF you are looking for...
                    xcdebug "Rejecting ${UUIDS[$I]} (${ARCHS[$I]}) as candidate DWARF file."
                    unset "UUIDS[$I]" "ARCHS[$I]"
                done
            fi

            unset SOUGHT_UUIDS
        fi

        for I in "${!BNDL_UUIDS[@]}"; do
            # See comment on extract_symbols_and_upload for why the
            # full path to the companion file is required.

            BNDL_UUID="${BNDL_UUIDS[$I]}" DWARF_COMPANION="${BUNDLE}/${BNDL_PATHS[$I]}"

            for J in "${!ARCHS[@]}"; do
                # A dSYM bundle can contain multiple architectures for
                # multiple applications.  Make sure we get the right
                # one.
                if [[ "${BNDL_UUID}" == "${UUIDS[$J]}" ]]; then
                    ARCH="${ARCHS[$J]}"
                    break
                fi
            done

            if [[ ! "${ARCH}" ]]; then
                # This is not an error: it is legal for a dSYM bundle
                # to contain debugging information for multiple
                # executables (such as a framework with multiple
                # subframeworks).  Just ignore it.
                xcdebug "No matching information found in ${DWARF_COMPANION} with UUID ${BNDL_UUID}."
                continue
            fi

            xcdebug "Found ${UUID} for ${ARCH} in ${DWARF_COMPANION}"

            # Have we already uploaded this file?
            for J in "${!SEEN_ARCH[@]}"; do
                if [[ "${ARCH}" == "${SEEN_ARCH[$J]}" ]] && cmp -s "${DWARF_COMPANION}" "${SEEN_PATH[$J]}"; then
                    xcdebug "${DWARF_COMPANION}: copy of ${SEEN_PATH[$J]}; no need to upload."
                    continue 2
                fi
            done

            if [[ -f "${DWARF_COMPANION}" ]]; then
                extract_symbols_and_upload "${DWARF_COMPANION}" "${ARCH}" "${EXE}" || exit $?
                SEEN_ARCH+=("${ARCH}") SEEN_PATH+=("${DWARF_COMPANION}")
            fi
        done
    done
done

# For debugging odd cases.
if "${KEEP_TEMPORARIES}"; then
    FCR_TEMPORARY_FILES=()
fi

echo "Done."
