#! /bin/bash
# vim: set tabstop=4 shiftwidth=4 noexpandtab filetype=sh:

# 2-25-11: tested on 10.6.5 and Ubuntu 10.10. Final.

# to do: allow hex offsets

# These bash functions perform single-byte dec-hex-character conversions
# and file read/write operations, and hopefully work identically across
# different platforms. Each can operate by itself without the presence
# of the others. They have been tested on Mac OS X 10.6 and
# Ubuntu Linux 10.10. Your mileage may vary.

# You provide parameters to the functions as arguments, or alternatively
# standard in (for the functions which accept characters). Examples:
#   Write hex byte with value "F0" to offset 23 in file "myFile":
# writecharHex myFile 23 F0
#   Write "ABCDE" to the beginning of file "myFile"
# echo "ABCDE" | writechars myFile

# For functions which output something (all but the write operations),
# you can call the functions with command substitution if you want to
# assign the output to a variable rather than display it. Examples:
#   Convert decimal value 65 to its hexadecimal equivalent:
# val=$(decToHex 65)
#   Get decimal value of the character/byte at offset 215 in "myFile":
# val=$(readcharDec "myFile" 215)

# For functions which convert to or from a character, 0-127 will be
# ASCII/UTF-8, while 128-255 will be system/locale/codepage dependent.
# In this context, a character is effectively the same as a byte.

# The functions return a non-zero exit status for invalid or missing
# arguments. If you don't need these checks, remove the lines
# above the comment "args are valid" (or as otherwise noted).
# The exit statuses are, generally:
#    0 = no error
#    8 = extraneous argument
#    9 = standard input is invalid
#   1x = missing required argument (e.g. 11 for missing argument 1)
#   2x = argument is invalid (e.g. 22 for invalid argument 2)
# any other exit status will originate from the final command in the
# function (e.g. dd, printf)

# For the functions which output chars (readchars, decToChar, and
# hexToChar), be aware that NUL (0) and trailing LF (10/0x0A) chars will
# be stripped when assigned to a variable, and cannot appear in an
# argument. To preserve them, pipe the output elsewhere, such as into
# charToDec, charToHex, writechars, or a command. (readcharDec and
# readcharHex handle these characters correctly.)

# questions/comments to ivan@ivanx.com

decToHex () {
# converts single-byte decimal value to hexadecimal equivalent
# arg: decimal value from 0-255
# out: two-digit hex value from 00-FF
#exit: 8=extraneous arg, 11=missing arg, 21=invalid arg
	[[ $1 ]] || return 11
	[[ $2 ]] && return 8
	[[ ( $(printf %d "$1" 2> /dev/null) == $1 ) \
		&& ( $1 -ge 0 ) && ( $1 -le 255 ) ]] || return 21
	# args are valid
	printf %02X "$1"
}

