From b97c40451425cb7507bf73f983db01d252155cc1 Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 16 Apr 2017 22:23:04 +0100 Subject: [PATCH] - Rename File class to FileType and add support for a filetype parser - Add support for parsing Applesoft basic files - Add a new File class that receives the contents of a DOS 3.3 file (also parsed, if applicable) - Some minor bugfixes --- src/apple2disk/applesoft.py | 156 ++++++++++++++++++++++++++++++++++++ src/apple2disk/dos33disk.py | 51 +++++++++--- src/apple2disk/process.py | 9 +++ 3 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 src/apple2disk/applesoft.py diff --git a/src/apple2disk/applesoft.py b/src/apple2disk/applesoft.py new file mode 100644 index 0000000..41c2fc6 --- /dev/null +++ b/src/apple2disk/applesoft.py @@ -0,0 +1,156 @@ +import bitstring + +TOKENS = { + 0x80: 'END', + 0x81: 'FOR', + 0x82: 'NEXT', + 0x83: 'DATA', + 0x84: 'INPUT', + 0x85: 'DEL', + 0x86: 'DIM', + 0x87: 'READ', + 0x88: 'GR', + 0x89: 'TEXT', + 0x8A: 'PR #', + 0x8B: 'IN #', + 0x8C: 'CALL', + 0x8D: 'PLOT', + 0x8E: 'HLIN', + 0x8F: 'VLIN', + 0x90: 'HGR2', + 0x91: 'HGR', + 0x92: 'HCOLOR=', + 0x93: 'HPLOT', + 0x94: 'DRAW', + 0x95: 'XDRAW', + 0x96: 'HTAB', + 0x97: 'HOME', + 0x98: 'ROT=', + 0x99: 'SCALE=', + 0x9A: 'SHLOAD', + 0x9B: 'TRACE', + 0x9C: 'NOTRACE', + 0x9D: 'NORMAL', + 0x9E: 'INVERSE', + 0x9F: 'FLASH', + 0xA0: 'COLOR=', + 0xA1: 'POP', + 0xA2: 'VTAB', + 0xA3: 'HIMEM:', + 0xA4: 'LOMEM:', + 0xA5: 'ONERR', + 0xA6: 'RESUME', + 0xA7: 'RECALL', + 0xA8: 'STORE', + 0xA9: 'SPEED=', + 0xAA: 'LET', + 0xAB: 'GOTO', + 0xAC: 'RUN', + 0xAD: 'IF', + 0xAE: 'RESTORE', + 0xAF: '&', + 0xB0: 'GOSUB', + 0xB1: 'RETURN', + 0xB2: 'REM', + 0xB3: 'STOP', + 0xB4: 'ON', + 0xB5: 'WAIT', + 0xB6: 'LOAD', + 0xB7: 'SAVE', + 0xB8: 'DEF FN', + 0xB9: 'POKE', + 0xBA: 'PRINT', + 0xBB: 'CONT', + 0xBC: 'LIST', + 0xBD: 'CLEAR', + 0xBE: 'GET', + 0xBF: 'NEW', + 0xC0: 'TAB', + 0xC1: 'TO', + 0xC2: 'FN', + 0xC3: 'SPC(', + 0xC4: 'THEN', + 0xC5: 'AT', + 0xC6: 'NOT', + 0xC7: 'STEP', + 0xC8: '+', + 0xC9: '-', + 0xCA: '*', + 0xCB: '/', + 0xCC: ';', + 0xCD: 'AND', + 0xCE: 'OR', + 0xCF: '>', + 0xD0: '=', + 0xD1: '<', + 0xD2: 'SGN', + 0xD3: 'INT', + 0xD4: 'ABS', + 0xD5: 'USR', + 0xD6: 'FRE', + 0xD7: 'SCRN (', + 0xD8: 'PDL', + 0xD9: 'POS', + 0xDA: 'SQR', + 0xDB: 'RND', + 0xDC: 'LOG', + 0xDD: 'EXP', + 0xDE: 'COS', + 0xDF: 'SIN', + 0xE0: 'TAN', + 0xE1: 'ATN', + 0xE2: 'PEEK', + 0xE3: 'LEN', + 0xE4: 'STR$', + 0xE5: 'VAL', + 0xE6: 'ASC', + 0xE7: 'CHR$', + 0xE8: 'LEFT$', + 0xE9: 'RIGHT$', + 0xEA: 'MID$' +} + +# TODO: report anomaly if an unknown token is used + +class AppleSoft(object): + def __init__(self, data): + data = bitstring.ConstBitStream(data) + + self.length = data.read('uintle:16') + + self.lines = {} + last_line_number = -1 + last_memory = 0x801 + while data: + next_memory, line_number = data.readlist('uintle:16, uintle:16') + if not next_memory: + break + + line = [] + bytes_read = 4 + while True: + token = data.read('uint:8') + bytes_read += 1 + if token == 0: + self.lines[line_number] = ''.join(line) + break + + if token in TOKENS: + line.append(' ' + TOKENS[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) + + if line_number <= last_line_number: + print "%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 __str__(self): + return '\n'.join('%s %s' % (num, line) for (num, line) in sorted(self.lines.items())) + + diff --git a/src/apple2disk/dos33disk.py b/src/apple2disk/dos33disk.py index 5577c5c..0014b05 100644 --- a/src/apple2disk/dos33disk.py +++ b/src/apple2disk/dos33disk.py @@ -1,20 +1,22 @@ +import applesoft import bitstring import disk as disklib import string PRINTABLE = set(string.letters + string.digits + string.punctuation + ' ') -class File(object): - def __init__(self, short_type, long_type): +class FileType(object): + def __init__(self, short_type, long_type, parser=None): self.short_type = short_type self.long_type = long_type + self.parser = parser FILE_TYPES = { - 0x00: File('T', 'TEXT'), - 0x01: File('I', 'INTEGER BASIC'), + 0x00: FileType('T', 'TEXT'), + 0x01: FileType('I', 'INTEGER BASIC'), # TODO: add handler for parsing file content - 0x02: File('A', 'APPLESOFT BASIC'), - 0x04: File('B', 'BINARY'), + 0x02: FileType('A', 'APPLESOFT BASIC', applesoft.AppleSoft), + 0x04: FileType('B', 'BINARY'), # TODO: others } @@ -152,9 +154,19 @@ class Dos33Disk(disklib.Disk): # TODO: why does DOS 3.3 sometimes display e.g. volume 254 when the VTOC says 178 self.volume = self.vtoc.volume + # List of stripped filenames in catalog order + self.filenames = [] + + # Maps stripped filenames to CatalogEntry objects + self.catalog = {} + self.ReadCatalog() + + # Maps stripped filename to File() object + self.files = {} for catalog_entry in self.catalog.itervalues(): - self.ReadCatalogEntry(catalog_entry) + # TODO: last character has special meaning for deleted files and may legitimately be whitespace. Could collide with a non-deleted file of the same stripped name + self.files[catalog_entry.FileName().rstrip()] = self.ReadCatalogEntry(catalog_entry) def _ReadVTOC(self): return VTOCSector.fromSector(self.ReadSector(0x11, 0x0)) @@ -218,14 +230,14 @@ class Dos33Disk(disklib.Disk): fds = FileDataSector.fromSector(self.ReadSector(t, s), entry.FileName()) contents.append(fds.data) - return contents + return File(entry, contents) 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][0] + 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 = '?' @@ -243,8 +255,8 @@ class CatalogEntry(object): def __init__(self, track, sector, file_type, file_name, length): self.track = track self.sector = sector - self.file_type = file_type & 0x7f - self.locked = file_type & 0x80 + self.file_type = FILE_TYPES[file_type & 0x7f] + self.locked = bool(file_type & 0x80) self.file_name = file_name self.length = length # TODO: handle deleted files (track = 0xff, original track in file_name[0x20]) @@ -253,4 +265,19 @@ class CatalogEntry(object): return '%s' % ''.join([chr(ord(b) & 0x7f) for b in self.file_name]) def __str__(self): - return "Track $%02x Sector $%02x Type %s Name: %s Length: %d" % (self.track, self.sector, FILE_TYPES[self.file_type], self.FileName(), self.length) + type_string = self.file_type.long_type + if self.locked: + type_string += ' (LOCKED)' + return "Track $%02x Sector $%02x Type %s Name: %s Length: %d" % (self.track, self.sector, type_string, self.FileName(), self.length) + + +class File(object): + def __init__(self, catalog_entry, contents): + self.catalog_entry = catalog_entry + + self.contents = contents + parser = catalog_entry.file_type.parser + if parser: + self.parsed_contents = parser(contents) + else: + self.parsed_contents = None \ No newline at end of file diff --git a/src/apple2disk/process.py b/src/apple2disk/process.py index ece88fc..1b47999 100644 --- a/src/apple2disk/process.py +++ b/src/apple2disk/process.py @@ -24,6 +24,14 @@ def main(): try: img = dos33disk.Dos33Disk.Taste(img) print "%s is a DOS 3.3 disk, volume %d" % (f, img.volume) + + for fn in img.filenames: + f = img.files[fn] + + print f.catalog_entry + if f.parsed_contents: + print f.parsed_contents + except IOError: pass except AssertionError: @@ -34,6 +42,7 @@ def main(): for ts, data in sorted(img.sectors.iteritems()): print data + # Group disks by hash of RWTS sector rwts_hashes = {} for f, d in disks.iteritems():