a2cloud/setup/cppo
T. Joseph Carter 55c53b9c10 Clarify license on script files
As noted, Ivan has agreed to allow these scripts to be relicensed under
CC0.  We have one file under LGPL (a unit file we lifted wholesake from
systemd) and the ADTPro wrapper which I'm pretty sure Ivan wrote, but if
he didn't we need to fix its license to be the same as ADTPro.

Either way, to the best of my knowledge, this resolves the question of
how things are licensed explicitly.  (Closes #21)
2018-06-24 21:48:42 -07:00

1386 lines
44 KiB
Python
Executable File

#!/usr/bin/env python
# vim: set tabstop=4 shiftwidth=4 noexpandtab filetype=python:
# cppu - a simple tool to list/extract Apple II archives and disk images
#
# To the extent possible under law, T. Joseph Carter and Ivan Drucker have
# waived all copyright and related or neighboring rights to the a2cloud
# scripts themselves. Software used or installed by these scripts is subject
# to other licenses. This work is published from the United States.
"""cppo: Copy/catalog files from a ProDOS/DOS 3.3/ShrinkIt image/archive.
copy all files: cppo [options] imagefile target_directory
copy one file : cppo [options] imagefile /extract/path target_path
catalog image : cppo -cat [options] imagefile
options:
-shk: ShrinkIt archive as source (also auto-enabled by filename).
-ad : Netatalk-compatible AppleDouble metadata files and resource forks.
-e : Nulib2-compatible filenames with type/auxtype and resource forks.
-uc : Copy GS/OS mixed case filenames as uppercase.
-pro: Adapt DOS 3.3 names to ProDOS and remove addr/len from file data.
/extract/path examples:
/FULL/PRODOS/PATH (ProDOS image source)
"MY FILENAME" (DOS 3.3 image source)
Dir:SubDir:FileName (ShrinkIt archive source)
+ after a file name indicates a GS/OS or Mac OS extended (forked) file.
Wildcard matching (*) is not supported and images are not validated.
ShrinkIt support requires Nulib2. cppo requires Python 2.6+ or 3.0+."""
# cppo by Ivan X, ivan@ivanx.com, ivanx.com/appleii
# Does anyone want to rewrite/refactor this? It works, but it's a mess.
# 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
import shutil
import errno
import uuid
import subprocess
import tempfile
# Intentionally fails on pre-2.6 (no b'') 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.exFileData = None
g.activeDirBlock = None
g.activeFileName = None
g.activeFileSize = None
g.activeFileBytesCopied = 0
g.resourceFork = 0
g.shk_hasrf = False
g.PDOSPATH = []
g.PDOSPATH_INDEX = 0
g.PDOSPATH_SEGMENT = None
g.DIRPATH = ""
g.targetName = None
g.targetDir = ""
g.ADdir = None
g.imageFile = None
g.extractFile = None
# runtime options
g.AD = 0 # -ad (AppleDouble headers + resource forks)
g.EX = 0 # -e (extended filenames + resource forks)
g.CAT = 0 # -cat (catalog only, no extract)
g.UC = 0 # -uc (GS/OS mixed case filenames extract as uppercase)
g.SHK = 0 # -shk (ShrinkIt archive source)
g.PNAME = 0 # -pro (adapt DOS 3.3 names to ProDOS)
g.nomsg = 0 # -s (suppress afpsync message at end)
g.nodir = 0 # -n (don't create parent dir for SHK, extract files in place)
g.D33 = 0 # (DOS 3.3 image source, selected automatically)
# 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))
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)
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()
return adDateHex
# cppo support functions:
# arg1: directory block or [T,S] containing file entry, or shk file dir path
# arg2: file index in overall directory (if applicable), or shk file name
# returns byte position in disk image file
def getStartPos(arg1, arg2):
if g.D33:
return (ts(arg1) + (35 * (arg2 % 7)) + 11)
else: # ProDOS
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 (int(firstByte != 255)*2 if g.D33 else (firstByte//16))
def getFileName(arg1, arg2):
start = getStartPos(arg1, arg2)
if g.D33:
fileNameLo = bytearray()
fileNameHi = readchars(g.imageData, start+3, 30)
for b in fileNameHi:
fileNameLo += to_bytes(to_dec(b)-128)
fileName = bytes(fileNameLo).rstrip()
else: # ProDOS
firstByte = readcharDec(g.imageData, start)
entryType = (firstByte//16)
nameLength = (firstByte - entryType*16)
fileName = readchars(g.imageData, start+1, nameLength)
caseMask = getCaseMask(arg1, arg2)
if (not g.UC and caseMask != None):
for i in range(0, len(fileName)):
if (caseMask[i] == "1"):
fileName = (fileName[:i] +
fileName[i:i+1].lower() +
fileName[i+1:])
return fileName
def getCaseMask(arg1, arg2):
start = getStartPos(arg1, arg2)
caseMaskDec = (readcharDec(g.imageData, start+28) +
readcharDec(g.imageData, start+29)*256)
if (caseMaskDec < 32768):
return None
else:
return to_bin(caseMaskDec - 32768).zfill(15)
def getFileType(arg1, arg2):
if g.SHK:
return arg2.split('#')[1][0:2]
start = getStartPos(arg1, arg2)
if g.D33:
d33fileType = readcharDec(g.imageData, start+2)
if ((d33fileType & 127) == 4): return '06' # BIN
elif ((d33fileType & 127) == 1): return 'FA' # INT
elif ((d33fileType & 127) == 2): return 'FC' # BAS
else: return '04' # TXT or other
else: # ProDOS
return readcharHex(g.imageData, start+16)
def getAuxType(arg1, arg2):
if g.SHK:
return arg2.split('#')[1][2:6]
start = getStartPos(arg1, arg2)
if g.D33:
fileType = getFileType(arg1, arg2)
if (fileType == '06'): # BIN (B)
# file address is in first two bytes of file data
fileTSlist = [readcharDec(g.imageData, start+0),
readcharDec(g.imageData, start+1)]
fileStart = [readcharDec(g.imageData, ts(fileTSlist)+12),
readcharDec(g.imageData, ts(fileTSlist)+13)]
return (readcharHex(g.imageData, ts(fileStart)+1) +
readcharHex(g.imageData, ts(fileStart)+0))
elif (fileType == 'FC'): # BAS (A)
return '0801'
elif (fileType == 'FA'): # INT (I)
return '9600'
else: # TXT (T) or other
return '0000'
else: # ProDOS
return (readcharHex(g.imageData, start+32) +
readcharHex(g.imageData, start+31))
def getKeyPointer(arg1, arg2):
start = getStartPos(arg1, arg2)
if g.D33:
return [readcharDec(g.imageData, start+0),
readcharDec(g.imageData, start+1)]
else: # ProDOS
return (readcharDec(g.imageData, start+17) +
readcharDec(g.imageData, start+18)*256)
def getFileLength(arg1, arg2):
start = getStartPos(arg1, arg2)
if g.D33:
fileType = getFileType(arg1, arg2)
fileTSlist = [readcharDec(g.imageData, start+0),
readcharDec(g.imageData, start+1)]
fileStart = [readcharDec(g.imageData, ts(fileTSlist)+12),
readcharDec(g.imageData, ts(fileTSlist)+13)]
if (fileType == '06'): # BIN (B)
# file length is in second two bytes of file data
return ((readcharDec(g.imageData, ts(fileStart)+2) +
readcharDec(g.imageData, ts(fileStart)+3)*256) + 4)
elif (fileType == 'FC' or fileType == 'FA'): # BAS (A) or INT (I)
# file length is in first two bytes of file data
return ((readcharDec(g.imageData, ts(fileStart)+0) +
readcharDec(g.imageData, ts(fileStart)+1)*256) + 2)
else: # TXT (T) or other
# sadly, we have to walk the whole file
# length is determined by sectors in TSlist, minus wherever
# anything after the first zero in the last sector
fileSize = 0
lastTSpair = None
nextTSlistSector = fileTSlist
endFound = False
while not endFound:
pos = ts(nextTSlistSector)
for tsPos in range(12, 256, 2):
if ts(readcharDec(g.imageData, pos+tsPos+0),
readcharDec(g.imageData, pos+tsPos+1)) != 0:
fileSize += 256
prevTSpair = [readcharDec(g.imageData, (pos+tsPos)+0),
readcharDec(g.imageData, (pos+tsPos)+1)]
else:
lastTSpair = prevTSpair
endFound = True
break
if not lastTSpair:
nextTSlistSector = [readcharDec(g.imageData, pos+1),
readcharDec(g.imageData, pos+2)]
if (nextTSlistSector[0]+nextTSlistSector[1] == 0):
lastTSpair = prevTSpair
endFound = True
break
fileSize -= 256
pos = ts(prevTSpair)
# now find out where the file really ends by finding the last 00
for offset in range(255, -1, -1):
if (readcharDec(g.imageData, pos+offset) != 0):
fileSize += (offset + 1)
break
return fileSize
else: # ProDOS
return (readcharDec(g.imageData, start+21) +
readcharDec(g.imageData, start+22)*256 +
readcharDec(g.imageData, start+23)*65536)
def getCreationDate(arg1, arg2):
#outputs prodos creation date/time as Unix time
# (seconds since Jan 1 1970 GMT)
#or None if there is none
if g.SHK:
return None
elif g.D33:
return None
else: # ProDOS
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
if g.SHK:
modifiedDate = int(time.mktime(
time.strptime(
time.ctime(
os.path.getmtime(os.path.join(arg1, arg2))))))
rVal = modifiedDate
elif g.D33:
rVal = None
else: # ProDOS
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, arg2=None):
# arg1:block, arg2:casemask (optional)
start = ( arg1 * 512 )
firstByte = readcharDec(g.imageData, start+4)
entryType = (firstByte//16)
nameLength = (firstByte - entryType*16)
workingDirName = readchars(g.imageData, start+5, nameLength)
if (entryType == 15): # volume directory, get casemask from header
caseMaskDec = (readcharDec(g.imageData, start+26) +
readcharDec(g.imageData, start+27)*256)
if (caseMaskDec < 32768):
caseMask = None
else:
caseMask = to_bin(caseMaskDec - 32768).zfill(15)
else: # subdirectory, get casemask from arg2 (not available in header)
caseMask = arg2
if (not g.UC and caseMask != None):
for i in range(0, len(workingDirName)):
if (caseMask[i] == "1"):
workingDirName = (workingDirName[:i] +
workingDirName[i:i+1].lower() +
workingDirName[i+1:])
return workingDirName
def getDirEntryCount(arg1):
if g.D33:
entryCount = 0
nextSector = arg1
while True:
top = ts(nextSector)
pos = top+11
for e in range(0, 7):
if (readcharDec(g.imageData, pos+0) == 0):
return entryCount # no more file entries
else:
if (readcharDec(g.imageData, pos+0) != 255):
entryCount += 1 # increment if not deleted file
pos += 35
nextSector = [readcharDec(g.imageData, top+1),
readcharDec(g.imageData, top+2)]
if (nextSector[0]+nextSector[1] == 0): # no more catalog sectors
return entryCount
else: # ProDOS
start = ( arg1 * 512 )
return (readcharDec(g.imageData, start+37) +
readcharDec(g.imageData, start+38)*256)
def getDirNextChunkPointer(arg1):
if g.D33:
start = ts(arg1)
return [readcharDec(g.imageData, start+1),
readcharDec(g.imageData, start+2)]
else: # ProDOS
start = ( arg1 * 512 )
return (readcharDec(g.imageData, start+2) +
readcharDec(g.imageData, start+3)*256)
def toProdosName(name):
i=0
if (name[0:1] == '.'): # eliminate leading period
name = name[1:]
for c in name:
if (c != '.' and not c.isalnum()):
name = name[:i] + '.' + name[i+1:]
i+=1
name = name[0:15]
return name
def ts(track, sector=None):
# returns offset; track and sector can be dec, or hex-ustr
# can also supply as [t,s] for convenience
if (sector == None):
sector = track[1]
track = track[0]
if isinstance(track, type("".encode("L1").decode("L1"))): # hex-ustr
track = int(track, 16)
if isinstance(sector, type("".encode("L1").decode("L1"))): # hex-ustr
sector = int(sector, 16)
return (track*16*256)+(sector*256)
# --- main logic functions
def copyFile(arg1, arg2):
#arg1/arg2:
# ProDOS : directory block / file index in overall directory
# DOS 3.3 : [track, sector] / file index in overall VTOC
# ShrinkIt: directory path / file name
# copies file or dfork to g.outFileData, rfork if any to g.exFileData
g.activeFileBytesCopied = 0
if g.SHK:
with open(os.path.join(arg1, arg2), 'rb') as infile:
g.outFileData += infile.read()
if g.shk_hasrf:
print(" [data fork]")
if (g.EX or g.AD):
print(" [resource fork]")
if (g.exFileData == None):
g.exFileData = bytearray(b'')
with open(os.path.join(arg1, (arg2 + "r")), 'rb') as infile:
g.exFileData += infile.read()
else: # ProDOS or DOS 3.3
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)
if g.PNAME:
# remove address/length data from DOS 3.3 file data if ProDOS target
if (getFileType(arg1, arg2) == '06'):
g.outFileData = g.outFileData[4:]
elif ((getFileType(arg1, arg2) == 'FA') or
getFileType(arg1, arg2) == 'FC'):
g.outFileData = g.outFileData[2:]
def copyBlock(arg1, arg2):
#arg1: block number or [t,s] to copy
#arg2: bytes to write (should be 256 (DOS 3.3) or 512 (ProDOS),
# unless final block with less)
if (arg1 == 0):
outBytes = (b'\x00' * arg2)
else:
outBytes = slyce(g.imageData, (ts(arg1) if g.D33 else arg1*512), arg2)
if (g.resourceFork > 0):
if g.AD or g.EX:
offset = (741 if g.AD else 0)
if (g.exFileData == None):
g.exFileData = bytearray(b'')
g.exFileData[(g.activeFileBytesCopied + offset):
(g.activeFileBytesCopied + offset + 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: ProDOS directory block, or DOS 3.3 [track,sector]
# for key block (with directory header):
# arg2: casemask (optional), arg3:None, arg4:None, arg5:None
# for secondary directory blocks (non-key block):
# arg2/3/4/5: for non-key chunks: entryCount, entry#,
# workingDirName, processedEntryCount
entryCount = None
e = None
pe = None
workingDirName = None
if arg3:
entryCount = arg2
e = arg3
workingDirName = arg4
pe = arg5
else:
e = 0
pe = 0
entryCount = getDirEntryCount(arg1)
if not g.D33:
workingDirName = getWorkingDirName(arg1, arg2).decode("L1")
g.DIRPATH = (g.DIRPATH + "/" + workingDirName)
if g.PDOSPATH_INDEX:
if (g.PDOSPATH_INDEX == 1):
if (("/" + g.PDOSPATH_SEGMENT.lower()) !=
g.DIRPATH.lower()):
print("ProDOS volume name does not match disk image.")
quitNow(2)
else:
g.PDOSPATH_INDEX += 1
g.PDOSPATH_SEGMENT = g.PDOSPATH[g.PDOSPATH_INDEX]
else:
pass
while (pe < entryCount):
if (getStorageType(arg1, e) > 0):
processEntry(arg1, e)
pe += 1
e += 1
if not ((e + (0 if g.D33 else (e>11)) ) % (7 if g.D33 else 13)):
processDir(getDirNextChunkPointer(arg1),
entryCount, e, workingDirName, pe)
break
def processEntry(arg1, arg2):
# arg1=block number, [t,s] if g.D33=1, or subdir name if g.SHK=1
# arg2=index number of entry in directory, or file name if g.SHK=1
'''
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))
'''
eTargetName = None
g.exFileData = None
g.outFileData = bytearray(b'')
if g.SHK: # ShrinkIt archive
g.activeFileName = (arg2 if g.EX else arg2.split('#')[0])
if g.UC:
g.activeFileName = g.activeFileName.upper()
origFileName = g.activeFileName
else: # ProDOS or DOS 3.3 image
g.activeFileName = getFileName(arg1 ,arg2).decode("L1")
origFileName = g.activeFileName
if g.PNAME:
g.activeFileName = toProdosName(g.activeFileName)
g.activeFileSize = getFileLength(arg1, arg2)
if (not g.PDOSPATH_INDEX or
g.activeFileName.upper() == g.PDOSPATH_SEGMENT.upper()):
# if ProDOS directory, not file
if (not g.SHK and getStorageType(arg1, arg2) == 13):
if not g.PDOSPATH_INDEX:
g.targetDir = (g.targetDir + "/" + g.activeFileName)
g.ADdir = (g.targetDir + "/.AppleDouble")
if not (g.CAT or os.path.isdir(g.targetDir)):
makedirs(g.targetDir)
if not (g.CAT 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), getCaseMask(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: # ProDOS or DOS 3.3 file either from image or ShrinkIt archive
dirPrint = ""
if g.DIRPATH:
dirPrint = g.DIRPATH + "/"
else:
if g.SHK:
if ("/".join(dirName.split('/')[3:])):
dirPrint = ("/".join(dirName.split('/')[3:]) + "/")
if (not g.extractFile or
(os.path.basename(g.extractFile.lower()) ==
origFileName.split('#')[0].lower())):
filePrint = g.activeFileName.split("#")[0]
print(dirPrint + filePrint +
("+" if (g.shk_hasrf or
(not g.SHK and getStorageType(arg1, arg2) == 5))
else "") +
((" [" + origFileName + "] ")
if (g.PNAME and (origFileName != g.activeFileName))
else ""))
if g.CAT:
return
if not g.targetName:
g.targetName = g.activeFileName
if g.EX:
if g.SHK:
eTargetName = arg2
else: # ProDOS image
eTargetName = (g.targetName + "#" +
getFileType(arg1, arg2).lower() +
getAuxType(arg1, arg2).lower())
if g.AD:
makeADfile()
copyFile(arg1, arg2)
saveName = (g.targetDir + "/" +
(eTargetName if eTargetName else g.targetName))
saveFile(saveName, 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.exFileData, 637,
(unixDateToADDate(creationDate) +
unixDateToADDate(modifiedDate)))
writecharHex(g.exFileData, 645, "80")
writecharHex(g.exFileData, 649, "80")
#set type/creator
writechars(g.exFileData, 653, b'p')
writecharsHex(g.exFileData, 654,
getFileType(arg1, arg2) +
getAuxType(arg1, arg2))
writechars(g.exFileData, 657, b'pdos')
saveFile(ADfilePath, g.exFileData)
touch(saveName, modifiedDate)
if g.EX: # extended name from ProDOS image
if (g.exFileData != None):
saveFile((saveName + "r"), g.exFileData)
touch((saveName + "r"), modifiedDate)
if (g.PDOSPATH_SEGMENT or
(g.extractFile and
(g.extractFile.lower() == origFileName.lower()))):
quitNow(0)
g.targetName = None
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
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))
if (g.AD or g.EX):
print(" [resource fork]")
if g.AD:
writecharsHex(g.exFileData, 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)
g.resourceFork = 0
def processMasterIndexBlock(arg1):
processIndexBlock(arg1, True)
def processIndexBlock(arg1, arg2=False):
#arg1: indexBlock, or [t,s] of track/sector list
#arg2: if True, it's a Master Index Block
pos = 12 if g.D33 else 0
bytesRemaining = g.activeFileSize
while (g.activeFileBytesCopied < g.activeFileSize):
if g.D33:
targetTS = [readcharDec(g.imageData, ts(arg1)+pos+0),
readcharDec(g.imageData, ts(arg1)+pos+1)]
bytesRemaining = (g.activeFileSize - g.activeFileBytesCopied)
bs = (bytesRemaining if (bytesRemaining < 256) else 256)
copyBlock(targetTS, bs)
pos += 2
if (pos > 255):
# continue with next T/S list sector
processIndexBlock([readcharDec(g.imageData, ts(arg1)+1),
readcharDec(g.imageData, ts(arg1)+2)])
else: # ProDOS
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.exFileData = bytearray(b'\x00' * 741)
# ADv2 header
writecharsHex(g.exFileData, hexToDec("00"), "0005160700020000")
# number of entries
writecharsHex(g.exFileData, hexToDec("18"), "000D")
# Resource Fork
writecharsHex(g.exFileData, hexToDec("1A"), "00000002000002E500000000")
# Real Name
writecharsHex(g.exFileData, hexToDec("26"), "00000003000000B600000000")
# Comment
writecharsHex(g.exFileData, hexToDec("32"), "00000004000001B500000000")
# Dates Info
writecharsHex(g.exFileData, hexToDec("3E"), "000000080000027D00000010")
# Finder Info
writecharsHex(g.exFileData, hexToDec("4A"), "000000090000028D00000020")
# ProDOS file info
writecharsHex(g.exFileData, hexToDec("56"), "0000000B000002C100000008")
# AFP short name
writecharsHex(g.exFileData, hexToDec("62"), "0000000D000002B500000000")
# AFP File Info
writecharsHex(g.exFileData, hexToDec("6E"), "0000000E000002B100000004")
# AFP Directory ID
writecharsHex(g.exFileData, hexToDec("7A"), "0000000F000002AD00000004")
# dbd (second time) will create DEV, INO, SYN, SV~
def quitNow(exitCode=0):
if (exitCode == 0 and not g.nomsg 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.")
if g.SHK: # clean up
for file in os.listdir('/tmp'):
if file.startswith("cppo-"):
shutil.rmtree('/tmp' + "/" + file)
sys.exit(exitCode)
def usage(exitcode=1):
print(sys.modules[__name__].__doc__)
quitNow(exitcode)
# --- 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("L1").decode("L1"))): 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("L1").decode("L1"))): 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"""
if (start_pos < 0):
the_slyce = val[start_pos:]
else:
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):
# hex returns str/bytes in P2, but str/unicode in P3, so
# .encode().decode() always returns unicode in either
if (val < 0): print ("val: " + str(val))
return hex(val)[2:].encode("L1").decode("L1").split("L")[0]
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("L1").decode("L1"))): # hex-ustr
return int(val, 16)
elif isnumber(val): # int/long
return val # so we can use a bytes[x] in P2 or P3
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("L1").decode("L1"))): # 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("L1").decode("L1"))): # hex-ustr
return a2b_hex(bytes(val.encode("L1"))) # works on both P2 and P3
elif isinstance(val, bytes): # so we can use a bytes[x] in P2 or P3
return val
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 OSError as e:
if (e.errno != errno.EEXIST):
raise
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
while True: # breaks when there are no more arguments starting with dash
if (len(args) == 1):
usage()
if (slyce(args[1],0,1) != "-"):
break
if (args[1] == "-s"):
g.nomsg = 1
args = args[1:] #shift
elif (args[1] == "-n"):
g.nodir = 1
args = args[1:] #shift
elif (args[1] == "-uc"):
g.UC = 1
args = args[1:] #shift
elif (args[1] == "-ad"):
g.AD = 1
g.PNAME = 1
args = args[1:] #shift
elif (args[1] == "-shk"):
g.SHK = 1
args = args[1:] #shift
elif (args[1] == "-pro"):
g.PNAME = 1
args = args[1:] #shift
elif (args[1] == "-e"):
g.EX = 1
g.PNAME = 1
args = args[1:] #shift
elif (args[1] == "-cat"):
g.CAT = 1
args = args[1:] #shift
else:
usage()
if g.EX:
if g.AD: usage()
if g.AD:
if g.EX: usage()
if g.CAT:
if not (len(args) == 2): usage()
else:
if not ((len(args) == 3) or (len(args) == 4)): usage()
g.imageFile = args[1]
if not os.path.isfile(g.imageFile):
print("Image/archive file \"" + g.imageFile + "\" was not found.")
quitNow(2)
# automatically set ShrinkIt mode if extension suggests it
if (g.SHK or
g.imageFile[-3:].lower() == "shk" or
g.imageFile[-3:].lower() == "sdk" or
g.imageFile[-3:].lower() == "bxy"):
if (os.name == "nt"):
print("ShrinkIt archives cannot be extracted on Windows.")
quitNow(2)
else:
try:
with open(os.devnull, "w") as fnull:
subprocess.call("nulib2", stdout = fnull, stderr = fnull)
g.SHK=1
except Exception:
print("Nulib2 is not available; not expanding ShrinkIt archive.")
quitNow(2)
if (len(args) == 4):
g.extractFile = args[2]
if g.extractFile:
targetPath = args[3]
if os.path.isdir(targetPath):
g.targetDir = targetPath
elif (targetPath.rsplit("/", 1) > 1):
g.targetDir = targetPath.rsplit("/", 1)[0]
g.targetName = targetPath.rsplit("/", 1)[1]
if not os.path.isdir(g.targetDir):
print("Target directory not found.")
quitNow(2)
else:
if not g.CAT:
if not os.path.isdir(args[2]):
print("Target directory not found.")
quitNow(2)
if g.SHK:
g.PNAME = 0
if not g.CAT:
targetDir = (args[3] if g.extractFile else args[2])
unshkdir = ('/tmp' + "/cppo-" + str(uuid.uuid4()))
makedirs(unshkdir)
result = os.system("/bin/bash -c 'cd " + unshkdir + "; " +
"result=$(nulib2 -xse " + os.path.abspath(g.imageFile) +
((" " + args[2].replace('/', ':'))
if g.extractFile else "") + " 2> /dev/null); " +
"if [[ $result == \"Failed.\" ]]; then exit 3; " +
"else if grep -q \"no records match\" <<< \"$result\"" +
" > /dev/null; then exit 2; else exit 0; fi; fi'")
if (result == 512):
print("File not found in ShrinkIt archive. Try cppo -cat to get the path,")
print(" and omit any leading slash or colon.")
quitNow(1)
elif (result != 0):
print("ShrinkIt archive is invalid, or some other problem happened.")
quitNow(1)
if g.extractFile:
g.extractFile = g.extractFile.replace(':', '/')
extractPath = (unshkdir + "/" + g.extractFile)
extractPathDir = os.path.dirname(extractPath)
# move the extracted file to the root
newunshkdir = ('/tmp' + "/cppo-" + str(uuid.uuid4()))
makedirs(newunshkdir)
for filename in os.listdir(extractPathDir):
shutil.move(extractPathDir + "/" + filename, newunshkdir)
shutil.rmtree(unshkdir)
unshkdir = newunshkdir
fileNames = [name for name in sorted(os.listdir(unshkdir))
if not name.startswith(".")]
if g.nodir: # extract in place from "-n"
curDir = True
elif (len(fileNames) == 1 and
os.path.isdir(unshkdir + "/" + fileNames[0])):
curDir = True # only one folder at top level, so extract in place
volumeName = toProdosName(fileNames[0])
elif (len(fileNames) == 1 and # disk image, so extract in place
fileNames[0][-1:] == "i"):
curDir = True
volumeName = toProdosName(fileNames[0].split("#")[0])
else: # extract in folder based on disk image name
curDir = False
volumeName = toProdosName(os.path.basename(g.imageFile))
if (volumeName[-4:].lower() == ".shk" or
volumeName[-4:].lower() == ".sdk" or
volumeName[-4:].lower() == ".bxy"):
volumeName = volumeName[0:-4]
if not g.CAT and not curDir and not g.extractFile:
print("Extracting into " + volumeName)
# recursively process unshrunk archive hierarchy
for dirName, subdirList, fileList in os.walk(unshkdir):
subdirList.sort()
if not g.CAT:
g.targetDir = (targetDir + ("" if curDir else ("/" + volumeName)) +
("/" if (dirName.count('/') > 2) else "") +
("/".join(dirName.split('/')[3:]))) # chop tempdir
if g.extractFile: # solo item, so don't put it in the tree
g.targetDir = targetDir
if g.UC:
g.targetDir = g.targetDir.upper()
g.ADdir = (g.targetDir + "/.AppleDouble")
makedirs(g.targetDir)
if g.AD:
makedirs(g.ADdir)
for fname in sorted(fileList):
if (fname[-1:] == "i"):
# disk image; rename to include suffix and correct type/auxtype
imagePath = os.path.join(dirName, fname).split("#")[0]
new_name = (imagePath +
("" if (imagePath.lower().endswith(".po") or
imagePath.lower().endswith(".hdv"))
else ".PO") + "#e00005")
os.rename(os.path.join(dirName, fname), new_name)
fname = os.path.basename(new_name)
g.shk_hasrf = False
rfork = False
if (fname[-1:] == "r" and
os.path.isfile(os.path.join(dirName, fname[:-1]))):
rfork = True
elif (os.path.isfile(os.path.join(dirName, (fname + "r")))):
g.shk_hasrf = True
if not rfork:
processEntry(dirName, fname)
shutil.rmtree(unshkdir, True)
quitNow(0)
# end script if SHK
g.imageData = loadFile(g.imageFile)
# detect if image is 2mg and remove 64-byte header if so
if (g.imageFile.lower().endswith(".2mg") or
g.imageFile.lower().endswith(".2img")):
g.imageData = g.imageData[64:]
# handle 140K disk image
if (len(g.imageData) == 143360):
prodosDisk = 0
fixOrder = 0
# is it ProDOS?
if (to_hex(readchars(g.imageData, ts(0,0)+0, 4)) == '0138b003'):
if (readchars(g.imageData, ts(0,1)+3, 6) == b'PRODOS'):
prodosDisk = 1
elif (readchars(g.imageData, ts(0,14)+3, 6) == b'PRODOS'):
prodosDisk = 1
fixOrder = 1
# is it DOS 3.3?
else:
if (readcharDec(g.imageData, ts(17,0)+3) == 3):
vtocT = readcharDec(g.imageData, ts(17,0)+1)
vtocS = readcharDec(g.imageData, ts(17,0)+2)
if (vtocT<35 and vtocS<16):
g.D33 = 1
# it's DOS 3.3; check sector order next
if (readcharDec(g.imageData, ts(17,14)+2) != 13):
fixOrder = 1
# fall back on disk extension if weird boot block (e.g. AppleCommander)
if not prodosDisk and not g.D33:
if (g.imageFile.lower().endswith(".dsk") or
g.imageFile.lower().endswith(".do")):
fixOrder = 1
if fixOrder:
# for each track,
# read each sector in the right sequence to make
# valid ProDOS blocks (sector pairs)
imageDataFixed = bytearray(143360)
for t in range(0, 35):
for s in [0, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 15]:
writechars(imageDataFixed,
ts(t,((15-s) if (s%15) else s)),
readchars(g.imageData, ts(t,s), 256))
g.imageData = bytes(imageDataFixed)
if not prodosDisk and not g.D33:
print("Warning: Unable to determine disk format, assuming ProDOS.")
# enforce leading slash if ProDOS
if (not g.SHK and
not g.D33 and
g.extractFile and
(slyce(args[2],0,1) != "/") and
(slyce(args[2],0,1) != ":")):
usage()
if g.D33:
diskName = os.path.basename(g.imageFile)
if (diskName[-4:].lower() == ".dsk" or
diskName[-3:].lower() == ".do" or
diskName[-3:].lower() == ".po"):
diskName = os.path.splitext(diskName)[0]
if g.PNAME:
diskName = toProdosName(diskName)
if not g.CAT:
g.targetDir = (args[3] if g.extractFile
else (args[2] + "/" + diskName))
g.ADdir = (g.targetDir + "/.AppleDouble")
makedirs(g.targetDir)
if g.AD:
makedirs(g.ADdir)
if not g.extractFile:
print("Extracting into " + diskName)
processDir([readcharDec(g.imageData, ts(17,0)+1),
readcharDec(g.imageData, ts(17,0)+2)])
if g.extractFile:
print("ProDOS file not found within image file.")
quitNow(0)
# below: ProDOS
g.activeDirBlock = 0
g.activeFileName = ""
g.activeFileSize = 0
g.activeFileBytesCopied = 0
g.resourceFork = 0
g.PDOSPATH_INDEX = 0
g.PNAME = 0
if g.extractFile:
g.PDOSPATH = g.extractFile.replace(':', '/').split('/')
g.extractFile = None
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.")
quitNow(2)
else:
if not g.CAT:
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.CAT:
quitNow(0)