mirror of
https://github.com/KrisKennaway/pyapple2disk.git
synced 2024-11-29 14:50:00 +00:00
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:
parent
8148053965
commit
f3bde766bf
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
20
src/apple2disk/container.py
Normal file
20
src/apple2disk/container.py
Normal 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)
|
@ -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
|
||||||
|
@ -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()
|
Loading…
Reference in New Issue
Block a user