mirror of
https://github.com/RasppleII/a2server.git
synced 2024-12-23 08:29:50 +00:00
1418 lines
52 KiB
Python
Executable File
1418 lines
52 KiB
Python
Executable File
#!/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
|
|
|
|
# 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.")
|
|
sys.exit(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()))):
|
|
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)
|
|
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 syncExit():
|
|
if (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.")
|
|
# saveFile(g.imageFile, g.imageData)
|
|
sys.exit(0)
|
|
|
|
def usage(exitcode=1):
|
|
print(sys.modules[__name__].__doc__)
|
|
sys.exit(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.")
|
|
sys.exit(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.")
|
|
sys.exit(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.")
|
|
sys.exit(2)
|
|
|
|
if (len(args) == 4):
|
|
g.extractFile = args[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.")
|
|
sys.exit(1)
|
|
elif (result != 0):
|
|
print("ShrinkIt archive is invalid, or some other problem happened.")
|
|
sys.exit(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) == 2 and # disk image, so extract in place
|
|
fileNames[1][-1:] == "i" and
|
|
fileNames[0] == fileNames[1][:-1]):
|
|
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):
|
|
g.shk_hasrf = False
|
|
rfork = False
|
|
disk_image = False
|
|
if (fname[-1:] == "r" and
|
|
os.path.isfile(os.path.join(dirName, fname[:-1]))):
|
|
rfork = True
|
|
elif (fname[-1:] == "i"):
|
|
disk_image = True
|
|
elif (os.path.isfile(os.path.join(dirName, (fname + "r")))):
|
|
g.shk_hasrf = True
|
|
elif (os.path.isfile(os.path.join(dirName, (fname + "i")))):
|
|
# disk image; remove shell image
|
|
os.remove(os.path.join(dirName, fname))
|
|
imagePath = os.path.join(dirName, fname).split("#")[0]
|
|
new_name = (imagePath +
|
|
("" if (fname.lower().endswith(".po") or
|
|
fname.lower().endswith(".hdv"))
|
|
else ".po") + "#e00005")
|
|
os.rename((os.path.join(dirName, fname) + "i"), new_name)
|
|
fname = os.path.basename(new_name)
|
|
if not rfork and not disk_image:
|
|
processEntry(dirName, fname)
|
|
shutil.rmtree(unshkdir, True)
|
|
syncExit()
|
|
|
|
# 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.extractFile:
|
|
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.CAT:
|
|
if not os.path.isdir(args[2]):
|
|
print("Target directory not found.")
|
|
sys.exit(2)
|
|
|
|
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.")
|
|
syncExit()
|
|
|
|
# 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.")
|
|
sys.exit(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:
|
|
syncExit()
|
|
|