mirror of
https://github.com/RasppleII/a2cloud.git
synced 2024-12-22 13:30:27 +00:00
55c53b9c10
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)
1386 lines
44 KiB
Python
Executable File
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)
|
|
|