#!/bin/bash
# joevt Jul 16, 2024

alias icns2icns=/Volumes/Work/Programming/XcodeProjects/Icons/osxiconutils/joevt-osxiconutils/DerivedData/osxiconutils/Build/Products/Debug/icns2icns
alias icns2image=/Volumes/Work/Programming/XcodeProjects/Icons/osxiconutils/joevt-osxiconutils/DerivedData/osxiconutils/Build/Products/Debug/icns2image
alias image2icns=/Volumes/Work/Programming/XcodeProjects/Icons/osxiconutils/joevt-osxiconutils/DerivedData/osxiconutils/Build/Products/Debug/image2icns

patmatch () {
	# substitute for [[ =~ ]] for Mac OS X 10.4
	perl -0777 -ne '<>; exit !( $_ =~ /'"$1"'/ )'
}

dumpicnslist () {
	local theicon="$1"
	local dumps="$2"
	local filelen="$3"
	local pos="$4"

	while (( pos < filelen )); do
		local datapos=0
		local datalen=0
		local header=""

		local blob=""
		blob="$(xxd -s "$pos" -l 32 -p -c 32 "$theicon")"
		local datatypehex="${blob:0:8}"
		local datatype=""
		datatype="$(printf "\x${blob:0:2}\x${blob:2:2}\x${blob:4:2}\x${blob:6:2}")"

		printf "%08X" "$pos" # because Mac OS X 10.4.11 xxd doesn't support -o option
		if ((pos == 0)) && [[ $datatypehex = $'\x89PNG' ]]; then
			((datapos = pos))
			((datalen = filelen - pos))
			datatype="PNG"
			header="${blob:0:24}"
			printf "%s" "${blob}" | xxd -r -p | xxd -g 4 -l 32 -c 32 | sed -E 's/^[^:]*//'
		else
			datalen="$((0x${blob:8:8}))"
			if ((datalen < 8)); then
				datalen=8
			fi
			blob="${blob:0:$datalen*2}"
			local header="${blob:16:24}"
			
			((datapos=pos+8))
			((datalen-=8))

			if [[ $datatype = "TOC " ]]; then
				printf "%s" "${blob:0:16}" | xxd -r -p | xxd -g 4 -l 32 -c 32 | sed -E 's/^[^:]*//'
				xxd -g 4 -s $datapos -l $datalen -c 32 "$theicon" | sed "s/^/ /"
				echo
#			lang=C elif [[ $datatype =~ $'icns|slct|sbtp|drop|odrp|open|over|tile|\xFD\xD9\x2F\xA8' ]]; then # not compatible with 10.4.11
			elif lang=C patmatch 'icns|slct|sbtp|drop|odrp|open|over|tile|\xFD\xD9\x2F\xA8' <<< "$datatype" ; then # compatible with 10.4.11
				printf "%s" "${blob:0:16}" | xxd -r -p | xxd -g 4 -l 32 -c 32 | sed -E 's/^[^:]*//'
				dumpicnslist "$theicon" "${dumps}" $((datapos+datalen)) $datapos
			else
				printf "%s" "${blob}" | xxd -r -p | xxd -g 4 -l 32 -c 32 | sed -E 's/^[^:]*//'
			fi
		fi
		
		if [[ -n $dumps ]]; then
			case $header in
				89504e470d0a1a0a*)
					dd "if=$theicon" bs=1 skip=$datapos count=$datalen "of=${dumps}_$(printf "%06x" "$pos")_${datatype}.png" 2> /dev/null
					;;
				0000000c6a5020200d0a870a*)
					dd "if=$theicon" bs=1 skip=$datapos count=$datalen "of=${dumps}_$(printf "%06x" "$pos")_${datatype}.jp2" 2> /dev/null
					;;
				ff4fff51*)
					dd "if=$theicon" bs=1 skip=$datapos count=$datalen "of=${dumps}_$(printf "%06x" "$pos")_${datatype}.j2c" 2> /dev/null
					;;
				62706c6973743030*)
					dd "if=$theicon" bs=1 skip=$datapos count=$datalen "of=${dumps}_$(printf "%06x" "$pos")_${datatype}.plist" 2> /dev/null
					;;
			esac
		fi
		((pos = datapos + datalen))
	done
}

dumpicns () {
	#1 = icns file
	#2 = prefix for png, jpg, etc. extracts
	dumpicnslist "$1" "$2" $(($(stat -f %z "$1") + 0)) 0
}

