#!/bin/bash # 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 } 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; 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; 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