First move toward Disk class w/ a2mg data

Finally!  The Disk class doesn't actually serve as much more than a slightly
improved Globals class at the moment holding every splitting of the source path
and filename that we use in legacy code, as well as a copy of the disk image
itself that gets used long enough to read the a2mg header.

The idea I have here is to begin building the module-based code in parallel.
Then I'll just modify the linear code to compare doing it the old way to doing
it the new.  That'll let me verify that the new code does what the old should.

When it's all done, we can just modify main to use the new modular code and
look at splitting the modular code into a package with cppo as a runner.  At
that point the code should begin being able to do things cppo cannot.  We could
continue to extend cppo at that point, but my inclination is to maintain the
cppo runner as a compatibility layer and begin building a more modern image
tool.  Essentially to begin building the CiderPress for Linux or the Java-free
AppleCommander.
This commit is contained in:
T. Joseph Carter 2017-07-01 03:45:53 -07:00
parent 551ffa0496
commit c20adbabfb

119
cppo
View File

@ -37,8 +37,43 @@ import subprocess
#import tempfile # not used, but should be for temp directory? #import tempfile # not used, but should be for temp directory?
import logging import logging
import struct import struct
from collections import namedtuple
from binascii import a2b_hex, b2a_hex from binascii import a2b_hex, b2a_hex
A2MG1_UNPACK = (
'<' # use little-endian numbers
'4s' # magic string '2IMG'
'4s' # creator string
'H' # header length
'H' # 2mg version
'L' # image format
'L' # flags (we unpack it into "vol")
'L' # number of 512 blocks
'L' # image data offset
'L' # image data length
'L' # comment offset
'L' # comment length
'L' # creator private use offset
'L' # creator private use length
'16x' # reserved for future use
)
A2MG1_ATTRS = (
'magic', 'creator', 'hdr_len', 'version',
'img_fmt', 'vol', 'num_blocks',
'data_offset', 'data_len',
'cmnt_offset', 'cmnt_len',
'creator_offset', 'creator_len'
)
A2MG1_VOL_ATTRS = ('locked', 'dosvol')
# We assume 2mg files with unknown version have these fields
A2MG_UNK_ATTRS = ('magic', 'creator', 'hdr_len', 'version')
A2mg1 = namedtuple('A2mg1', A2MG1_ATTRS)
A2mg1Vol = namedtuple('A2mg1Vol', A2MG1_VOL_ATTRS)
A2mgUnk = namedtuple('A2mgUnk', A2MG_UNK_ATTRS)
class Globals: class Globals:
pass pass
@ -63,7 +98,6 @@ g.DIRPATH = ""
g.target_name = None g.target_name = None
g.target_dir = "" g.target_dir = ""
g.appledouble_dir = None g.appledouble_dir = None
g.image_file = None
g.extract_file = None g.extract_file = None
# runtime options # runtime options
@ -79,12 +113,12 @@ g.dos33 = False # (DOS 3.3 image source, selected automatically
# functions # functions
def pack_u24be(buf: bytes, offset: int, val: int): def pack_u24be(buf: bytearray, offset: int, val: int):
lo16 = val & 0xffff lo16 = val & 0xffff
hi8 = (val >> 16) & 0xff hi8 = (val >> 16) & 0xff
struct.pack_into('>BH', buf, offset, hi8, lo16) struct.pack_into('>BH', buf, offset, hi8, lo16)
def pack_u32be(buf: bytes, offset: int, val: int): def pack_u32be(buf: bytearray, offset: int, val: int):
# Currently unused, will be needed for resource fork dates later # Currently unused, will be needed for resource fork dates later
struct.pack_into('>L', buf, offset, val) struct.pack_into('>L', buf, offset, val)
@ -95,6 +129,35 @@ def unpack_u24le(buf: bytes, offset: int = 0) -> int:
lo16, hi8 = struct.unpack_from('<HB', buf, offset) lo16, hi8 = struct.unpack_from('<HB', buf, offset)
return lo16 | (hi8 << 16) return lo16 | (hi8 << 16)
# FIXME: I know -> namedtuple below is _wrong_. It's a hint, and I am doing
# things wrong here. I'm not 100% sure how to do this right. If you are,
# please submit a more pythonic fix.
def unpack_2mg(buf: bytes, offset: int = 0) -> namedtuple:
try:
# 2017-07-01: Version 1 data is the only kind so far...
a2mg = A2mg1(*struct.unpack_from(A2MG1_UNPACK, buf, offset))
if a2mg.magic == b'2IMG':
if a2mg.version == 1:
vol = A2mg1Vol(
locked=bool(a2mg.vol & (1<<31)),
dosvol=a2mg.vol & 0xff if a2mg.vol & 0x100 else None
)
a2mg = a2mg._replace(vol=vol)
else:
log.warn(
"Unrecognized 2mg version {}: '{}'".format(
a2mg.version, name
)
)
a2mg = A2mgUnk(*a2mg[0:len(A2MG_UNK_ATTRS)])
else:
a2mg = None
except ValueError:
a2mg = None
print(a2mg)
return a2mg
def date_prodos_to_unix(prodos_date: bytes) -> int: def date_prodos_to_unix(prodos_date: bytes) -> int:
"""Returns a UNIX timestamp given a raw ProDOS date""" """Returns a UNIX timestamp given a raw ProDOS date"""
"""The ProDOS date consists of two 16-bit words stored little- """The ProDOS date consists of two 16-bit words stored little-
@ -875,6 +938,22 @@ def isnumber(number):
#---- end IvanX general purpose functions ----# #---- end IvanX general purpose functions ----#
### NEW DISK CLASSES
class Disk:
def __init__(self, name=None):
if name is not None:
self.pathname = name
self.path, self.filename = os.path.split(name)
self.diskname, self.ext = os.path.splitext(self.filename)
self.ext = os.path.splitext(name)[1].lower()
# FIXME: Handle compressed images?
with open(to_sys_name(name), "rb") as f:
self.image = f.read()
self.a2mg = unpack_2mg(self.image)
### LOGGING ### LOGGING
# *sigh* No clean/simple way to use str.format() type log strings without # *sigh* No clean/simple way to use str.format() type log strings without
# jumping through a few hoops # jumping through a few hoops
@ -894,10 +973,11 @@ class StyleAdapter(logging.LoggerAdapter):
def log(self, level, msg, *args, **kwargs): def log(self, level, msg, *args, **kwargs):
if self.isEnabledFor(level): if self.isEnabledFor(level):
msg, kwargs = self.process(msg, kwargs) msg, kwargs = self.process(msg, kwargs)
self.logger._log(level, Message(msg, args), (), **kwargs) self.logger._log(level, Message(str(msg), args), (), **kwargs)
log = StyleAdapter(logging.getLogger(__name__)) log = StyleAdapter(logging.getLogger(__name__))
def main(args: list): def main(args: list):
# Set up our logging facility # Set up our logging facility
handler = logging.StreamHandler(sys.stdout) handler = logging.StreamHandler(sys.stdout)
@ -959,16 +1039,14 @@ def main(args: list):
if len(args) not in (3, 4): if len(args) not in (3, 4):
usage() usage()
g.image_file = args[1] try:
if not os.path.isfile(g.image_file): disk = Disk(args[1])
print('Image/archive file "{}" was not found.'.format(g.image_file)) except IOError as e:
log.critical(e)
quit_now(2) quit_now(2)
g.image_name = os.path.splitext(os.path.basename(g.image_file))
g.image_ext = g.image_name[1].lower()
# automatically set ShrinkIt mode if extension suggests it # automatically set ShrinkIt mode if extension suggests it
if g.src_shk or g.image_ext in ('.shk', '.sdk', '.bxy'): if g.src_shk or disk.ext in ('.shk', '.sdk', '.bxy'):
if os.name == "nt": if os.name == "nt":
print("ShrinkIt archives cannot be extracted on Windows.") print("ShrinkIt archives cannot be extracted on Windows.")
quit_now(2) quit_now(2)
@ -984,6 +1062,7 @@ def main(args: list):
quit_now(2) quit_now(2)
if len(args) == 4: if len(args) == 4:
print(args)
g.extract_file = args[2] g.extract_file = args[2]
if g.extract_file: if g.extract_file:
@ -1009,7 +1088,7 @@ def main(args: list):
makedirs(unshkdir) makedirs(unshkdir)
result = os.system( result = os.system(
"/bin/bash -c 'cd " + unshkdir + "; " "/bin/bash -c 'cd " + unshkdir + "; "
+ "result=$(nulib2 -xse " + os.path.abspath(g.image_file) + "result=$(nulib2 -xse " + os.path.abspath(disk.pathname)
+ ((" " + args[2].replace('/', ':')) + ((" " + args[2].replace('/', ':'))
if g.extract_file else "") + " 2> /dev/null); " if g.extract_file else "") + " 2> /dev/null); "
+ "if [[ $result == \"Failed.\" ]]; then exit 3; " + "if [[ $result == \"Failed.\" ]]; then exit 3; "
@ -1052,7 +1131,7 @@ def main(args: list):
volumeName = toProdosName(fileNames[0].split("#")[0]) volumeName = toProdosName(fileNames[0].split("#")[0])
else: # extract in folder based on disk image name else: # extract in folder based on disk image name
curDir = False curDir = False
volumeName = toProdosName(os.path.basename(g.image_file)) volumeName = toProdosName(os.path.basename(disk.pathname))
if volumeName[-4:].lower() in ('.shk', '.sdk', '.bxy'): if volumeName[-4:].lower() in ('.shk', '.sdk', '.bxy'):
volumeName = volumeName[:-4] volumeName = volumeName[:-4]
if not g.catalog_only and not curDir and not g.extract_file: if not g.catalog_only and not curDir and not g.extract_file:
@ -1099,10 +1178,10 @@ def main(args: list):
# end script if SHK # end script if SHK
g.image_data = load_file(g.image_file) g.image_data = load_file(disk.pathname)
# detect if image is 2mg and remove 64-byte header if so # detect if image is 2mg and remove 64-byte header if so
if g.image_ext in ('.2mg', '.2img'): if disk.ext in ('.2mg', '.2img'):
g.image_data = g.image_data[64:] g.image_data = g.image_data[64:]
# handle 140k disk image # handle 140k disk image
@ -1137,7 +1216,7 @@ def main(args: list):
# fall back on disk extension if weird boot block (e.g. AppleCommander) # fall back on disk extension if weird boot block (e.g. AppleCommander)
if not prodos_disk and not g.dos33: if not prodos_disk and not g.dos33:
log.debug("format and ordering unknown, checking extension") log.debug("format and ordering unknown, checking extension")
if g.image_ext in ('.dsk', '.do'): if disk.ext in ('.dsk', '.do'):
log.debug("extension indicates DO, changing to PO") log.debug("extension indicates DO, changing to PO")
fix_order = True fix_order = True
if fix_order: if fix_order:
@ -1156,12 +1235,14 @@ def main(args: list):
usage() usage()
if g.dos33: if g.dos33:
disk_name = (g.image_name[0] if g.image_ext in ('.dsk', '.do', '.po') disk_name = (disk.diskname
else "".join(g.image_name)) if disk.ext in ('.dsk', '.do', '.po')
else disk.filename)
if g.prodos_names: if g.prodos_names:
disk_name = toProdosName(disk_name) disk_name = toProdosName(disk_name)
if not g.catalog_only: if not g.catalog_only:
g.target_dir = (args[3] if g.extract_file g.target_dir = (args[3]
if g.extract_file
else (args[2] + "/" + disk_name)) else (args[2] + "/" + disk_name))
g.appledouble_dir = (g.target_dir + "/.AppleDouble") g.appledouble_dir = (g.target_dir + "/.AppleDouble")
makedirs(g.target_dir) makedirs(g.target_dir)