From f3bde766bf713d79438ba752e1e5d68f906e73c4 Mon Sep 17 00:00:00 2001 From: kris Date: Thu, 20 Apr 2017 23:04:24 +0100 Subject: [PATCH] Add a container.Container() type that carries a list of Anomaly() objects associated to the container, and builds a tree of child containers with support for depth-first recursion Convert all of the disk container classes to use Container() and register themselves with this tree. This constructs a hierarchy of container objects rooted in Disk that describe the structures found on the disk image. Convert most of the print statements and some of the assertions to register Anomaly records instead Fix/improve some of the __str__ representations --- src/apple2disk/applesoft.py | 47 ++++++++++++++++++++++++--------- src/apple2disk/container.py | 20 ++++++++++++++ src/apple2disk/disk.py | 15 +++++++++-- src/apple2disk/dos33disk.py | 52 +++++++++++++++++++++++++------------ 4 files changed, 104 insertions(+), 30 deletions(-) create mode 100644 src/apple2disk/container.py diff --git a/src/apple2disk/applesoft.py b/src/apple2disk/applesoft.py index 41c2fc6..eef9e7c 100644 --- a/src/apple2disk/applesoft.py +++ b/src/apple2disk/applesoft.py @@ -1,3 +1,5 @@ +import anomaly +import container import bitstring TOKENS = { @@ -110,15 +112,18 @@ TOKENS = { 0xEA: 'MID$' } -# TODO: report anomaly if an unknown token is used +class AppleSoft(container.Container): + def __init__(self, filename, data): + super(AppleSoft, self).__init__() -class AppleSoft(object): - def __init__(self, data): + self.filename = filename data = bitstring.ConstBitStream(data) + # TODO: assert length is met self.length = data.read('uintle:16') - self.lines = {} + self.lines = [] + self.program = {} last_line_number = -1 last_memory = 0x801 while data: @@ -132,25 +137,43 @@ class AppleSoft(object): token = data.read('uint:8') bytes_read += 1 if token == 0: - self.lines[line_number] = ''.join(line) + self.lines.append(line_number) + self.program[line_number] = ''.join(line) break - if token in TOKENS: - line.append(' ' + TOKENS[token] + ' ') + if token >= 0x80: + try: + line.append(' ' + TOKENS[token] + ' ') + except KeyError: + self.anomalies.append(anomaly.Anomaly( + self, anomaly.CORRUPTION, 'Line number %d contains unexpected token: %02X' % ( + line_number, token) + ) + ) else: line.append(chr(token)) if last_memory + bytes_read != next_memory: - print "%x + %x == %x != %x (gap %d)" % (last_memory, bytes_read, last_memory + bytes_read, next_memory, next_memory - last_memory - bytes_read) + self.anomalies.append(anomaly.Anomaly( + self, anomaly.UNUSUAL, "%x + %x == %x != %x (gap %d)" % ( + last_memory, bytes_read, last_memory + bytes_read, next_memory, + next_memory - last_memory - bytes_read) + ) + ) if line_number <= last_line_number: - print "%d <= %d: %s" % (line_number, last_line_number, ''.join(line)) + self.anomalies.append(anomaly.Anomaly( + self, anomaly.UNUSUAL, "%d <= %d: %s" % ( + line_number, last_line_number, ''.join(line)) + ) + ) - print "%d %s" % (line_number, ''.join(line)) last_line_number = line_number last_memory = next_memory + def List(self): + return '\n'.join('%s %s' % (num, self.program[num]) for num in self.lines) + def __str__(self): - return '\n'.join('%s %s' % (num, line) for (num, line) in sorted(self.lines.items())) - + return 'AppleSoft(%s)' % self.filename diff --git a/src/apple2disk/container.py b/src/apple2disk/container.py new file mode 100644 index 0000000..a603942 --- /dev/null +++ b/src/apple2disk/container.py @@ -0,0 +1,20 @@ +class Container(object): + """Generic container type, every structure on the disk extends from this.""" + + def __init__(self): + self.anomalies = [] + + self.parent = None + self.children = [] + + def AddChild(self, child): + assert child.parent is None, "%s already has parent %s" % (child, child.parent) + + self.children.append(child) + child.parent = self + + def Recurse(self, callback): + """Depth-first recursive traversal of children.""" + for child in self.children: + callback(child) + child.Recurse(callback) diff --git a/src/apple2disk/disk.py b/src/apple2disk/disk.py index 1469c25..f4335ea 100644 --- a/src/apple2disk/disk.py +++ b/src/apple2disk/disk.py @@ -1,3 +1,6 @@ +import anomaly +import container + import bitstring import hashlib import zlib @@ -12,16 +15,20 @@ TRACK_SIZE = SECTORS_PER_TRACK * SECTOR_SIZE class IOError(Exception): pass -class Disk(object): +class Disk(container.Container): def __init__(self, name, data): + super(Disk, self).__init__() + self.name = name self.data = data + # TODO: support larger disk sizes assert len(data) == 140 * 1024 self.hash = hashlib.sha1(data).hexdigest() self.sectors = {} + # Pre-load all sectors into map for (track, sector) in self.EnumerateSectors(): self._ReadSector(track, sector) @@ -47,6 +54,8 @@ class Disk(object): raise IOError("Track $%02x sector $%02x out of bounds" % (track, sector)) data = bitstring.BitString(self.data[offset:offset + SECTOR_SIZE]) + + # This calls SetSectorOwner to register in self.sectors return Sector(self, track, sector, data) def ReadSector(self, track, sector): @@ -56,11 +65,12 @@ class Disk(object): raise IOError("Track $%02x sector $%02x out of bounds" % (track, sector)) -class Sector(object): +class Sector(container.Container): # TODO: other types will include: VTOC, Catalog, File metadata, File content, Deleted file, Free space TYPE = 'Unknown sector' def __init__(self, disk, track, sector, data): + super(Sector, self).__init__() # Reference back to parent disk self.disk = disk @@ -75,6 +85,7 @@ class Sector(object): self.compress_ratio = len(compressed_data) * 100 / len(data.tobytes()) disk.SetSectorOwner(track, sector, self) + disk.AddChild(self) # TODO: if all callers are using disk.ReadSector(track, sector) to get the sector then do that here @classmethod diff --git a/src/apple2disk/dos33disk.py b/src/apple2disk/dos33disk.py index 26d05ac..3ef0b2e 100644 --- a/src/apple2disk/dos33disk.py +++ b/src/apple2disk/dos33disk.py @@ -1,8 +1,11 @@ +import anomaly import applesoft -import bitstring +import container import disk as disklib import utils +import bitstring + class FileType(object): def __init__(self, short_type, long_type, parser=None): self.short_type = short_type @@ -56,7 +59,13 @@ class VTOCSector(disklib.Sector): if free: old_sector = self.disk.ReadSector(track, sector) # check first this is an unclaimed sector - assert type(old_sector) == disklib.Sector + if type(old_sector) != disklib.Sector: + self.anomalies.append( + anomaly.Anomaly( + self, anomaly.CORRUPTION, 'VTOC claims used sector is free: %s' % old_sector + ) + ) + FreeSector.fromSector(old_sector) # TODO: also handle sectors that are claimed to be used but don't end up getting referenced by anything @@ -232,17 +241,15 @@ class Dos33Disk(disklib.Disk): continue contents.append(fds.data) - return File(entry, contents) + newfile = File(entry, contents) + self.AddChild(newfile) + return newfile def __str__(self): catalog = ['DISK VOLUME %d\n' % self.volume] - for filename in self.catalog: - entry = self.files[filename] - try: - file_type = FILE_TYPES[entry.file_type].short_type - except KeyError: - print "%s has unknown file type %02x" % (entry.FileName(), entry.file_type) - file_type = '?' + for filename in self.filenames: + entry = self.catalog[filename] + file_type = entry.file_type.short_type catalog.append( '%s%s %03d %s' % ( '*' if entry.locked else ' ', @@ -253,10 +260,13 @@ class Dos33Disk(disklib.Disk): return '\n'.join(catalog) -class CatalogEntry(object): +class CatalogEntry(container.Container): def __init__(self, track, sector, file_type, file_name, length): + super(CatalogEntry, self).__init__() + self.track = track self.sector = sector + # TODO: add anomaly for unknown file type self.file_type = FILE_TYPES[file_type & 0x7f] self.locked = bool(file_type & 0x80) self.file_name = file_name @@ -273,16 +283,26 @@ class CatalogEntry(object): return "Track $%02x Sector $%02x Type %s Name: %s Length: %d" % (self.track, self.sector, type_string, self.FileName(), self.length) -class File(object): +class File(container.Container): def __init__(self, catalog_entry, contents): + super(File, self).__init__() + self.catalog_entry = catalog_entry self.contents = contents self.parsed_contents = None + parser = catalog_entry.file_type.parser if parser: try: - self.parsed_contents = parser(contents) - except Exception: - print "Failed to parse file %s" % self.catalog_entry - print utils.HexDump(contents.tobytes()) \ No newline at end of file + self.parsed_contents = parser(catalog_entry.FileName(), contents) + self.AddChild(self.parsed_contents) + except Exception, e: + self.anomalies.append( + anomaly.Anomaly( + self, anomaly.CORRUPTION, 'Failed to parse file %s: %s' % (self.catalog_entry, e) + ) + ) + + def __str__(self): + return 'File(%s)' % self.catalog_entry.FileName() \ No newline at end of file