- 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
This commit is contained in:
kris 2017-04-16 22:23:04 +01:00
parent f6581c9078
commit b97c404514
3 changed files with 204 additions and 12 deletions

156
src/apple2disk/applesoft.py Normal file
View File

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

View File

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

View File

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