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
This commit is contained in:
kris 2017-04-20 23:04:24 +01:00
parent 8148053965
commit f3bde766bf
4 changed files with 104 additions and 30 deletions

View File

@ -1,3 +1,5 @@
import anomaly
import container
import bitstring import bitstring
TOKENS = { TOKENS = {
@ -110,15 +112,18 @@ TOKENS = {
0xEA: 'MID$' 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): self.filename = filename
def __init__(self, data):
data = bitstring.ConstBitStream(data) data = bitstring.ConstBitStream(data)
# TODO: assert length is met
self.length = data.read('uintle:16') self.length = data.read('uintle:16')
self.lines = {} self.lines = []
self.program = {}
last_line_number = -1 last_line_number = -1
last_memory = 0x801 last_memory = 0x801
while data: while data:
@ -132,25 +137,43 @@ class AppleSoft(object):
token = data.read('uint:8') token = data.read('uint:8')
bytes_read += 1 bytes_read += 1
if token == 0: if token == 0:
self.lines[line_number] = ''.join(line) self.lines.append(line_number)
self.program[line_number] = ''.join(line)
break break
if token in TOKENS: if token >= 0x80:
line.append(' ' + TOKENS[token] + ' ') 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: else:
line.append(chr(token)) line.append(chr(token))
if last_memory + bytes_read != next_memory: 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: 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_line_number = line_number
last_memory = next_memory last_memory = next_memory
def List(self):
return '\n'.join('%s %s' % (num, self.program[num]) for num in self.lines)
def __str__(self): def __str__(self):
return '\n'.join('%s %s' % (num, line) for (num, line) in sorted(self.lines.items())) return 'AppleSoft(%s)' % self.filename

View File

@ -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)

View File

@ -1,3 +1,6 @@
import anomaly
import container
import bitstring import bitstring
import hashlib import hashlib
import zlib import zlib
@ -12,16 +15,20 @@ TRACK_SIZE = SECTORS_PER_TRACK * SECTOR_SIZE
class IOError(Exception): class IOError(Exception):
pass pass
class Disk(object): class Disk(container.Container):
def __init__(self, name, data): def __init__(self, name, data):
super(Disk, self).__init__()
self.name = name self.name = name
self.data = data self.data = data
# TODO: support larger disk sizes # TODO: support larger disk sizes
assert len(data) == 140 * 1024 assert len(data) == 140 * 1024
self.hash = hashlib.sha1(data).hexdigest() self.hash = hashlib.sha1(data).hexdigest()
self.sectors = {} self.sectors = {}
# Pre-load all sectors into map
for (track, sector) in self.EnumerateSectors(): for (track, sector) in self.EnumerateSectors():
self._ReadSector(track, sector) self._ReadSector(track, sector)
@ -47,6 +54,8 @@ class Disk(object):
raise IOError("Track $%02x sector $%02x out of bounds" % (track, sector)) raise IOError("Track $%02x sector $%02x out of bounds" % (track, sector))
data = bitstring.BitString(self.data[offset:offset + SECTOR_SIZE]) data = bitstring.BitString(self.data[offset:offset + SECTOR_SIZE])
# This calls SetSectorOwner to register in self.sectors
return Sector(self, track, sector, data) return Sector(self, track, sector, data)
def ReadSector(self, track, sector): def ReadSector(self, track, sector):
@ -56,11 +65,12 @@ class Disk(object):
raise IOError("Track $%02x sector $%02x out of bounds" % (track, sector)) 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 # TODO: other types will include: VTOC, Catalog, File metadata, File content, Deleted file, Free space
TYPE = 'Unknown sector' TYPE = 'Unknown sector'
def __init__(self, disk, track, sector, data): def __init__(self, disk, track, sector, data):
super(Sector, self).__init__()
# Reference back to parent disk # Reference back to parent disk
self.disk = disk self.disk = disk
@ -75,6 +85,7 @@ class Sector(object):
self.compress_ratio = len(compressed_data) * 100 / len(data.tobytes()) self.compress_ratio = len(compressed_data) * 100 / len(data.tobytes())
disk.SetSectorOwner(track, sector, self) 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 # TODO: if all callers are using disk.ReadSector(track, sector) to get the sector then do that here
@classmethod @classmethod

View File

@ -1,8 +1,11 @@
import anomaly
import applesoft import applesoft
import bitstring import container
import disk as disklib import disk as disklib
import utils import utils
import bitstring
class FileType(object): class FileType(object):
def __init__(self, short_type, long_type, parser=None): def __init__(self, short_type, long_type, parser=None):
self.short_type = short_type self.short_type = short_type
@ -56,7 +59,13 @@ class VTOCSector(disklib.Sector):
if free: if free:
old_sector = self.disk.ReadSector(track, sector) old_sector = self.disk.ReadSector(track, sector)
# check first this is an unclaimed 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) FreeSector.fromSector(old_sector)
# TODO: also handle sectors that are claimed to be used but don't end up getting referenced by anything # 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 continue
contents.append(fds.data) contents.append(fds.data)
return File(entry, contents) newfile = File(entry, contents)
self.AddChild(newfile)
return newfile
def __str__(self): def __str__(self):
catalog = ['DISK VOLUME %d\n' % self.volume] catalog = ['DISK VOLUME %d\n' % self.volume]
for filename in self.catalog: for filename in self.filenames:
entry = self.files[filename] entry = self.catalog[filename]
try: file_type = entry.file_type.short_type
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 = '?'
catalog.append( catalog.append(
'%s%s %03d %s' % ( '%s%s %03d %s' % (
'*' if entry.locked else ' ', '*' if entry.locked else ' ',
@ -253,10 +260,13 @@ class Dos33Disk(disklib.Disk):
return '\n'.join(catalog) return '\n'.join(catalog)
class CatalogEntry(object): class CatalogEntry(container.Container):
def __init__(self, track, sector, file_type, file_name, length): def __init__(self, track, sector, file_type, file_name, length):
super(CatalogEntry, self).__init__()
self.track = track self.track = track
self.sector = sector self.sector = sector
# TODO: add anomaly for unknown file type
self.file_type = FILE_TYPES[file_type & 0x7f] self.file_type = FILE_TYPES[file_type & 0x7f]
self.locked = bool(file_type & 0x80) self.locked = bool(file_type & 0x80)
self.file_name = file_name 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) 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): def __init__(self, catalog_entry, contents):
super(File, self).__init__()
self.catalog_entry = catalog_entry self.catalog_entry = catalog_entry
self.contents = contents self.contents = contents
self.parsed_contents = None self.parsed_contents = None
parser = catalog_entry.file_type.parser parser = catalog_entry.file_type.parser
if parser: if parser:
try: try:
self.parsed_contents = parser(contents) self.parsed_contents = parser(catalog_entry.FileName(), contents)
except Exception: self.AddChild(self.parsed_contents)
print "Failed to parse file %s" % self.catalog_entry except Exception, e:
print utils.HexDump(contents.tobytes()) 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()