hexToDec () {
# converts single-byte hexadecimal value to decimal equivalent
# arg: two-digit hex value from 00-FF
# out: decimal value
#exit: 8=extraneous arg, 11=missing arg, 21=invalid arg
	[[ $1 ]] || return 11
	[[ $2 ]] && return 8
	[[ ${#1} -eq 2 ]] || return 21
	[[ $(printf %02X "0x$1" 2> /dev/null) == \
		$(echo -n "$1" | tr [a-z] [A-Z]) ]] || return 21
	# args are valid
	printf %d "0x$1"
}

charToDec () {
# converts single character to corresponding decimal value
# stdin OR arg: one character
#  [arg overrides stdin; stdin is required for NUL (0) or LF (0x0A)]
# out: decimal value from 0-255
#exit: 8=extraneous arg, 9=invalid stdin,
#      11=missing stdin/arg, 21=invalid arg
	[[ ( ! -t 0 ) && $1 ]] && { cat > /dev/null; return 8; }
	[[ ( -t 0 ) ]] && { [[ $2 ]] && return 8; [[ $1 ]] || return 11; }
	# arg/stdin is potentially valid (additional check below)
	charX="$1X"; [[ $1 ]] || charX="$(cat; echo -n 'X';)"
	[[ ${#charX} -le 2 ]] || return $(( $([[ $1 ]]; echo $?) ? 9 : 21 ))
	# above line verifies that arg/stdin is valid
	[[ ${#charX} -ne 2 ]] && { echo -n 0; return 0; }
	echo -n "${charX:0:1}" | od -t u1 | \
		head -1 | sed 's/[0\ ]*//' | tr -d ' \n'
}

charToHex () {
# converts single character to corresponding hexadecimal value
# stdin OR arg: one character
#  [arg overrides stdin; stdin is required for NUL (0) or LF (0x0A)]
# out: decimal value from 0-255
#exit: 8=extraneous arg, 9=invalid stdin,
#      11=missing stdin/arg, 21=invalid arg
	[[ ( ! -t 0 ) && $1 ]] && { cat > /dev/null; return 8; }
	[[ ( -t 0 ) ]] && { [[ $2 ]] && return 8; [[ $1 ]] || return 11; }
	# arg/stdin is potentially valid (additional check below)
	charX="$1X"; [[ $1 ]] || charX="$(cat; echo -n 'X';)"
	[[ ${#charX} -le 2 ]] || return $(( $([[ $1 ]]; echo $?) ? 9 : 21 ))
	# above line verifies that stdin/arg is valid
	[[ ${#charX} -ne 2 ]] && { echo -n "00"; return 0; }
	printf %02X $(echo -n "${charX:0:1}" | od -t u1 | \
		head -1 | sed 's/[0\ ]*//' | tr -d ' \n')
}

decToChar () {
# converts single-byte decimal value to equivalent character
# arg: decimal number from 0-255
# out: one character
#exit: 8=extraneous arg, 11=missing arg, 21=invalid arg
	[[ $1 ]] || return 11
	[[ $2 ]] && return 8
	[[ ( $(printf %d "$1" 2> /dev/null ) == $1 ) \
		&& ( $1 -ge 0 ) && ( $1 -le 255 ) ]] || return 21
	# args are valid
	echo -n -e "\x$(printf %02X "$1")"
}

hexToChar () {
# converts single-byte hexadecimal value to corresponding character
# arg: two-digit hexadecimal number from 00-FF
# out: one character
#exit: 8=extraneous arg, 11=missing arg, 21=invalid arg
	[[ $1 ]] || return 11
	[[ $2 ]] && return 8
	[[ ${#1} -eq 2 ]] || return 21
	[[ $(printf %02X "0x$1" 2> /dev/null) == \
		$(echo -n "$1" | tr [a-z] [A-Z]) ]] || return 21
	# args are valid
	echo -n -e "\x$1"
}

readchars () {
# read one or more characters from a file
# arg1: filename
# arg2: (optional) offset (# of bytes to skip before reading)
# arg3: (optional) # of chars to read (default is until end of file)
#  out: sequence of characters
# exit: 8=extraneous arg, 11=missing arg1,
#       21=invalid arg1, 22=invalid arg2, 23=invalid arg3
	[[ $1 ]] || return 11
	[[ $4 ]] && return 8
	[[ -f $1 ]] || return 21
	[[ $2 ]] && { [[ ( $(printf %d "$2" 2> /dev/null) == $2 ) \
		&& ( $2 -ge 0 ) ]] || return 22; }
	[[ $3 ]] && { [[ ( $(printf %d "$3" 2> /dev/null) == $3 ) \
		&& ( $3 -ge 0 ) ]] || return 23; }
	# args are valid
	dd if="$1" bs=1 skip=$(($2)) $([[ $3 ]] && echo -n "count=$3") \
		2> /dev/null | tr -d ' \0'
}

readcharDec () {
# read one character from file & convert to equivalent decimal value
# arg1: filename
# arg2: (optional) offset (# of bytes to skip before reading)
#  out: decimal value from 0-255
# exit: 8=extraneous arg, 11=missing arg1,
#       21=invalid arg1, 22=invalid arg2
	[[ $1 ]] || return 11
	[[ $3 ]] && return 8
	[[ -f $1 ]] || return 21
	[[ $2 ]] && { [[ ( $(printf %d "$2" 2> /dev/null) == $2 ) \
		&& ( $2 -ge 0 ) ]] || return 22; }
	# args are valid
	charX="$(dd if="$1" bs=1 skip=$(($2)) \
		count=1 2> /dev/null | tr -d '\0'; echo -n X)"
	[[ ${#charX} -gt 1 ]] || { echo -n 0; return 0; }
	echo -n "${charX:0:1}" | od -t u1 | \
		head -1 | sed 's/[0\ ]*//' | tr -d ' \n'
}

readcharHex () {
# read one character from file & convert to corresponding hex value
# arg1: filename
# arg2: (optional) offset (# of bytes to skip before reading)
#  out: two-digit hex value from 00-FF
# exit: 8=extraneous arg, 11=missing arg1,
#       21=invalid arg1, 22=invalid arg2
	[[ $1 ]] || return 11
	[[ $3 ]] && return 8
	[[ -f $1 ]] || return 21
	[[ $2 ]] && { [[ ( $(printf %d "$2" 2> /dev/null) == $2 ) \
		&& ( $2 -ge 0 ) ]] || return 22; }
	# args are valid
	charX="$(dd if="$1" bs=1 skip=$(($2)) \
		count=1 2> /dev/null | tr -d '\0'; echo -n X)"
	[[ ${#charX} -gt 1 ]] || { echo -n "00"; return 0; }
	printf %02X $(echo -n "${charX:0:1}" | od -t u1 | \
		head -1 | sed 's/[0\ ]*//' | tr -d ' \n')
}

### 2-15-11 above tested on OS X and Linux

writechars () {
# write one or more characters (bytes) to file
# arg1: filename
# arg2: (optional) offset (# of bytes to skip before writing)
# arg3 OR stdin: sequence of characters
#  [stdin required if writing NUL (0) or trailing LF (0x0A) chars]
#  out: nothing
# exit: 8=extraneous arg, 11=missing arg1,
#       13=missing stdin/arg3, 22=invalid arg2
	[[ $1 ]] || { [[ -t 0 ]] || cat > /dev/null; return 11; }
	[[ $2 ]] && { [[ ( $(printf %d "$2" 2> /dev/null) == $2 ) && \
		( $2 -ge 0 ) ]] || { [[ -t 0 ]] || cat > /dev/null; return 22; } }
	[[ ( ! -t 0 ) && $3 ]] && { cat > /dev/null; return 8; }
	[[ ( -t 0 ) ]] && { [[ $4 ]] && return 8; [[ $3 ]] || return 13; }
	# args are valid
	if [[ -t 0 ]]; then
		echo -n "$3" | \
			dd of="$1" bs=1 seek=$(($2)) conv=notrunc 2> /dev/null
	else
		dd of="$1" bs=1 seek=$(($2)) conv=notrunc 2> /dev/null
	fi
}

writecharDec () {
# write corresponding character of single-byte decimal value into file
# arg1: filename
# arg2: offset (# of bytes to skip before writing)
# arg3: decimal number from 0-255
# exit: 8=extraneous arg, 11=missing arg1, 12=missing arg2,
#       13=missing arg3, 22=invalid arg2, 23=invalid arg3
#  out: nothing
	[[ $1 ]] || return 11; [[ $2 ]] || return 12; [[ $3 ]] || return 13
	[[ $4 ]] && return 8
	[[ ( $(printf %d "$2" 2> /dev/null) == $2 ) \
		&& ( $2 -ge 0 ) ]] || return 22
	[[ ( $(printf %d "$3" 2> /dev/null) == $3 ) \
		&& ( $3 -ge 0 ) && ( $3 -lt 255 ) ]] || return 23
	# args are valid
	echo -n -e "\x$(printf %02X "$3")" | \
		dd of="$1" bs=1 seek=$(($2)) conv=notrunc 2> /dev/null
}

writecharHex () {
# write corresponding character of single-byte hex value into file
# arg1: filename
# arg2: offset (# of bytes to skip before writing)
# arg3: two-digit hexadecimal number from 00-FF
#  out: nothing
# exit: 8=extraneous arg, 11=missing arg1, 12=missing arg2,
#       13=missing arg3, 22=invalid arg2, 23=invalid arg3
	[[ $1 ]] || return 11; [[ $2 ]] || return 12; [[ $3 ]] || return 13
	[[ $4 ]] && return 8
	[[ ( $(printf %d "$2" 2> /dev/null) == $2 ) \
		&& ( $2 -ge 0 ) ]] || return 22
	[[ $(printf %02X "0x$3" 2> /dev/null) == \
		$(echo -n "$3" | tr [a-z] [A-Z]) ]] || return 23
	# args are valid
	echo -n -e "\x$3" | \
		dd of="$1" bs=1 seek=$2 conv=notrunc 2> /dev/null
}


# --- afptype is below this line

isHexByte () {
	[[ $(printf %02X "0x$1" 2> /dev/null) == \
	$(echo -n "$1" | tr [a-z] [A-Z]) ]] || return 1
}

# support 00 and 0A as filetype chars?

debug=1
ptypes="04:TXT 06:BIN B3:S16 E0:SHK F9:P16 FA:INT FC:BAS FF:SYS"

quit () {
	if [[ $2 && $debug ]]; then
		echo "$1" "$2"
	else
		echo -e "Error: $1"
	fi
	exit_usage
}

exit_usage () {
	echo "Usage:"
	echo
	echo "show types: afptype filename"
	echo "set Mac OS: afptype [-t 'type'] [-c 'creator'] [-q] filename"
	echo "set ProDOS: afptype [-p type] [-a auxtype] [-q] filename"
	echo "Mac OS type or creator must be four characters; use \x plus"
	echo "  two hex digits for untypeables (note: use '\xZZ' for 00)."
	echo "ProDOS type should be two hex digits, and auxtype should be four;"
	echo "  type can alternatively be BAS, BIN, INT, P16, S16, SHK, SYS, TXT."
	echo "-q skips recheck of file (show types) after setting"
	echo
	exit 1
}

lookupPdosType () {
# looks up ProDOS hex type from code in list 'ptypes'
# arg: three-character code
# out: two-digit hex value
#exit: 0=type found, 1=error, 2=type not found
	ptypes="04:TXT 06:BIN B3:S16 E0:SHK F9:P16 FA:INT FC:BAS FF:SYS"
	[[ $1 ]] || quit "lookupPdosType:" "no argument supplied ($1)"
	[[ ${#1} -eq 3 ]] || return 1
	arg=$(echo -n "$1" | tr [a-z] [A-Z])
	for ptype in $ptypes; do
		if [[ ${ptype:3:3} == $arg ]]; then
			echo -n "${ptype:0:2}"
			return 0
		fi
	done
	echo "$1"
	return 1
}

verifyTC () {
	[[ $1 ]] || return 1
	tcX="$(echo -e -n "$1"X)"
	[[ ${#tcX} -eq 5 ]] || return 1
	echo "$tcX"
}

while [[ $1 && ( "${1:0:1}" == '-' ) ]]; do
	if [[ $1 == "-p" ]]; then
		[[ $p ]] && exit_usage
		shift
		p="$1"
		shift
		continue
	elif [[ $1 == "-a" ]]; then
		[[ $a ]] && exit_usage
		shift
		a="$1"
		shift
		continue
	elif [[ $1 == "-t" ]]; then
		[[ $t ]] && exit_usage
		shift
		t="$1"
		shift
		continue
	elif [[ $1 == "-c" ]]; then
		[[ $c ]] && exit_usage
		shift
		c="$1"
		shift
		continue
	elif [[ $1 == "-q" ]]; then
		[[ $q ]] && exit_usage
		q=1
		shift
		continue
	else
		exit_usage
		break
	fi
done

if [[ ( ( $p || $a ) && ( $t || $c ) ) || ( -z $1 ) ]]; then
	exit_usage
fi

#filename="$1"
#shift

for filename in $@; do

	[[ ${#@} -gt 1 ]] && linestart="($filename) "

	if [[ ! -f $filename ]]; then
		echo "${linestart}Not found."
		continue
	fi

	adname="$(dirname "$filename")/.AppleDouble/$(basename "$filename")"
	[[ -f $adname ]] && filename=$adname
	ADversion=$(readcharDec "$filename" 5)
	if [[ ( ( $ADversion -ne 1 ) && ( $ADversion -ne 2 ) ) \
		|| ( "$(readchars "$filename" 1 3)" != "$(echo -e -n "\x05\x16\x07")" ) \
		|| ( $(readcharDec "$filename" 0) -ne 0 ) \
		|| ( $(readcharDec "$filename" 4) -ne 0 ) \
		|| ( $(readcharDec "$filename" 6) -ne 0 ) \
		|| ( $(readcharDec "$filename" 7) -ne 0 ) ]]; then
		echo "${linestart}Not an AppleDouble file."
		continue
	fi

	entrycount=`readcharDec "$filename" 25`

	entry=1
	offset=29

	while [[ $(readcharDec "$filename" $offset) -ne 9 ]]; do
		(( entry = entry + 1 ))
		if (( entry > entrycount )); then
			echo "${linestart}Finder Info entry not found in AppleDouble file."
			break
		fi
		(( offset = (entry * 12 + 29) - 12 ))
	done
	(( entry > entrycount )) && continue

	(( offset = offset + 3 ))
	(( tposHi = $(readcharDec "$filename" $offset) * 256 ))
	(( offset = offset + 1 ))
	(( tpos = $(readcharDec "$filename" $offset) + tposHi ))
	(( cpos = tpos + 4 ))
	(( ppos = tpos + 1 ))
	(( apos = tpos + 2 ))

	if [[ $p || $a || $t || $c ]]; then # set
		if [[ $p || $a ]]; then
			if [[ $p ]]; then
				[[ ${#p} -eq 3 ]] && { p=$(lookupPdosType $p) || quit "Invalid ProDOS type name ($p)."; }
				isHexByte "$p" || quit "Invalid ProDOS file type ($p)."
				writecharHex "$filename" $ppos "$p"
			fi
			if [[ $a ]]; then
				isHexByte "${a:0:2}" && isHexByte "${a:2:2}" || quit "Invalid ProDOS aux type ($a)."
				writecharHex "$filename" $apos "${a:0:2}"
				(( apos=apos+1 ))
				writecharHex "$filename" $apos "${a:2:2}"
			fi
			writechars "$filename" $tpos "p"
			writechars "$filename" $cpos "pdos"
		elif [[ $t || $c ]]; then
			if [[ $t ]]; then
				type=$(verifyTC "$t") || quit "$(echo -n "Invalid Mac file type ($t)."; [[ $t == *x* ]] && echo -n " Try quotes."; echo)"
				writechars "$filename" $tpos "${type:0:4}"
			fi
			if [[ $c ]]; then
				creator=$(verifyTC "$c") || quit "$(echo -n "Invalid Mac file creator ($c)."; [[ $c == *x* ]] && echo -n " Try quotes."; echo)"
				writechars "$filename" $cpos "${creator:0:4}"
			fi
		fi
		[[ $q ]] || { echo -n "${linestart}File changed: "; "$0" "$filename"; }

	else # show
		[[ $q ]] && quit "Can only use -q when changing type."

		type="$(readchars "$filename" $tpos 4)"
		creator="$(readchars "$filename" $cpos 4)"

		echo -n "$linestart"

		if [[ $creator != "pdos" || ( ( $type != "TEXT" ) \
			&& ( $type != "PSYS" ) && ( ${type:0:1} != "p" ) ) ]]; then
			if [[ $creator || $type ]]; then
				echo "Mac file.   Type:$type   Creator:$creator"
			else
				echo "This file has no Mac or ProDOS file type information."
			fi
			continue
		fi

		if [[ $type == "TEXT" ]]; then
			pdosType="\$04 [TXT]"
			pdosAuxType='$0000'
		elif [[ $type == "PSYS" ]]; then
			pdosType="\$FF [SYS]"
			pdosAuxType='$0000'
		else
			(( tpos=tpos+1 ))
			pdosType=$(readcharHex "$filename" $tpos)
			for ptype in $ptypes; do
				if [[ "${ptype:0:2}" == "$pdosType" ]]; then
					pdosType="$pdosType [${ptype:3:3}]"
					break
				fi
			done

			(( tpos=tpos+1 ))
			auxTypeHi=$(readcharHex "$filename" $tpos)
			(( tpos=tpos+1 ))
			auxTypeLo=$(readcharHex "$filename" $tpos)
			pdosAuxType=$auxTypeHi$auxTypeLo
		fi

		echo "ProDOS file.  Type:\$$pdosType   AuxType:\$$pdosAuxType"
	fi

done

# 7-19-11
# quick ProDOS testing/fixing on Linux, needs more
# Mac Type testing not done yet, nor testing on a Mac

# test on lunix
# test inside and outside of AD directory, and from other dirs (both cases)
# finish conversion writebyte/readchar library
# consider return 2 for missing parameters for subroutines
