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