From 36d15356c7281bf49cdd51b024bdf55cb2a7c186 Mon Sep 17 00:00:00 2001 From: "T. Joseph Carter" Date: Sun, 4 Oct 2015 02:20:54 -0700 Subject: [PATCH] Added the other A2SERVER scripts These also come from @IvanExpert's ivanx server, and I believe he's the author of all of them. He can correct me if I'm wrong on that. --- scripts/afpsync.txt | 114 ++++++ scripts/afptype.txt | 498 ++++++++++++++++++++++++ scripts/cppo.txt | 882 ++++++++++++++++++++++++++++++++++++++++++ scripts/mkatinit.txt | 286 ++++++++++++++ scripts/mkvolinfo.txt | 82 ++++ 5 files changed, 1862 insertions(+) create mode 100644 scripts/afpsync.txt create mode 100644 scripts/afptype.txt create mode 100644 scripts/cppo.txt create mode 100644 scripts/mkatinit.txt create mode 100644 scripts/mkvolinfo.txt diff --git a/scripts/afpsync.txt b/scripts/afpsync.txt new file mode 100644 index 0000000..af0c70f --- /dev/null +++ b/scripts/afpsync.txt @@ -0,0 +1,114 @@ +#!/bin/bash + +# afpsync: updates .AppleDouble components of files on shared volumes + +# this must be used if any files are copied to the shared volume via +# non-AFP methods (directly, via Samba, etc). + +# usage: +# afpsync [-v] [shared volume path] +# +# -v will silently create a .volinfo file if none exists. +# If a path is specified, only that volume is synced; otherwise, all +# all paths in /media which appear in +# /usr/local/etc/netatalk/AppleVolumes.default are synced. + +processPath () { + if [[ ! -d $sharepath ]]; then + echo "$sharepath does not exist." + else + volinfo="$sharepath/.AppleDesktop/.volinfo" + if [[ ! -f $volinfo ]]; then + if [[ ! $force ]]; then + echo "Cannot update AppleDouble files for volume $sharepath," + echo "because its .volinfo file does not exist. To create it, log" + echo "into the volume from an Apple II or Mac client computer," + echo "or use \"afpsync -v\"." + else + if (( $1 )); then + mkvolinfo -f -c $sharepath + else + mkvolinfo -f $sharepath + fi + $0 $sharepath + fi + else + IFS='' + result=$(sudo dbd -r $sharepath | grep encoding) + f=$(echo $result | wc -l) + [[ $(echo $result | wc -w) == 0 ]] && f=0 + [[ f -eq 1 && $(echo $result | grep APPLEDESKTOP) && $(grep MTOULOWER $sharepath/.AppleDesktop/.volinfo) ]] && (( f-- )) + if (( f == 0 )); then + echo "AppleDouble files have been updated for volume $sharepath." + else + [[ ! $renameLower ]] && echo "Could not update all files on volume $sharepath." + [[ ! $renameLower ]] && echo "Ensure filenames are all caps on volume $sharepath." + if [[ $showerrors ]]; then + echo $result \ + | while read LINE; do + [[ ! $(echo $LINE | grep APPLEDESKTOP) ]] && echo $LINE + done + elif [[ $renameLower ]]; then + echo $result \ + | while read LINE; do + if [[ ! $(echo $LINE | grep APPLEDESKTOP) ]]; then + filepath=$(echo $LINE | sed "s/^Bad\ encoding\ for\ '//" | sed s/\'//) + filename=${filepath##*/} + filedir=${filepath%/*} + mv $filepath $filedir/${filename^^} + echo "Renamed $filedir/${filename^^}." + fi + done + $0 $sharepath + else + echo "Use afpsync -e to see error details." + echo "Use afpsync -r to rename lowercase names to uppercase." + fi + fi + unset IFS + fi + fi +} + +while [[ $1 == "-r" || $1 == "-e" || $1 = "-v" ]]; do + + if [[ $1 == "-v" ]]; then + force=1 + shift + fi + + if [[ $1 == "-e" ]]; then + showerrors=1 + shift + fi + + if [[ $1 == "-r" ]]; then + renameLower=1 + shift + fi + +done + +if [[ ${1:0:1} == "-" ]]; then + echo "Usage: afpsync [-e|-r] [-v] [shared volume path]" + echo + echo "-e: show error details" + echo "-r: rename lowercase filenames to uppercase" + echo "-v: create .volinfo file if none exists" + echo "If no directory is specified, all found in" + echo " /usr/local/etc/netatalk/AppleVolumes.default are processed." + echo +else + sudo true + if [[ $1 ]]; then + sharepath=$(readlink -m $1) + processPath + else + grep ^/media /usr/local/etc/netatalk/AppleVolumes.default | \ + while read line; do + [[ $(echo $line | grep toupper) ]]; nocasefold=$? + sharepath=$(echo $line | cut -d" " -f1) + processPath nocasefold + done + fi +fi \ No newline at end of file diff --git a/scripts/afptype.txt b/scripts/afptype.txt new file mode 100644 index 0000000..ebbdbaa --- /dev/null +++ b/scripts/afptype.txt @@ -0,0 +1,498 @@ +#!/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 \ No newline at end of file diff --git a/scripts/cppo.txt b/scripts/cppo.txt new file mode 100644 index 0000000..fdbf660 --- /dev/null +++ b/scripts/cppo.txt @@ -0,0 +1,882 @@ +#!/usr/bin/env python +"""cppo: Copy or catalog one or all files from a ProDOS raw disk image. + +copy all files: + cppo [-ad|-e] imagefile target_directory +copy one file: + cppo [-ad|-e] imagefile /FULL/PRODOS/FILE/PATH target_path +catalog image: + cppo -cat imagefile + +-ad : Create AppleDouble header files and preserve resource forks. +-e : Append ProDOS type and auxtype to filenames, and copy resource + forks, for adding to ShrinkIt archives with Nulib2 + using its -e option. + +Wildcard matching/globbing (*) is not supported. +No verification or validation of the disk image is performed. + +(Compatible with Python 2.6 and later, including 3.x.) +""" + +# cppo by Ivan X, ivan@ivanx.com, ivanx.com/appleii + +# If anyone's looking at this, and feels it's not sufficiently Pythonic, +# I know that. It's pretty much a line-for-line conversion of the original +# Bash script. I did start a beautiful from-the-ground-up object-oriented +# version, then realized it would be faster to translate it ugly and quick. + +# imports for python 3 code compatibility +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import absolute_import +from __future__ import division + +import sys +import os +import time +import datetime + +# Intentially fails on pre-2.6 so user can see what's wrong +b'ERROR: cppo requires Python 2.6 or later, including 3.x.' + +class Globals(object): + pass + +g = Globals() + +g.imageData = b'' +g.outFileData = bytearray(b'') +g.adFileData = bytearray(b'') +g.exFileData = bytearray(b'') + +g.activeDirBlock = None +g.activeFileName = None +g.activeFileSize = None +g.activeFileBytesCopied = 0 +g.resourceFork = 0 + +g.PDOSPATH = [] +g.PDOSPATH_INDEX = 0 +g.PDOSPATH_SEGMENT = None +g.DIRPATH = "" + +g.targetName = None +g.targetDir = "" +g.ADdir = None +g.imageFile = None + +g.AD = 0 +g.EX = 0 +g.DIR = 0 +g.silent = 0 + +# functions + +def pdosDateToUnixDate(arg1): + # input: ProDOS date/time bit sequence string in format: + # "yyyyyyymmmmddddd000hhhhh00mmmmmm" (ustr) + # output: seconds since Unix epoch (1-Jan-1970), + # or current date/time if no ProDOS date + year = (binToDec(slyce(arg1,0,7)) + 1900) + if (year < 1940): year += 100 + month = binToDec(slyce(arg1,7,4)) + day = binToDec(slyce(arg1,11,5)) + hour = binToDec(slyce(arg1,19,5)) + minute = binToDec(slyce(arg1,26,6)) + # print(year, month, day, hour, minute) + td = (datetime.datetime(year, month, day, hour, minute) - + datetime.datetime(1970,1,1)) + unixDate_naive = (td.days*24*60*60 + td.seconds) + td2 = (datetime.datetime.fromtimestamp(unixDate_naive) - + datetime.datetime.utcfromtimestamp(unixDate_naive)) + utcoffset = (td2.days*24*60*60 + td2.seconds) + # print(unixDate_naive - utcoffset) + return (unixDate_naive - utcoffset) # local time zone with DST + +def unixDateToADDate(arg1): + # input: seconds since Unix epoch (1-Jan-1970 00:00:00 GMT) + # output: seconds since Netatalk epoch (1-Jan-2000 00:00:00 GMT), + # in hex-ustr (big endian) + adDate = (arg1 - 946684800) + if (adDate < 0 ): + adDate += 4294967296 # to get negative hex number + adDateHex = to_hex(adDate).zfill(8).upper() + # print(arg1, adDate, adDateHex) + return adDateHex + +# cppo support routines: +# arg1: directory block +# arg2: file index (if applicable) +# arg3: directory chunk # (if applicable) + +#most of these not tested yet in Python +# returns byte position in disk image file +def getStartPos(arg1, arg2): + return ( (arg1 * 512) + + (39 * ((arg2 + (arg2 > 11)) % 13)) + + (4 if (arg2 > 11) else 43) ) + +def getStorageType(arg1, arg2): + start = getStartPos(arg1, arg2) + firstByte = readcharDec(g.imageData, start) + return (firstByte//16) + +def getFileName(arg1, arg2): + start = getStartPos(arg1, arg2) + firstByte = readcharDec(g.imageData, start) + entryType = (firstByte//16) + nameLength = (firstByte - entryType*16) + return readchars(g.imageData, start+1, nameLength) + +def getFileType(arg1, arg2): + start = getStartPos(arg1, arg2) + return readcharHex(g.imageData, start+16) + +def getKeyPointer(arg1, arg2): + start = getStartPos(arg1, arg2) + return (readcharDec(g.imageData, start+17) + + readcharDec(g.imageData, start+18)*256) + +def getFileLength(arg1, arg2): + start = getStartPos(arg1, arg2) + return (readcharDec(g.imageData, start+21) + + readcharDec(g.imageData, start+22)*256 + + readcharDec(g.imageData, start+23)*65536) + +def getAuxType(arg1, arg2): + start = getStartPos(arg1, arg2) + return (readcharHex(g.imageData, start+32) + + readcharHex(g.imageData, start+31)) + +def getCreationDate(arg1, arg2): + #outputs prodos creation date/time as Unix time + # (seconds since Jan 1 1970 GMT) + #or None if there is none + start = getStartPos(arg1, arg2) + pdosDate = (hexToBin(readcharHex(g.imageData, start+25)) + + hexToBin(readcharHex(g.imageData, start+24)) + + hexToBin(readcharHex(g.imageData, start+27)) + + hexToBin(readcharHex(g.imageData, start+26))) + try: + rVal = pdosDateToUnixDate(pdosDate) + except Exception: + rVal = None + return rVal + +def getModifiedDate(arg1, arg2): + #outputs prodos modified date/time as Unix time + # (seconds since Jan 1 1970 GMT) + #or None if there is none + start = getStartPos(arg1, arg2) + pdosDate = (hexToBin(readcharHex(g.imageData, start+34)) + + hexToBin(readcharHex(g.imageData, start+33)) + + hexToBin(readcharHex(g.imageData, start+36)) + + hexToBin(readcharHex(g.imageData, start+35))) + try: + rVal = pdosDateToUnixDate(pdosDate) + except Exception: + rVal = None + return rVal + +def getVolumeName(): + return getWorkingDirName(2) + +def getWorkingDirName(arg1): + start = ( arg1 * 512 ) + firstByte = readcharDec(g.imageData, start+4) + entryType = (firstByte//16) + nameLength = (firstByte - entryType*16) + return readchars(g.imageData, start+5, nameLength) + +def getDirEntryCount(arg1): + start = ( arg1 * 512 ) + return (readcharDec(g.imageData, start+37) + + readcharDec(g.imageData, start+38)*256) + +def getDirNextChunkPointer(arg1): + start = ( arg1 * 512 ) + return (readcharDec(g.imageData, start+2) + + readcharDec(g.imageData, start+3)*256) + +# -- script begins in earnest here + +def copyFile(arg1, arg2): + g.outFileData = bytearray(b'') + g.exFileData = bytearray(b'') + g.activeFileBytesCopied = 0 + storageType = getStorageType(arg1, arg2) + keyPointer = getKeyPointer(arg1, arg2) + fileLen = getFileLength(arg1, arg2) + if (storageType == 1): #seedling + copyBlock(keyPointer, fileLen) + elif (storageType == 2): #sapling + processIndexBlock(keyPointer) + elif (storageType == 3): #tree + processMasterIndexBlock(keyPointer) + elif (storageType == 5): #extended (forked) + processForkedFile(keyPointer) + +def copyBlock(arg1, arg2): + #arg1: block to copy + #arg2: bytes to write (should be 512, + # unless final block with less than 512 bytes) + #print(arg1 + " " + arg2 + " " + g.activeFileBytesCopied) + if (arg1 == 0): + outBytes = (b'\x00' * arg2) + else: + outBytes = slyce(g.imageData, arg1*512, arg2) + if (g.resourceFork > 0): + if g.AD: + g.adFileData[g.activeFileBytesCopied+741: + (g.activeFileBytesCopied+741 + arg2)] = outBytes + if g.EX: + g.exFileData[g.activeFileBytesCopied: + (g.activeFileBytesCopied + arg2)] = outBytes + else: + g.outFileData[g.activeFileBytesCopied: + (g.activeFileBytesCopied + arg2)] = outBytes + g.activeFileBytesCopied += arg2 + +def processDir(arg1, arg2=None, arg3=None, arg4=None, arg5=None): + # arg1: dirBlock + # arg2/3/4/5: for non-key chunks: entryCount, entry#, + # workingDirName, processedEntryCount + + entryCount = None + e = None + pe = None + workingDirName = None + + if arg2: + entryCount = arg2 + e = arg3 + workingDirName = arg4 + pe = arg5 + else: + e = 0 + pe = 0 + entryCount = getDirEntryCount(arg1) + workingDirName = getWorkingDirName(arg1).decode("L1") + g.DIRPATH = (g.DIRPATH + "/" + workingDirName) + if g.PDOSPATH_INDEX: + if (g.PDOSPATH_INDEX == 1): + if (("/" + g.PDOSPATH_SEGMENT) != g.DIRPATH): + print("ProDOS volume name does not match disk image.") + sys.exit(2) + else: + g.PDOSPATH_INDEX += 1 + g.PDOSPATH_SEGMENT = g.PDOSPATH[g.PDOSPATH_INDEX] + else: + print(g.DIRPATH) + while (pe < entryCount): + if (getStorageType(arg1, e) > 0): + processEntry(arg1, e) + pe += 1 + e += 1 + if not ((e + ( e>11 ) ) % 13): + processDir(getDirNextChunkPointer(arg1), + entryCount, + e, + workingDirName, + pe) + break + +def processEntry(arg1, arg2): + ''' + print(getFileName(arg1, arg2), getStorageType(arg1, arg2), + getFileType(arg1, arg2), getKeyPointer(arg1, arg2), + getFileLength(arg1, arg2), getAuxType(arg1, arg2), + getCreationDate(arg1, arg2), getModifiedDate(arg1, arg2)) + ''' + g.activeFileName = getFileName(arg1 ,arg2).decode("L1") + g.activeFileSize = getFileLength(arg1, arg2) + + if ((not g.PDOSPATH_INDEX) or (g.activeFileName == g.PDOSPATH_SEGMENT)): + + if (getStorageType(arg1, arg2) == 13): # if ProDOS directory + if not g.PDOSPATH_INDEX: + g.targetDir = (g.targetDir + "/" + g.activeFileName) + g.ADdir = (g.targetDir + "/.AppleDouble") + if not (g.DIR or os.path.isdir(g.targetDir)): + makedirs(g.targetDir) + if not (g.DIR or (not g.AD) or os.path.isdir(g.ADdir)): + makedirs(g.ADdir) + if g.PDOSPATH_SEGMENT: + g.PDOSPATH_INDEX += 1 + g.PDOSPATH_SEGMENT = g.PDOSPATH[g.PDOSPATH_INDEX] + processDir(getKeyPointer(arg1, arg2)) + g.DIRPATH = g.DIRPATH.rsplit("/", 1)[0] + if not g.PDOSPATH_INDEX: + g.targetDir = g.targetDir.rsplit("/", 1)[0] + g.ADdir = (g.targetDir + "/.AppleDouble") + else: # if ProDOS file + if not g.PDOSPATH_INDEX: + print(" " + g.activeFileName) + if g.DIR: + return + if not g.targetName: + g.targetName = g.activeFileName + if g.EX: + eTargetName = (g.targetName + "#" + + getFileType(arg1, arg2).lower() + + getAuxType(arg1, arg2).lower()) + touch(g.targetDir + "/" + g.targetName) + if g.AD: makeADfile() + copyFile(arg1, arg2) + saveFile((g.targetDir + "/" + g.targetName), g.outFileData) + creationDate = getCreationDate(arg1, arg2) + modifiedDate = getModifiedDate(arg1, arg2) + if (creationDate is None and modifiedDate is not None): + creationDate = modifiedDate + elif (creationDate is not None and modifiedDate is None): + modifiedDate = creationDate + elif (creationDate is None and modifiedDate is None): + creationDate = (datetime.datetime.today() - + datetime.datetime(1970,1,1)).days*24*60*60 + modifiedDate = creationDate + if g.AD: # AppleDouble + # set dates + ADfilePath = (g.ADdir + "/" + g.targetName) + writecharsHex(g.adFileData, + 637, + (unixDateToADDate(creationDate) + + unixDateToADDate(modifiedDate))) + writecharHex(g.adFileData, 645, "80") + writecharHex(g.adFileData, 649, "80") + #set type/creator + writechars(g.adFileData, 653, b'p') + writecharsHex(g.adFileData, + 654, + (getFileType(arg1, arg2) + + getAuxType(arg1, arg2))) + writechars(g.adFileData, 657, b'pdos') + saveFile(ADfilePath, g.adFileData) + touch((g.targetDir + "/" + g.targetName), modifiedDate) + if g.EX: # extended name + os.rename((g.targetDir + "/" + g.targetName), + (g.targetDir + "/" + eTargetName)) + if (len(g.exFileData) > 0): + saveFile((g.targetDir + "/" + eTargetName + "r"), + g.exFileData) + touch((g.targetDir + "/" + eTargetName + "r"), + modifiedDate) + if g.PDOSPATH_SEGMENT: + syncExit() + g.targetName = None + + #else: + #print(g.activeFileName + " doesn't match " + g.PDOSPATH_SEGMENT) + +def processForkedFile(arg1): + # finder info except type/creator + fInfoA_entryType = readcharDec(g.imageData, 9) + fInfoB_entryType = readcharDec(g.imageData, 27) + if (fInfoA_entryType == 1): + writechars(g.imageData, 661, readchars(g.imageData, 18, 8)) + elif (fInfoA_entryType == 2): + writechars(g.imageData, 669, readchars(g.imageData, 10, 16)) + if (fInfoB_entryType == 1): + writechars(g.imageData, 661, readchars(g.imageData, 36, 8)) + elif (fInfoB_entryType == 2): + writechars(g.imageData, 669, readchars(g.imageData, 28, 16)) + + for f in [0, 256]: + g.resourceFork = f + g.activeFileBytesCopied = 0 + forkStart = (arg1 * 512) # start of Forked File key block + # print("--" + forkStart) + forkStorageType = readcharDec(g.imageData, forkStart+f+0) + forkKeyPointer = (readcharDec(g.imageData, forkStart+f+1) + + readcharDec(g.imageData, forkStart+f+2)*256) + forkFileLen = (readcharDec(g.imageData, forkStart+f+5) + + readcharDec(g.imageData, forkStart+f+6)*256 + + readcharDec(g.imageData, forkStart+f+7)*256*256) + g.activeFileSize = forkFileLen + if (g.resourceFork > 0): + rsrcForkLenHex = (readcharHex(g.imageData, forkStart+f+7) + + readcharHex(g.imageData, forkStart+f+6) + + readcharHex(g.imageData, forkStart+f+5)) + # print(">>>" + rsrcForkLenHex) + print(" [resource fork]") + if g.AD: + writecharsHex(g.adFileData, 35, rsrcForkLenHex) + else: + print(" [data fork]") + if (forkStorageType == 1): #seedling + copyBlock(forkKeyPointer, forkFileLen) + elif (forkStorageType == 2): #sapling + processIndexBlock(forkKeyPointer) + elif (forkStorageType == 3): #tree + processMasterIndexBlock(forkKeyPointer) + # print() + g.resourceFork = 0 + +def processMasterIndexBlock(arg1): + processIndexBlock(arg1, True) + +def processIndexBlock(arg1, arg2=False): + #arg1: indexBlock + #arg2: if True, it's a Master Index Block + pos = 0 + bytesRemaining = g.activeFileSize + while (g.activeFileBytesCopied < g.activeFileSize): + targetBlock = (readcharDec(g.imageData, arg1*512+pos) + + readcharDec(g.imageData, arg1*512+(pos+256))*256) + if arg2: + processIndexBlock(targetBlock) + else: + bytesRemaining = (g.activeFileSize - g.activeFileBytesCopied) + bs = (bytesRemaining if (bytesRemaining < 512) else 512) + copyBlock(targetBlock, bs) + pos += 1 + if (pos > 255): + break # go to next entry in Master Index Block (tree) + +def makeADfile(): + if not g.AD: return + touch(g.ADdir + "/" + g.targetName) + g.adFileData = bytearray(b'\x00' * 741) + # ADv2 header + writecharsHex(g.adFileData, hexToDec("00"), "0005160700020000") + # number of entries + writecharsHex(g.adFileData, hexToDec("18"), "000D") + # Resource Fork + writecharsHex(g.adFileData, hexToDec("1A"), "00000002000002E500000000") + # Real Name + writecharsHex(g.adFileData, hexToDec("26"), "00000003000000B600000000") + # Comment + writecharsHex(g.adFileData, hexToDec("32"), "00000004000001B500000000") + # Dates Info + writecharsHex(g.adFileData, hexToDec("3E"), "000000080000027D00000010") + # Finder Info + writecharsHex(g.adFileData, hexToDec("4A"), "000000090000028D00000020") + # ProDOS file info + writecharsHex(g.adFileData, hexToDec("56"), "0000000B000002C100000008") + # AFP short name + writecharsHex(g.adFileData, hexToDec("62"), "0000000D000002B500000000") + # AFP File Info + writecharsHex(g.adFileData, hexToDec("6E"), "0000000E000002B100000004") + # AFP Directory ID + writecharsHex(g.adFileData, hexToDec("7A"), "0000000F000002AD00000004") + # dbd (second time) will create DEV, INO, SYN, SV~ + +def syncExit(): + if (not g.silent and g.AD and os.path.isdir("/usr/local/etc/netatalk")): + print("File(s) have been copied to the target directory. " + + "If the directory") + print("is shared by Netatalk, please type 'afpsync' now.") + # saveFile(g.imageFile, g.imageData) + sys.exit(0) + +def usage(): + print(sys.modules[__name__].__doc__) + sys.exit(1) + +# --- ID bashbyter functions (adapted) + +def decToHex(arg1): +# converts single-byte decimal value to hexadecimal equivalent +# arg: decimal value from 0-255 +# out: two-digit hex string from 00-FF +#exit: 21=invalid arg + if (arg1<0 or arg1>255): sys.exit(21) + return to_hex(arg1).upper() + +def hexToDec(arg1): +# converts single-byte hexadecimal value to decimal equivalent +# arg: two-digit hex value from 00-FF +# out: decimal value +#exit: 21=invalid arg + if (len(arg1) != 2): return 21 + return to_dec(arg1) + +def hexToBin(arg1): +# converts single-byte hexadecimal value to binary string +# arg: two-digit hex value from 00-FF +# out: binary string value +#exit: 21=invalid arg + if (len(arg1) != 2): return 21 + return to_bin(arg1).zfill(8) + +def binToDec(arg1): +# converts single-byte binary string (8 bits) value to decimal +# warning: no error checking +# arg: binary string up to 8 bits +# out: decimal value + return to_dec([arg1]) + +def binToHex(arg1): +# converts single-byte binary string (8 bits) value to hex +# warning: no error checking +# arg: binary string up to 8 bits +# out: hex value + return to_hex(arg1).upper() + +def charToDec(arg1): +# converts single char (of type bytes) to corresponding decimal value +# arg: one char (of type bytes) +# out: decimal value from 0-255 +#exit: 21=invalid arg + if (len(arg1) != 1): return 21 + return to_dec(arg1) + +def charToHex(arg1): +# converts single char (of type bytes) to corresponding hex value +# arg: one char (of type bytes) +# out: hexadecimal value from 00-FF +#exit: 21=invalid arg + if (len(arg1) != 1): return 21 + return to_hex(arg1).upper() + +def decToChar(arg1): +# converts single-byte decimal value to equivalent char (of type bytes) +# arg: decimal number from 0-255 +# out: one character +#exit: 21=invalid arg + if (arg1<0 or arg1>255): sys.exit(21) + return to_bytes(arg1) + +def hexToChar(arg1): +# converts single-byte hex value to corresponding char (of type bytes) +# arg: two-digit hexadecimal number from 00-FF +# out: one character +#exit: 21=invalid arg + if (len(arg1) != 2): return 21 + return to_bytes(arg1) + +def readchars(arg1, arg2=0, arg3=0): +# read one or more characters from a bytes variable +# arg1: bytes or bytearray variable +# arg2: (optional) offset (# of bytes to skip before reading) +# arg3: (optional) # of chars to read (default is to end of bytes var) +# out: sequence of characters (bytes or bytearray) +# exit: 21=invalid arg1, 22=invalid arg2, 23=invalid arg3 + if not (isinstance(arg1, bytes) or isinstance(arg1, bytearray)): + sys.exit(21) + if (arg2<0): sys.exit(22) + if (arg3<0): sys.exit(23) + if (arg3 == 0): + arg3 = len(arg1) + return slyce(arg1, arg2, arg3) + +def readcharDec(arg1, arg2=0): +# read one character from bytes var & convert to equivalent dec value +# arg1: bytes var +# arg2: (optional) offset (# of bytes to skip before reading) +# out: decimal value from 0-255 +# exit: 21=invalid arg1, 22=invalid arg2 + if not (isinstance(arg1, bytes) or isinstance(arg1, bytearray)): + sys.exit(21) + if (arg2<0): sys.exit(22) + return to_dec(slyce(arg1, arg2, 1)) + +def readcharHex(arg1, arg2=0): +# read one character from bytes var & convert to corresponding hex value +# arg1: bytes var +# arg2: (optional) offset (# of bytes to skip before reading) +# out: two-digit hex value from 00-FF +# exit: 21=invalid arg1, 22=invalid arg2 + if not (isinstance(arg1, bytes) or isinstance(arg1, bytearray)): + sys.exit(21) + if (arg2<0): sys.exit(22) + return to_hex(slyce(arg1, arg2, 1)) + +def writechars(arg1, arg2, arg3): +# write one or more characters (bytes) to bytearray +# arg1: bytearray variable +# arg2: offset (# of bytes to skip before writing) +# arg3: sequence of bytes (or bytearray) +# out: nothing +# exit: 21=invalid arg1, 22=invalid arg2, 23=invalid arg3 + if not isinstance(arg1, bytearray): sys.exit(21) + if (arg2<0): sys.exit(22) + if not (isinstance(arg3, bytes) or isinstance(arg3, bytearray)): + sys.exit(23) + arg1[arg2:arg2+len(arg3)] = arg3 + +def writecharDec(arg1, arg2, arg3): +# write corresponding char of single-byte decimal value into bytearray +# arg1: bytearray +# arg2: offset (# of bytes to skip before writing) +# arg3: decimal number from 0-255 +# exit: 21=invalid arg1, 22=invalid arg2, 23=invalid arg3 +# out: nothing + if not isinstance(arg1, bytearray): sys.exit(21) + if (arg2<0): sys.exit(22) + if not isnumber(arg3): sys.exit(23) + arg1[arg2:arg2+1] = to_bytes(arg3) + +def writecharHex(arg1, arg2, arg3): +# write corresponding character of single-byte hex value into bytearray +# arg1: bytearray +# arg2: offset (# of bytes to skip before writing) +# arg3: two-digit hexadecimal number from 00-FF +# out: nothing +# exit: 21=invalid arg1, 22=invalid arg2, 23=invalid arg3 + if not isinstance(arg1, bytearray): sys.exit(21) + if (arg2<0): sys.exit(22) + if not isinstance(arg3, type("".encode().decode())): sys.exit(23) + arg1[arg2:arg2+1] = to_bytes(arg3) + +def writecharsHex(arg1, arg2, arg3): +# write corresponding characters of hex values into bytearray +# arg1: bytearray +# arg2: offset (# of bytes to skip before writing) +# arg3: string of two-digit hexadecimal numbers from 00-FF +# out: nothing +# exit: 21=invalid arg1, 22=invalid arg2, 23=invalid arg3 + if not isinstance(arg1, bytearray): sys.exit(21) + if (arg2<0): sys.exit(22) + if not isinstance(arg3, type("".encode().decode())): sys.exit(23) + arg1[arg2:arg2+len(to_bytes(arg3))] = to_bytes(arg3) + +#---- IvanX general purpose functions ----# + +def slyce(val, start_pos=0, length=1, reverse=False): + """returns slice of object (but not a slice object) + allows specifying length, and 3.x "bytes" consistency""" + the_slyce = val[start_pos:start_pos+length] + return (the_slyce[::-1] if reverse else the_slyce) + +def to_hex(val): + """convert bytes, decimal number, or [bin-ustr] to two-digit hex values + unlike hex(), accepts bytes; has no leading 0x or trailing L""" + from binascii import b2a_hex + if isinstance(val, list): # [bin-ustr] + val = int(val[0], 2) + if isinstance(val, bytes): # bytes + return b2a_hex(val).decode("L1") + elif isnumber(val): + # .encode().decode() always returns unicode in both P2 and P3 + return (hex(val)[2:].split("L")[0]).encode("L1").decode("L1") + else: + raise Exception("to_hex() requires bytes, int/long, or [bin-ustr]") + +def hex_slyce(val, start_pos=0, length=1, little_endian=False): + """returns bytes slyce as hex-ustr""" + return to_hex(slyce(val, start_pos, length, little_endian)) + +def dec_slyce(val, start_pos=0, length=1, little_endian=False): + """returns bytes slyce converted to decimal int/long""" + return to_dec(hex_slyce(val, start_pos, length, little_endian)) + +def bin_slyce(val, start_pos=0, length=1, little_endian=False): + """returns bytes slyce converted to bin-ustr""" + return to_bin(hex_slyce(val, start_pos, length, little_endian)) + +def to_dec(val): + """convert bytes, hex-ustr or [bin-ustr] to decimal int/long""" + if isinstance(val, list): # [bin-ustr] + return int(val[0], 2) + elif isinstance(val, bytes): # bytes + return int(to_hex(val), 16) + elif isinstance(val, type("".encode().decode())): # hex-ustr + return int(val, 16) + else: + raise Exception("to_dec() requires bytes, hex-ustr or [bin-ustr]") + +def to_bin(val): + """convert bytes, hex-ustr, or int/long to bin-ustr""" + if isinstance(val, bytes): # bytes + return (bin(to_dec(to_hex(val))))[2:].encode("L1").decode("L1") + elif isinstance(val, type("".encode().decode())): # hex-ustr + return (bin(int(val, 16)))[2:].encode("L1").decode("L1") + elif isnumber(val): # int/long + return (bin(val))[2:].encode("L1").decode("L1") + else: + raise Exception("to_bin() requires bytes, hex-ustr, or int/long") + +def to_bytes(val): + """converts hex-ustr, int/long, or [bin-ustr] to bytes""" + from binascii import a2b_hex + if isinstance(val, list): # [bin-ustr] + val = to_hex(val[0]) + if isnumber(val): # int/long + val = to_hex(val) + if isinstance(val, type("".encode().decode())): # hex-ustr + return a2b_hex(bytes(val.encode("L1"))) # works on both P2 and P3 + else: + raise Exception( + "to_bytes() requires hex-ustr, int/long, or [bin-ustr]") + +def shift(items): + """Shift list items to left, losing the first item. + + in : list + out: list + """ + for i in range(0, (len(items)-1)): + items[i] = items[i+1] + del items[-1] + return items + +def s(string): + """Perform local variable substution, e.g. 'total: {num} items'""" + # http://stackoverflow.com/questions/2960772/ + # putting-a-variable-inside-a-string-python + # http://stackoverflow.com/questions/6618795/ + # get-locals-from-calling-namespace-in-python + import inspect + frame = inspect.currentframe() + try: + rVal = string.format(**frame.f_back.f_locals) + finally: + del frame + return rVal + +def get_object_names(cls, include_subclasses=True): + object_names = [] + for (this_object_name, this_object_id) in list(globals().items()): + if include_subclasses: + if isinstance(this_object_id, cls): + object_names.append(this_object_name) + else: + if type(this_object_id) is cls: + object_names.append(this_object_name) + return object_names + +def touch(filePath, modTime=None): + # http://stackoverflow.com/questions/1158076/implement-touch-using-python + import os + if (os.name == "nt"): + if filePath[-1] == ".": filePath += "-" + filePath = filePath.replace("./", ".-/") + with open(filePath, "ab"): + os.utime(filePath, (None if (modTime is None) else (modTime, modTime))) + +def mkdir(dirPath): + import os + if (os.name == "nt"): + if dirPath[-1] == ".": dirPath += "-" + dirPath = dirPath.replace("./", ".-/") + try: + os.mkdir(dirPath) + except FileExistsError: + pass + +def makedirs(dirPath): + import os + if (os.name == "nt"): + if dirPath[-1] == ".": dirPath += "-" + dirPath = dirPath.replace("./", ".-/") + try: + os.makedirs(dirPath) + except FileExistsError: + pass + +def loadFile(filePath): + import os + if (os.name == "nt"): + if filePath[-1] == ".": filePath += "-" + filePath = filePath.replace("./", ".-/") + with open(filePath, "rb") as imageHandle: + return imageHandle.read() + +def saveFile(filePath, fileData): + import os + if (os.name == "nt"): + if filePath[-1] == ".": filePath += "-" + filePath = filePath.replace("./", ".-/") + with open(filePath, "wb") as imageHandle: + imageHandle.write(fileData) + +def isnumber(number): + try: # make sure it's not a string + len(number) + return False + except TypeError: + pass + try: + int(number) + except ValueError: + return False + return True + +#---- end IvanX general purpose functions ----# + + +# --- start + +args = sys.argv +if (len(args) == 1): + usage() + +if (args[1] == "-s"): + g.silent=1 + args = args[1:] #shift + +if (args[1] == "-ad"): + g.AD = 1 + args = args[1:] #shift + +if (args[1] == "-e"): + if g.AD: usage() + g.EX = 1 + args = args[1:] #shift + +if (args[1] == "-cat"): + g.DIR = 1 + args = args[1:] + +if not ((g.DIR and len(args) >= 2) or (len(args) >= 3)): + usage() +if ((len(args) == 4) and + (slyce(args[2],0,1) != "/") and + (slyce(args[2],0,1) != ":")): + usage() + +g.imageFile = args[1] +if not os.path.isfile(g.imageFile): + print("Source " + g.imageFile + " was not found.") + sys.exit(2) +g.imageData = loadFile(g.imageFile) + +if (len(args) == 4): + g.PDOSPATH = args[2].upper() + targetPath = args[3] + if os.path.isdir(targetPath): + g.targetDir = targetPath + else: + g.targetDir = targetPath.rsplit("/", 1)[0] + g.targetName = targetPath.rsplit("/", 1)[1] + if not os.path.isdir(g.targetDir): + print("Target directory not found.") + sys.exit(2) +else: + if not g.DIR: + if not os.path.isdir(args[2]): + print("Target directory not found.") + sys.exit(2) + +g.activeDirBlock = 0 +g.activeFileName = "" +g.activeFileSize = 0 +g.activeFileBytesCopied = 0 +g.resourceFork = 0 +g.PDOSPATH_INDEX = 0 + +if (len(args) == 4): + g.PDOSPATH = g.PDOSPATH.replace(':', '/').split('/') + if not g.PDOSPATH[0]: + g.PDOSPATH_INDEX += 1 + g.PDOSPATH_SEGMENT = g.PDOSPATH[g.PDOSPATH_INDEX] + g.ADdir = (g.targetDir + "/.AppleDouble") + if not ((not g.AD) or os.path.isdir(g.ADdir)): + mkdir(g.ADdir) + processDir(2) + print("ProDOS file not found within image file.") + sys.exit(2) +else: + if not g.DIR: + # print(args[0], args[1], args[2]) + g.targetDir = (args[2] + "/" + getVolumeName().decode("L1")) + g.ADdir = (g.targetDir + "/.AppleDouble") + if not os.path.isdir(g.targetDir): + makedirs(g.targetDir) + if not ((not g.AD) or os.path.isdir(g.ADdir)): + makedirs(g.ADdir) + processDir(2) + if not g.DIR: + syncExit() + diff --git a/scripts/mkatinit.txt b/scripts/mkatinit.txt new file mode 100644 index 0000000..c1fa802 --- /dev/null +++ b/scripts/mkatinit.txt @@ -0,0 +1,286 @@ +#!/bin/bash + +# mkatinit by Ivan Drucker, http://appleii.ivanx.com +# not licensed; please modify and redistribute as you like +# 1.0b4 1-30-11 + +# 1.1d1 -- uses bashByter. NOT TESTED. + +# This script creates the ATINIT file required for netbooting Apple IIe and +# IIGS clients from a netatalk server. ATINIT is put in the in the +# (sharename)/USERS hierarchy, or the current directory if it can't. +# The user can specify the ProDOS startup system program and default prefix. +# If an argument is supplied, it is used as the username and the prompt is +# skipped. + +# The ATINIT file is not as complete as the ones generated by AppleShare 3.0, +# and features such as default printer are not used. + +# to do +# possibly require confirmation if AFPD_GUEST is not set correctly +# or offer to change it + +# from ID-bashByter library +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")" +} + +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 +} + +#---- mkatinit starts here +IFS='' + +#defaults +sharepath=$(grep options:prodos /usr/local/etc/netatalk/AppleVolumes.default | tail -1 | cut -d" " -f1) +prefix=/$(grep options:prodos /usr/local/etc/netatalk/AppleVolumes.default | tail -1 | cut -d" " -f2) +gsprefix=$prefix +startprog="$prefix/BASIC.SYSTEM" +gsstartprog="$prefix/SYSTEM/FINDER" +filename="ATINIT" +user=$(whoami) + +echo + +usersfolder=1 +while [ -n "$1" ] && [ "${1:0:1}" = '-' ]; do + flag="$1" + if [ $flag = "-gs" ]; then + gs=1 + shift + continue + elif [ $flag = "-d" ]; then + noprompt=1 + shift + continue + elif [ $flag = "-f" ]; then + overwrite=1 + shift + continue + elif [ $flag = "-g" ]; then + alsoguest=1 + shift + continue + elif [ $flag = "-c" ]; then + usersfolder=0 + shift + continue + else + badflag=1 + break + fi +done + +if [ $usersfolder -eq 0 ] && [ -n "$alsoguest" ]; then + badflag=1 +fi + +if [ $badflag ]; then + echo "usage: mkatinit [-gs] [-d] [-f] [-g | -c] [|guest]" + echo + echo " should be the name of a netatalk user, or 'guest'" + echo "-gs sets the default ProDOS netboot system startup program and" + echo " prefix for GS/OS (if omitted, ProDOS 8 defaults are used)" + echo "-d use the default start program and prefix, without prompting" + echo "-f overwrite existing ATINIT file if present, without warning" + echo "-g create an ATINIT file for Guest as well as the specified user" + echo "-c write ATNIT to current directory instead of USERS hierarchy" + echo + exit +fi + +arg="$1" + +[[ -z $arg ]] && arg=$user + +while : ; do + + if [ ${#arg} -gt 32 ]; then + echo "User name is too long. Exiting with no action." + exit + elif [ -n "$arg" ]; then + if [ `echo $arg | tr '[:upper:]' '[:lower:]'` = "guest" ]; then + username='' + alsoguest= + if [[ ! $(grep -F -s $(ls -1 /home) /etc/default/netatalk) ]]; then + echo 'Warning: The AFPD_GUEST setting in /etc/default/netatalk is not assigned to a' + echo 'user with a folder in /home. Guest users may be able to boot over the network' + echo 'into ProDOS 8 for read-only access, but will not be able to boot into GS/OS.' + if [ $gs ]; then + echo ' Using ProDOS 8 defaults.' + gs= + else + echo + fi + echo + fi + else + username="$arg" + if [[ ! $(ls -1 /home | grep -F -s $username) ]]; then + echo "Warning: This username ($username) does not have a folder in /home. This user may" + echo "not be able to boot over the network into ProDOS 8 or GS/OS." + echo + fi + fi + if [ $gs ]; then + startprog="$gsstartprog" + prefix="$gsprefix" + fi + fi + + # make username all caps + username=`echo $username | tr '[:lower:]' '[:upper:]'` + + # prompt for folders + if [ ! $doalsoguest ] && [ ! $noprompt ]; then + while : ; do + echo "Enter the ProDOS path to the startup system program." + echo "default (CR to accept): $startprog" + read + if [ ${#REPLY} -gt 64 ]; then + continue + elif [ -n "$REPLY" ]; then + startprog="`echo $REPLY | tr '[:lower:]' '[:upper:]'`" + echo + fi + break + done + + while : ; do + echo "Enter the initial ProDOS prefix:" + echo "default (CR to accept): $prefix" + read + if [ ${#REPLY} -gt 64 ]; then + continue + elif [ -n "$REPLY" ]; then + prefix="`echo $REPLY | tr '[:lower:]' '[:upper:]'`" + echo + fi + break + done + fi + + # create folders if needed + filepath="$PWD/$filename" + + while [ $usersfolder -eq 1 ]; do + # create folders for ATINIT + while [ ! -d "$sharepath" ]; do + echo "Shared volume '$sharepath' not found. Edit mkatinit to change the default." + echo "Enter the local path to the netatalk shared volume used for netboot:" + read sharepath + done + + if [ ! -d "$sharepath/USERS" ]; then + mkdir "$sharepath/USERS" + if [ $? -ne 0 ]; then + break + fi + fi + if [ ! -d "$sharepath/USERS/$username" ]; then + mkdir "$sharepath/USERS/$username" + if [ $? -ne 0 ]; then + break + fi + fi + if [ ! -d "$sharepath/USERS/$username/SETUP" ]; then + mkdir "$sharepath/USERS/$username/SETUP" + if [ $? -ne 0 ]; then + break + fi + else + if [ ! -w "$sharepath/USERS/$username/SETUP" ]; then + break + fi + fi + + # we have a valid USERS folder target for ATINIT + filepath="$sharepath/USERS/$username/SETUP/$filename" + usersfolder=2 + done + + if [ $usersfolder -eq 1 ]; then + echo "Could not write to shared volume. Creating ATINIT in current directory." + if [ -n "$alsoguest" ]; then + echo "ATINIT for Guest user will not be created." + fi + echo + alsoguest= + fi + + if [ ! $doalsoguest ]; then + echo "netboot start program: $startprog" + echo "netboot start prefix : $prefix" + echo + fi + + #remove ATINIT if present + if [ ! $overwrite ] && [ -f "$filepath" ]; then + echo -n "$filepath already exists. Overwrite? " + while read -s -n 1 > /dev/null 2>&1; do + if [ -z "$REPLY" ]; then + continue + elif [ "$REPLY" = "Y" ] || [ "$REPLY" = "y" ]; then + echo + break + elif [ "$REPLY" = "N" ] || [ "$REPLY" = "n" ]; then + echo + echo "Exiting with no action." + exit + fi + done + fi + + #write the file. start with zeroes + dd if=/dev/zero of="$filepath" bs=1 count=276 2> /dev/null + #put in startprog, prefix, username (first byte of each field is length) + # ( echo -n "${#startprog}" | awk '{printf("%c",$0);}'; echo -n "$startprog"; ) | dd of="$filepath" bs=1 seek=7 conv=notrunc 2> /dev/null + # ( echo -n "${#prefix}" | awk '{printf("%c",$0);}'; echo -n "$prefix"; ) | dd of="$filepath" bs=1 seek=78 conv=notrunc 2> /dev/null + # ( echo -n "${#username}" | awk '{printf("%c",$0);}'; echo -n "$username"; ) | dd of="$filepath" bs=1 seek=143 conv=notrunc 2> /dev/null + ( decToChar "${#startprog}"; echo -n "$startprog"; ) | writechars "$filepath" 7 + ( decToChar "${#prefix}"; echo -n "$prefix"; ) | writechars "$filepath" 78 + ( decToChar "${#username}"; echo -n "$username"; ) | writechars "$filepath" 143 + + echo "Created $filepath" + if [ ! $alsoguest ]; then + break + fi + + if [ $usersfolder -eq 2 ]; then + usersfolder=1 + fi + doalsoguest=1 + arg='guest' +done + +echo + diff --git a/scripts/mkvolinfo.txt b/scripts/mkvolinfo.txt new file mode 100644 index 0000000..d981670 --- /dev/null +++ b/scripts/mkvolinfo.txt @@ -0,0 +1,82 @@ +#!/bin/bash + +# mkvolinfo -- creates a (share)/.AppleDesktop/.volinfo file + +makeVolInfoFile () { + [[ -d $sharepath/.AppleDesktop ]] || mkdir $sharepath/.AppleDesktop + touch $volinfo + echo 'MAC_CHARSET:MAC_ROMAN' >> $volinfo + echo 'VOL_CHARSET:UTF8' >> $volinfo + echo 'ADOUBLE_VER:v2' >> $volinfo + echo 'CNIDBACKEND:dbd' >> $volinfo + echo 'CNIDDBDHOST:localhost' >> $volinfo + echo 'CNIDDBDPORT:4700' >> $volinfo + echo "CNID_DBPATH:$sharepath" >> $volinfo + echo 'VOLUME_OPTS:PRODOS CACHEID' >> $volinfo + if (( $mixedcase )); then + echo 'VOLCASEFOLD:' >> $volinfo + else + echo 'VOLCASEFOLD:MTOULOWER UTOMUPPER' >> $volinfo + fi + echo 'EXTATTRTYPE:AFPVOL_EA_AD' >> $volinfo + echo ".volinfo for $sharepath has been created." +} + +while [[ $1 == "-f" || $1 == "-c" ]]; do + if [[ $1 == "-f" ]]; then + force=1 + shift + fi + + if [[ $1 == "-c" ]]; then + mixedcase=1 + shift + fi +done + +if [[ ${1:0:1} == "-" ]]; then + + echo "usage: mkvolinfo [-f] [-c] [shared volume path]" + echo + echo "-c will create the .volinfo file to specify no uppercase filename conversion" + echo "-f will create the .volinfo file without prompting, if none exists" + echo "If a path is specified, that is what is used, otherwise the last entry" + echo "in /usr/local/etc/netatalk/AppleVolumes.default is used." + +else + sudo true + if [[ $1 ]]; then + sharepath=$(readlink -m $1) + else + sharepath=$(grep ^/media /usr/local/etc/netatalk/AppleVolumes.default | tail -1 | cut -d" " -f1) + fi + + volinfo=$sharepath/.AppleDesktop/.volinfo + + if [[ ! -d $sharepath ]]; then + echo "$sharepath does not exist." + else + if [[ -f $volinfo ]]; then + echo "$volinfo already exists." + else + if [[ $force ]]; then + makeVolInfoFile + else + echo "The .volinfo file is automatically generated when you first" + echo "log in from an AFP (Apple II or Mac) client machine." + echo "If you can't do this, you can create a .volinfo file now" + echo "based on assumed defaults; proceed with caution if you have" + echo "customized your AppleVolumes files." + echo + echo "If the path shown below is incorrect, you can specify" + echo "the path to your shared volume as an argument to mkvolinfo." + echo + echo -n "Make .volinfo for shared volume $sharepath now? " + read + if [[ ${REPLY:0:1} == "y" ]] || [[ ${REPLY:0:1} == "Y" ]]; then + makeVolInfoFile + fi + fi + fi + fi +fi