makeicon () {
	#1 = source icon suite
	#... = hex offset of each icon in the icon suite that you want to include
	local theicon="$1"
	shift
	local theicondata=""
	while (($#)); do
		theoffset="$1"
		shift
		if [[ "${theoffset}" = "-" ]]; then
			thetype="$1"
			shift
			thesize="$1"
			shift
			theicondata=${theicondata}$(printf "%s" "$thetype" | xxd -p)$(printf "%08x" "$thesize")$(xxd -p -l $((thesize - 8)) -c $((thesize - 8)) /dev/random)
		else
			theheader=$(xxd -p -s $((0x$theoffset)) -l 8 "$theicon")
			thesize=$((0x${theheader:8}))
			theicondata=${theicondata}$(xxd -p -s $((0x$theoffset)) -l $thesize -c $thesize "$theicon")
		fi
	done
	printf "69636e73%08x%s" $((${#theicondata} / 2 + 8)) "$theicondata" | xxd -p -r
}

iconfromresource () {
	local srcicon="$1"
	local dsticon="$2"
	DeRez "${srcicon}" -only "'icns' (-16455)" | perl -nE 'if (/^\t\$"([0-9A-F ]+)".*/) { print $1 }' | xxd -p -r > "$dsticon"
}

icontoresource () {
	local srcicon="$1"
	local dsticon="$2"
	{
		printf "data 'icns' (-16455) {\n"
		xxd -p -c 16 "$srcicon" | perl -pE 's/(.*)/\$"\1"/'
		printf "};\n"
	} > "/tmp/iconrez.r"
	Rez "/tmp/iconrez.r" -o "$dsticon"
}

checkiconcompatibility () {
	#.VolumeIcon.icns requirements:
	#- 10.4: icons in an icns file cannot be 300KB or greater otherwise the icns is not used. 1024x1024 icons are usually too large. Some 512x512 icons are too large (depends on png compressibility).
	#- 10.5: The icns file cannot exceed 1MB-2B otherwise the icns is not used.
	#- 10.6: no limits that I've encountered
	#- 10.7: TOC must be ordered if it exists
	#- 10.8: TOC must be ordered if it exists
	#- 10.9 - 12.2: no limits that I've encountered
	#- Startup Manager on old Macs like my MacPro3,1 require it32 icons (at least for the 128x128 size - I haven't checked what icon types can be used)
	#- rEFInd doesn't do icons that have png or jpeg or ARGB data - it only does the RGB+8 icon types (it32 and it's smaller versions).
	#- I added support for png and ARGB to my RefindPlus fork (plus old 8bit, 4bit, 1bit icons! I tested these by converting all icons in a Mac OS 9
	#  system file and ROM resources into icns files). I don't know where to get a jpeg 2000 library that can be ported to EFI to support icns files that have jpeg 2000 icons.

	local disklist=""
	local doingfolders=0
	local copycommand="cp -p"

	if [[ $1 == '-f' ]]; then
		doingfolders=1
		local copycommand="icontoresource"
		disklist=$( find "$2" -type f -name "Icon"$'\r' -exec dirname {} \; )
	elif [[ -n $1 ]]; then
		disklist="$(getallmounteddisks -d | grep -E "$1")"
	else
		disklist="$(getallmounteddisks -d)"
	fi

	local foldernumber="1"
	while [[ -e VolumeIcons"${foldernumber}" ]]; do
		((foldernumber++))
	done

	IFS=$'\n'
	for thepart in $(printf "%s" "$disklist"); do
		local themount=""
		local thedevice=""
		local thevolume=""
		local theicon=""

		if ((doingfolders)); then
			thedevice=""
			themount="$thepart"
			thevolume="$(basename "$themount")"
			theicon="$themount/Icon"$'\r'
		else
			thedevice="${thepart%:*}"
			themount="${thepart#*:}"
			thevolume="$(basename "$themount")"
			theicon="$themount/.VolumeIcon.icns"
		fi

		echo "#•••••$(dirname "$theicon")"
		if [[ -f "$theicon" ]]; then
			local thecount=""
			while [[ -d "VolumeIcons${foldernumber}/$thevolume$thecount" ]]; do
				((thecount++))
			done
			local thedir="VolumeIcons${foldernumber}/$thevolume$thecount"
			mkdir -p "$thedir"
			local theprefix="$thedir/$thevolume"
			if ((doingfolders)); then
				iconfromresource "${theicon}" "$theprefix.icns"
			else
				cp -p "${theicon}" "$theprefix.icns"
			fi
			icns2icns "${theprefix}.icns" "${theprefix}_after.icns" || echo "### Error icns2icns"
			dumpicns "${theprefix}.icns" > "${theprefix}.txt" || echo "### Error dumpicns"
			dumpicns "${theprefix}_after.icns" > "${theprefix}_after.txt" || echo "### Error dumpicns2"

			it32=1
			grep it32 "${theprefix}.txt" > /dev/null || it32=0

			if (( it32 )); then
				echo "# Found it32 in icon at $thepart"
				local toccontents=""
				toccontents="$(perl -ne 'while (<>) {if ((/^.{8}: 544f4320.*/ .. /^$/) && /^ /) { s/^ .{8}:(( \w{8})+).*\n/\1/; s/ (.{8}) .{8}/\1 /g; print "$_"; } } ' < "${theprefix}.txt")"
				if [[ -n $toccontents ]]; then # has a TOC
					local tocexpected=""
					tocexpected="$(perl -ne 'while (<>) {if (!/^00000000/ && !/^\w{8}: 544f4320/ && !/^ / && !/^$/) { s/\w{8}: (\w{8}).*\n/\1/; print "$_ "; } } ' < "${theprefix}.txt")"
					if [[ $toccontents != "$tocexpected" ]]; then
						echo "#TOC was not sorted"
						printf '%s "%s" "%s"' "$copycommand" "${theprefix}_after.icns" "$theicon" | perl -0777 -pE 's/\r/"\$'"'"'\\r'"'"'"/g'
						echo
					fi
				fi
				if [[ -z $device ]] || [[ "$(getvolumeproperty "$thedevice" FilesystemType)" != "apfs" ]]; then
					# we only need to worry about icon sizes in partitions that are viewable in old macOS versions which are unable to mount apfs partitions
					IFS=$'\n'
					local iconoffset=""
					local iconsize=""
					local icontype=""
					local includedicons=""
					local excludedicons=0
					local totalsize=0
					local includedsizes=0
					for theline in $(sed -nE '/^([0-9A-Fa-f]+): .{8} (.{8}) .{53}  (....).*/s//iconoffset=\1;iconsize=$((0x\2));icontype="\3"/p' "${theprefix}.txt"); do
						eval "$theline"
						if [[ $icontype = "icns" ]]; then
							totalsize=iconsize
						elif [[ $icontype != "icns" ]] && (( iconsize >= 307200 )); then
							echo "#icon at $iconoffset is too large"
							((excludedicons++))
						elif [[ $icontype != 'TOC ' ]]; then
							includedicons="${includedicons} ${iconoffset}"
							((includedsizes += iconsize))
						fi
					done

					if (( totalsize > 1048574 )); then
						printf "#icon file is too large by %x bytes" $((totalsize - 1048574))
						if (( includedsizes > 1048574 )); then
							if (( includedsizes == totalsize )); then
								printf "\n"
							else
								printf " or %x bytes after remaking the icon\n" $((includedsizes - 1048574))
							fi
						else
							printf " but remaking the icon is sufficient\n"
						fi
						((excludedicons++))
					fi
				
					if ((excludedicons)); then
						echo 'bbedit "'"${theprefix}.txt"'"'
						echo 'makeicon "'"${theprefix}.icns"'"'"${includedicons}"' > "'"${theprefix}_reduced.icns"'"'
						printf '%s "%s" "%s"' "$copycommand" "${theprefix}_reduced.icns" "$theicon" | perl -0777 -pE 's/\r/"\$'"'"'\\r'"'"'"/g'
						echo
					fi
				fi
			else
				it32after=1
				grep it32 "${theprefix}_after.txt" > /dev/null || it32after=0
				if (( it32after )); then
					echo "#it32 added"
					printf '%s "%s" "%s"' "$copycommand" "${theprefix}_after.icns" "$theicon" | perl -0777 -pE 's/\r/"\$'"'"'\\r'"'"'"/g'
					echo
				else
					echo "#it32 didn't get added - try explicitly adding it"
					icns2image "${theprefix}.icns" "${theprefix}.tiff" || echo "### Error icns2image"
					image2icns "${theprefix}.tiff" "${theprefix}_after2.icns" || echo "### Error image2icns"
					dumpicns "${theprefix}_after2.icns" > "${theprefix}_after2.txt" || echo "### Error dumpicns2"
					printf '%s "%s" "%s"' "$copycommand" "${theprefix}_after2.icns" "$theicon" | perl -0777 -pE 's/\r/"\$'"'"'\\r'"'"'"/g'
					echo
				fi
			fi
		else
			echo "# No icon at $thepart"
		fi
	done 2>&1
}