#!/bin/bash

#---------------------------------------------------------------------
# Bash script that loops through all the files of a folder and
# - print the paths of binaries using a UI framework (Catalyst, SwiftUI, AppKit)
# - detect the UI framework used
#
# Created by Alexandre Colucci on 18.08.2022
# https://blog.timac.org/2022/0818-state-of-appkit-catalyst-swiftui-mac/
#---------------------------------------------------------------------


#---------------------------------------------------------------------
# Force expand a wildcard pattern into the list of matching pathnames
#---------------------------------------------------------------------
shopt -s nullglob

#---------------------------------------------------------------------
# Function to print the usage
#---------------------------------------------------------------------
printUsage ()
{
	echo "Usage: detectUIFramework.sh PATH"
	echo "PATH: Folder to search for binaries and detect the UI framework"
	echo ""
	echo "Examples:"
	echo "  detectUIFramework.sh /System/Library"
	echo "  detectUIFramework.sh /System"
	echo "  detectUIFramework.sh /"
	echo ""
}

#---------------------------------------------------------------------
# Function to check if a file (passed as argument $1) is an executable
# It returns the number of occurrences of the string 'Mach-O' returned by
# the output of the file command. File would generally return a line like
# - Mach-O 64-bit executable arm64e (/usr/bin/sysdiagnose)
# - Mach-O 64-bit dynamically linked shared library arm64e (/usr/lib/libobjc-trampolines.dylib)
# - Mach-O 64-bit bundle arm64e (/usr/lib/xpc/support.bundle/support)
# - Mach-O 64-bit dynamic linker arm64e (/usr/lib/dyld)
# - Mach-O 64-bit kext bundle arm64e (/System/Library/Extensions/AppleIDV.kext/AppleIDV)
# - Mach-O 64-bit dynamically linked shared library stub arm64e (/Applications/FTMInternal-4.app/libMobileGestalt.dylib)
#---------------------------------------------------------------------
isExecutable ()
{
	file "$1" 2>/dev/null | grep -o "Mach-O" | wc -l
}

#---------------------------------------------------------------------
# Function to process a file (passed as argument $1).
# It calls the function isExecutable() to determine
# if this file is an executable and in this case
# print the path of this file and tries to detect the UI framework.
#---------------------------------------------------------------------
processFile ()
{
	isExecutable=$( isExecutable "$1" )
 	if [ ${isExecutable} != 0 ]
 	then
 		# Default
 		isSwiftUIApp=0
 		isAppKitApp=0
 		isUIKitApp=0
 	
 		#
 		# Run otool on the binary
 		#
	 	otoolOutput=$( otool -L "$1" )
	 	
	 	#
	 	# The Binary uses UIKit: this is a Mac Catalyst app
	 	#
	 	useUIKit=$( echo "${otoolOutput}" | grep -o "/System/iOSSupport/System/Library/Frameworks/UIKit\.framework/Versions/A/UIKit" | wc -l )
	 	
	 	#
	 	# The binary uses the SwiftUI framework
	 	# Note that Mac Catalyst apps could use SwiftUI (link to /System/iOSSupport/System/Library/Frameworks/SwiftUI.framework/Versions/A/SwiftUIs)
	 	#
	 	useSwiftUI=$( echo "${otoolOutput}" | grep -o "/System/Library/Frameworks/SwiftUI\.framework/Versions/A/SwiftUI" | wc -l )
	 	if [ ${useSwiftUI} != 0 ]
	 	then
	 		#
			# Run nm on the binary
			#
			nmOutput=$( nm "$1" )
	 		isSwiftUIApp=$( echo "${nmOutput}" | grep -o "s7SwiftUI3AppPAAE4mainyyFZ" | wc -l )
	 		isAppKitApp=$( echo "${nmOutput}" | grep -o "NSApplicationMain" | wc -l )
	 		isUIKitApp=$( echo "${nmOutput}" | grep -o "UIApplicationMain" | wc -l )
	 	fi
	 	
	 	#
	 	# Check if the binary links to AppKit
	 	#
	 	useAppKit=$( echo "${otoolOutput}" | grep -o "/System/Library/Frameworks/AppKit\.framework/Versions/C/AppKit" | wc -l )

	 	
	 	#
	 	# Print
	 	#
	 	if [ ${useUIKit} != 0 ]
	 	then
	 		if [ ${useSwiftUI} != 0 ]
	 		then
	 			echo "   $1 : Catalyst with SwiftUI"
	 		else
	 			echo "   $1 : Catalyst without SwiftUI"
	 		fi
	 	elif [ ${isSwiftUIApp} != 0 ]
	 	then
	 		if [ ${useAppKit} != 0 ]
	 		then
	 			echo "   $1 : SwiftUI with AppKit"
	 		else
	 			echo "   $1 : SwiftUI without AppKit"
	 		fi
	 	elif [ ${isAppKitApp} != 0 ]
	 	then
	 		if [ ${useSwiftUI} != 0 ]
	 		then
	 			echo "   $1 : AppKit with SwiftUI"
	 		else
	 			echo "   $1 : AppKit without SwiftUI"
	 		fi
	 	elif [ ${useSwiftUI} != 0 ]
	 	then
	 		# These are for example plugins like /System/Library/Assistant/UIPlugins/SiriFindMyUIPlugin.siriUIBundle/Contents/MacOS/SiriFindMyUIPlugin
	 		if [ ${useAppKit} != 0 ]
	 		then
	 			echo "   $1 : SwiftUI with AppKit*"
	 		else
	 			echo "   $1 : SwiftUI without AppKit*"
	 		fi
	 	elif [ ${useAppKit} != 0 ]
	 	then
	 		if [ ${useSwiftUI} != 0 ]
	 		then
	 			echo "   $1 : AppKit with SwiftUI*"
	 		else
	 			echo "   $1 : AppKit without SwiftUI*"
	 		fi
	 	fi
	fi
}

#---------------------------------------------------------------------
# Check if the script was called with the expected usage
#---------------------------------------------------------------------
PARAMETER_NUMBER=$#
PARAMETER_REQUIRED=1
if [ $PARAMETER_NUMBER != $PARAMETER_REQUIRED ];
then
	printUsage
	exit 1
fi


#---------------------------------------------------------------------
# Get the folder path
#---------------------------------------------------------------------
PATH_TO_CHECK="$1"

echo ""
echo "Start time:"
date
echo ""
echo "Executables found in ${PATH_TO_CHECK}"


#---------------------------------------------------------------------
# Export the functions so that the subshell inherits them
#---------------------------------------------------------------------
export -f isExecutable
export -f processFile

#---------------------------------------------------------------------
# Find all the regular files in all subdirectories
# and call for each the function processFile()
#---------------------------------------------------------------------

echo "$1"
find ${PATH_TO_CHECK} -type f -exec bash -c 'processFile "$0"' {} \;


#---------------------------------------------------------------------
# Finalizing
#---------------------------------------------------------------------
echo ""
echo "Completed at:"
date
echo ""