#! /bin/sh
# vim: set tabstop=4 syntax=sh :
# SPDX-License-Identifier: GPL-2.0-or-later
#######################################################################################################
#                                                                                                     #
# copy data with AVM's FIT format from a specified block device                                       #
#                                                                                                     #
###################################################################################################VER#
#                                                                                                     #
# fit-get-image-from-blockdevice, version 0.2                                                         #
#                                                                                                     #
# This script is a part of the YourFritz project from https://github.com/PeterPawn/YourFritz.         #
#                                                                                                     #
###################################################################################################CPY#
#                                                                                                     #
# Copyright (C) 2022 P.Haemmerlein (peterpawn@yourfritz.de)                                           #
#                                                                                                     #
###################################################################################################LIC#
#                                                                                                     #
# This project is free software, you can redistribute it and/or modify it under the terms of the GNU  #
# General Public License as published by the Free Software Foundation; either version 2 of the        #
# License, or (at your option) any later version.                                                     #
#                                                                                                     #
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without   #
# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU       #
# General Public License under http://www.gnu.org/licenses/gpl-2.0.html for more details.             #
#                                                                                                     #
#######################################################################################################
#                                                                                                     #
# This script takes a block-device name as first (and only) parameter and extracts data with the      #
# correct length (taken from header fields) to STDOUT, where it may be redirected to a file.          #
#                                                                                                     #
#######################################################################################################
#                                                                                                     #
# the whole logic as a sealed sub-function                                                            #
#                                                                                                     #
#######################################################################################################
get_avm_image_from_blockdevice()
(
	action_message=""
	printf_ss() {
		mask="$1"
		shift
		# shellcheck disable=SC2059
		printf -- "$mask" "$@"
	}
	result() {
		[ "$dbg" = "1" ] || return 0
		if [ "$1" = "0" ]; then
			color="$__yf_ansi_bright_green__"
		elif [ "$1" = "1" ]; then
			color="$__yf_ansi_bright_red__"
		else
			color="$__yf_ansi_yellow__"
		fi
		shift
		printf -- "%s" "$color" 1>&2
		debug "$@"
		printf -- "%s\n" "$__yf_ansi_reset__" 1>&2
	}
	debug() {
		[ "$dbg" = "1" ] || return 0
		printf_ss "$@" 1>&2
	}
	action() {
		[ "$dbg" = "1" ] || return 0
		action_message="$(printf_ss "$@")"
		debug "$@"
	}
	indent() { i=1; while [ "$i" -lt "${1:-1}" ]; do printf -- ">> "; i=$(( i + 1 )); done; printf -- " "; }
	sbo() (
		sbo_ro()
		{
			v1=0; v2=0; v3=0; v4=0
			while read -r p _ rt; do
				[ "$p" -gt 5 ] && exit 1
				[ "$p" -eq 5 ] && [ "$rt" -ne 0377 ] && exit 1
				if [ "$p" -eq 5 ]; then
					for i in $v4 $v3 $v2 $v1; do
						printf -- "%b" "\0$(printf -- "%o" "$i")"
					done
					exit 0
				fi
				eval "v$p"=$(( 0$rt ))
			done
			exit 1
		}
		( cat; printf -- "%b" "\377" ) | cmp -l -- "$zeros" - 2>"$null" | sbo_ro
	)
	b2d() (
		b2d_ro()
		{
			i=1; v=0; ff=0
			while read -r p _ rt; do
				if [ "$ff" -eq 1 ]; then
					v=$(( v * 256 ))
					ff=255
					v=$(( v + ff ))
					i=$(( i + 1 ))
					ff=0
				fi
				while [ "$i" -lt "$p" ]; do
					v=$(( v * 256 ))
					i=$(( i + 1 ))
				done
				if [ "$rt" = 377 ] && [ $ff -eq 0 ]; then
					ff=1
					continue
				fi
				v=$(( v * 256 ))
				rt=$(( 0$rt ))
				v=$(( v + rt ))
				i=$(( p + 1 ))
			done
			printf -- "%d" $v
		}
		( cat; printf -- "%b" "\377" ) | cmp -l -- "$zeros" - 2>"$null" | b2d_ro
		return 0
	)
	get_data() ( dd if="$1" bs="$3" count=$(( ( $2 / $3 ) + 1 )) skip=1 2>"$null" | dd bs=1 count="$2" 2>"$null"; )
	get_fdt32_be() ( get_data "$1" 4 "$2" | b2d; )
	get_fdt32_le() ( get_data "$1" 4 "$2" | sbo | b2d; )
	__yf_ansi_sgr() { printf -- '\033[%sm' "$1"; }
	__yf_ansi_bold__="$(__yf_ansi_sgr 1)"
	__yf_ansi_yellow__="$(__yf_ansi_sgr 33)"
	__yf_ansi_bright_red__="$(__yf_ansi_sgr 91)"
	__yf_ansi_bright_green__="$(__yf_ansi_sgr 92)"
	__yf_ansi_reset__="$(__yf_ansi_sgr 0)"
	__yf_get_script_lines() {
		sed -n -e "/^#*${1}#\$/,/^#\{20\}.*#\$/p" -- "$0" | \
		sed -e '1d;$d' | \
		sed -e 's|# \(.*\) *#$|\1|' | \
		sed -e 's|^#*#$|--|p' | \
		sed -e '$d' | \
		sed -e 's| *$||'
	}
	__yf_show_script_name() {
		[ -n "$1" ] && printf -- '%s' "$1"
		printf -- '%s' "${0#*/}"
		[ -n "$1" ] && printf -- "%s" "${__yf_ansi_reset__}"
	}
	__yf_show_license() { __yf_get_script_lines 'LIC'; }
	__yf_show_version() {
		printf -- "\n%s%s%s, " "${__yf_ansi_bold__}" "$(__yf_get_script_lines 'VER' | sed -n -e "2s|^\([^,]*\),.*|\1|p")" "${__yf_ansi_reset__}"
		v_display="$(__yf_get_script_lines 'VER' | sed -n -e "2s|^[^,]*, \(.*\)|\1|p")"
		printf -- "%s\n" "$v_display"
	}
	__yf_show_copyright() { __yf_get_script_lines 'CPY'; }
	usage() {
		exec 1>&2
		__yf_show_version
		__yf_show_copyright
		__yf_show_license
		printf -- "\n"
		printf -- "%sfit-get-avm-image-from-blockdevice.sh%s - copy only data being the real FIT image in a greater partition to STDOUT\n\n" "${__yf_ansi_bold__}" "${__yf_ansi_reset__}"
		printf -- "Usage: %s [ options ] <block-device>\n\n" "$0"
		printf -- "Options:\n\n"
		printf -- "-d or --debug - show some extra info on STDERR\n"
		printf -- "\n"
		exec 1>&2
	}
	#
	# time-optimized copying of data with offset and known size
	#
	# $1 = input file
	# $2 = start offset
	# $3 = data length to copy
	# $4 = block size to use (best a tradeoff between time and memory used), default is 1 MB
	#
	copy_optimized() (
		lvl=$(( ${5:-0} + 1 ))
		def_bsz=$(( 1024 * 1024 ))
		bsz=$(( ${4:-$def_bsz} ))
		[ $(( bsz & ( bsz - 1 ) )) -gt 0 ] && bsz=$def_bsz
		cnt=$(( $3 ))
		[ $cnt -le 0 ] && exit 0
		off=$(( $2 ))
		[ $off -lt 0 ] && exit 1
		if [ $cnt -lt $bsz ]; then
			if [ $(( off % bsz )) -ne 0 ]; then
				if [ $bsz -gt 1024 ]; then
					copy_optimized "$1" $off $(( cnt & 1023 )) 1024 $lvl
					shft=$(( 1024 - ( off % 1024 ) ))
					cnt=$(( cnt - shft ))
					off=$(( off + shft ))
					copy_optimized "$1" $off $cnt 1024 $lvl
					exit 0
				fi
				of=$(( off ))
				s=1
				while [ $(( of & 1 )) -eq 0 ]; do s=$(( s * 2 )); of=$(( of / 2 )); done
				skp=$(( off / s ))
				cnt=$(( cnt / s ))
				r=0
			else
				s=1
				c=$(( cnt & ~1023 ))
				while [ $(( c % 2 )) -eq 0 ] && [ $c -gt 0 ]; do s=$(( s * 2 )); c=$(( c / 2 )); done
				skp=$(( off / s ))
				[ $s -eq 1 ] && r=0 || r=$(( cnt & 1023 ))
				cnt=$(( cnt / s ))
				rb=1024
			fi
			action "%sCopying %u (%#x) data blocks of size %u (%#X) starting at offset %u (%#x) ..." "$(indent "$lvl")" "$cnt" "$cnt" "$s" "$s" "$off" "$off"
			if dd if="$1" bs="$s" skip="$skp" count="$cnt" 2>"$null"; then
				result "0" " OK"
				if [ $r -ne 0 ]; then
					copy_optimized "$1" $(( ( skp + cnt ) * s )) $r $rb $lvl
				fi
				exit 0
			else
				result "1" " failed"
				exit 1
			fi
		else
			if [ $(( off % bsz )) -ne 0 ]; then
				copy_optimized "$1" $off $(( bsz - ( off % bsz ) )) $bsz $lvl
				shft=$(( bsz - ( off % bsz ) ))
				off=$(( off + shft ))
				cnt=$(( cnt - shft ))
				copy_optimized "$1" $off $cnt $bsz $lvl
			else
				s=$(( off / bsz ))
				c=$(( cnt / bsz ))
				action "%sCopying %u (%#x) data blocks of size %u (%#X) starting at offset %u (%#x) ..." "$(indent "$lvl")" "$c" "$c" "$bsz" "$bsz" "$off" "$off"
				if dd if="$1" bs="$bsz" skip="$s" count="$c" 2>"$null"; then
					result "0" " OK"
					if [ $cnt -gt $(( c * bsz )) ]; then
						shft=$(( c * bsz ))
						off=$(( off + shft ))
						cnt=$(( cnt - shft ))
						copy_optimized "$1" $off $cnt $bsz $lvl
					fi
					exit 0
				else
					result "1" " failed"
					exit 1
				fi
			fi
		fi
	)
	# shellcheck disable=SC2015
	dev_info() ( [ -z "$2" ] && udevadm info -q all -n "$1" || { udevadm info -q all -n "$1" | sed -n -e "s|^E: $2=\(.*\)|\1|p"; } )
	mtd_type() ( [ "$(dev_info "$1" DEVTYPE)" = "mtd" ] && cat "/sys$(dev_info "$1" DEVPATH)/type" && exit 0 || exit 1; )
	nand_pagesize() ( [ "$(dev_info "$1" DEVTYPE)" = "mtd" ] && [ "$(mtd_type "$1")" = "nand" ] && cat "/sys$(dev_info "$1" DEVPATH)/subpagesize" && exit 0 || exit 1; )
	copy_image() (
		case "$1" in
			("partition"|"nor")
				dd if="$2" bs="$3" count=1 2>"$null"
				;;
			("nand")
				! command -v nanddump 2>"null" 1>&2 && printf -- "Missing 'nanddump' utility.\a\n" 1>&2 && exit 1
				tmpdir="${TMP:-$TMPDIR}"
				[ -z "$tmpdir" ] && tmpdir="/tmp"
				out="$tmpdir/nanddump-$$"
				trap '[ -f "$out" ] && rm -f "$out" 2>/dev/null' EXIT
				pagesize="$(nand_pagesize "$2")"
				"$(command -v nanddump 2>"$null")" --bb skipbad "$2" 2>"$null" | dd bs="$pagesize" count="$(( $3 / pagesize + 1 ))" of="$out" 2>"$null" || exit 1
				dd if="$null" of="$out" bs="$3" seek=1 2>"$null" && cat "$out" 2>"$null" && exit 0
				exit 1
				;;
			(*)
				printf -- "Unable to detect device type of FIT image source (%s) or this type (%s) is unsupported.\a\n" "$1" "$([ -z "$type" ] && printf -- "(unknown)" || printf "%s\n" "$type")" 1>&2
				exit 1
				;;
		esac
	)

	null="/dev/null"
	zeros="/dev/zero"
	fdt32_size=4

	dbg=0
	while [ "$(expr "$1" : "\(.\).*")" = "-" ]; do
		[ "$1" = "--" ] && shift && break

		if [ "$1" = "-d" ] || [ "$1" = "--debug" ]; then
			dbg=1
			shift
		elif [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
			usage
			exit 0
		else
			printf -- "Unknown option: %s\a\n" "$1" 1>&2 && exit 1
		fi
	done

	img="$1"
	if [ "$dbg" = "1" ]; then
		__yf_show_version 1>&2
		__yf_show_copyright 1>&2
		debug "\n"
	fi
	! command -v udevadm 2>"$null" 1>&2 && printf -- "Missing 'udevadm' utility.\a\n" 1>&2 && exit 1
	[ -z "$img" ] && printf -- "Missing input source parameter.\a\n" 1>&2 && exit 1
	debug "Input device/file: %s%s%s\n" "$__yf_ansi_bright_green__" "$img" "$__yf_ansi_reset__"

	[ -f "$img" ] && devtype="file" || devtype="$(dev_info "$1" DEVTYPE)"
	[ "$devtype" = "mtd" ] && devtype="$(mtd_type "$1")"
	debug "Input data source type: %s%s%s\n" "$__yf_ansi_bright_green__" "$devtype" "$__yf_ansi_reset__"

	debug "Looking for magic value at offset 0x00 ..."
	if ! [ "$(dd if="$img" bs=4 count=1 2>"$null" | b2d)" = "218164734" ]; then
		debug "$(result 1 " failed")"
		printf -- "Missing magic value (0x0d 0x00 0xed 0xfe) from specified input file '%s'.\a\n" "$img" 1>&2
		exit 1
	else
		debug "$(result 0 " OK")"
	fi

	offset=$(( fdt32_size ))
	payload_size="$(get_fdt32_le "$img" "$offset")"
	debug "FDT payload size at offset 0x04: %s%u%s (%#x)\n" "$__yf_ansi_bright_green__" "$payload_size" "$__yf_ansi_reset__" "$payload_size"

	offset=$(( offset + fdt32_size ))
	size=64
	if [ "$dbg" = "1" ] && command -v hexdump 2>"$null" 1>&2; then
		debug "\nData at offset 0x%02x, size %u:\n\n%s" "$offset" "$size" "$__yf_ansi_yellow__"
		get_data "$img" "$size" "$offset" | hexdump -C | sed -n -e "1,$(( size / 16 ))p" 1>&2 2>"$null"
		debug "%s\n" "$__yf_ansi_reset__"
	fi
	offset=$(( offset + size ))

	fdt_magic="$(get_fdt32_be "$img" "$offset")"
	if { [ "$fdt_magic" -gt 0 ] && [ "$fdt_magic" -ne 3490578157 ]; } || { [ "$fdt_magic" -lt 0 ] && [ "$fdt_magic" -ne -804389139 ]; }; then
		printf "Invalid FDT magic found at offset 0x%02x: 0x%02x%06x\a\n" "$offset" $(( ( fdt_magic >> 24 ) & 0xff )) $(( fdt_magic & 0xffffff )) 1>&2
		exit 1
	fi

	fdt_totalsize="$(get_fdt32_be "$img" $(( offset + fdt32_size )))"
	if [ "$fdt_totalsize" -ne "$payload_size" ]; then
		printf "Payload size (%u = %#x) at offset 0x%02x doesn't match FDT data size at offset 0x%02x (%u = %#x), processing aborted.\a\n" "$payload_size" "$payload_size" "4" "$offset" "$fdt_totalsize" "$fdt_totalsize" 1>&2
		exit 1
	fi

	[ -t 1 ] && printf -- "STDOUT is a terminal device, output suppressed.\a\n" 1>&2 && exit 1

	payload_size=$(( payload_size + 64 + 8 + 8 ))
	offset=0
	action "Copying %u (%#x) bytes of data starting from offset %u (%#x) ..." "$payload_size" "$payload_size" "$offset" "$offset"
	[ "$dbg" = "1" ] && result 2 " running"
	[ "$devtype" = "file" ] && cmd="copy_optimized \"$img\" \"$offset\" \"$payload_size\"" || cmd="copy_image \"$devtype\" \"$img\" \"$payload_size\""
	if eval "$cmd"; then
		[ "$dbg" = "1" ] && debug "$action_message"
		result 0 " OK"
		debug "\n"
		exit 0
	else
		[ "$dbg" = "1" ] && debug "$action_message"
		result 1 " failed"
		debug "\n"
		exit 1
	fi
)
#######################################################################################################
#                                                                                                     #
# invoke sealed function from above                                                                   #
#                                                                                                     #
#######################################################################################################
get_avm_image_from_blockdevice "$@"
#######################################################################################################
#                                                                                                     #
# end of script                                                                                       #
#                                                                                                     #
#######################################################################################################
