#!/usr/bin/env python # vim: set tabstop=4 shiftwidth=4 expandtab filetype=python: """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)) # 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 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): #print("pos: " + to_hex(pos)) 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 = [readcharDec(g.imageData, ts(arg1)+1), # readcharDec(g.imageData, ts(arg1)+2)] 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) #print(arg1 + " " + arg2 + " " + g.activeFileBytesCopied) 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 # print(g.DIRPATH) while (pe < entryCount): if (getStorageType(arg1, e) > 0): #print(pe, e, entryCount) 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()) # touch(g.targetDir + "/" + g.targetName) 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 #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) 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) # print() 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)] #print(to_hex(targetTS[0]),to_hex(targetTS[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 # print(filePath) 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): #print("140K disk") prodosDisk = 0 fixOrder = 0 # is it ProDOS? if (to_hex(readchars(g.imageData, ts(0,0)+0, 4)) == '0138b003'): #print("detected ProDOS by boot block") if (readchars(g.imageData, ts(0,1)+3, 6) == b'PRODOS'): prodosDisk = 1 #print("order OK (PO)") elif (readchars(g.imageData, ts(0,14)+3, 6) == b'PRODOS'): #print("order needs fixing (DO)") prodosDisk = 1 fixOrder = 1 # is it DOS 3.3? else: #print("it's not ProDOS") 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): #print("it's DOS 3.3") g.D33 = 1 # it's DOS 3.3; check sector order next if (readcharDec(g.imageData, ts(17,14)+2) != 13): #print("order needs fixing (PO)") fixOrder = 1 #else: # remove this # print("order OK (DO)") # pass # fall back on disk extension if weird boot block (e.g. AppleCommander) if not prodosDisk and not g.D33: #print("format and ordering unknown, checking extension") if (g.imageFile.lower().endswith(".dsk") or g.imageFile.lower().endswith(".do")): fixOrder = 1 # print("extension indicates DO, changing to PO") if fixOrder: #print("fixing order") # 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) #print("saving fixed order file as outfile.dsk") #saveFile("outfile.dsk", g.imageData) #print("saved") 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: # 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.CAT: quitNow(0)