mirror of
https://github.com/KrisKennaway/pyapple2disk.git
synced 2024-06-10 16:29:28 +00:00
257 lines
9.3 KiB
Python
257 lines
9.3 KiB
Python
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):
|
|
self.short_type = short_type
|
|
self.long_type = long_type
|
|
|
|
FILE_TYPES = {
|
|
0x00: File('T', 'TEXT'),
|
|
0x01: File('I', 'INTEGER BASIC'),
|
|
# TODO: add handler for parsing file content
|
|
0x02: File('A', 'APPLESOFT BASIC'),
|
|
0x04: File('B', 'BINARY'),
|
|
# TODO: others
|
|
}
|
|
|
|
class VTOCSector(disklib.Sector):
|
|
TYPE = 'DOS 3.3 VTOC'
|
|
|
|
def __init__(self, disk, track, sector, data):
|
|
super(VTOCSector, self).__init__(disk, track, sector, data)
|
|
(
|
|
catalog_track, catalog_sector, dos_release, volume, max_track_sector_pairs,
|
|
last_track_allocated, track_direction, tracks_per_disk, sectors_per_track,
|
|
bytes_per_sector, freemap
|
|
) = data.unpack(
|
|
'pad:8, uint:8, uint:8, uint:8, pad:16, uint:8, pad:256, uint:8, pad:64, uint:8, ' +
|
|
'int:8, pad:16, uint:8, uint:8, uintle:16, bits:1600'
|
|
)
|
|
|
|
# TODO: throw a better exception here to reject the identification as a DOS 3.3 disk
|
|
assert dos_release == 3
|
|
assert bytes_per_sector == disklib.SECTOR_SIZE
|
|
assert sectors_per_track == disklib.SECTORS_PER_TRACK
|
|
|
|
self.catalog_track = catalog_track
|
|
self.catalog_sector = catalog_sector
|
|
|
|
# TODO: why does DOS 3.3 sometimes display e.g. volume 254 when the VTOC says 178
|
|
self.volume = volume
|
|
|
|
# Process freemap
|
|
offset = 0
|
|
track = 0
|
|
while offset < len(freemap):
|
|
track_freemap = freemap[offset:offset+32]
|
|
# Each track freemap is a 32-bit sequence where the sector order is
|
|
# FEDCBA9876543210................
|
|
for sector in xrange(disklib.SECTORS_PER_TRACK):
|
|
free = track_freemap[15-sector]
|
|
|
|
if free:
|
|
old_sector = self.disk.ReadSector(track, sector)
|
|
# check first this is an unclaimed sector
|
|
assert type(old_sector) == disklib.Sector
|
|
FreeSector.fromSector(old_sector)
|
|
# TODO: also handle sectors that are claimed to be used but don't end up getting referenced by anything
|
|
|
|
if track == tracks_per_disk:
|
|
break
|
|
track += 1
|
|
offset += 32
|
|
|
|
class CatalogSector(disklib.Sector):
|
|
TYPE = 'DOS 3.3 Catalog'
|
|
|
|
def __init__(self, disk, track, sector, data):
|
|
super(CatalogSector, self).__init__(disk, track, sector, data)
|
|
|
|
(next_track, next_sector, file_entries) = data.unpack(
|
|
'pad:8, int:8, int:8, pad:64, bits:1960'
|
|
)
|
|
|
|
catalog_entries = []
|
|
offset = 0
|
|
while offset < len(file_entries):
|
|
file_entry = file_entries[offset:offset+(35*8)]
|
|
(file_track, file_sector, file_type, file_name, file_length) = file_entry.unpack(
|
|
'uint:8, uint:8, uint:8, bytes:30, uintle:16'
|
|
)
|
|
if file_track and file_sector:
|
|
entry = CatalogEntry(file_track, file_sector, file_type, file_name, file_length)
|
|
catalog_entries.append(entry)
|
|
offset += (35*8)
|
|
|
|
self.next_track = next_track
|
|
self.next_sector = next_sector
|
|
self.catalog_entries = catalog_entries
|
|
|
|
class FileMetadataSector(disklib.Sector):
|
|
|
|
def __init__(self, disk, track, sector, data, filename):
|
|
super(FileMetadataSector, self).__init__(disk, track, sector, data)
|
|
|
|
self.filename = filename
|
|
self.TYPE = 'DOS 3.3 File Metadata (%s)' % filename
|
|
|
|
(next_track, next_sector, sector_offset, data_sectors) = data.unpack(
|
|
'pad:8, uint:8, uint:8, pad:16, uintle:16, pad:40, bits:1952'
|
|
)
|
|
|
|
offset = 0
|
|
data_track_sectors = []
|
|
while offset < len(data_sectors):
|
|
ds = data_sectors[offset:offset + 16]
|
|
(t, s) = ds.unpack(
|
|
'uint:8, uint:8'
|
|
)
|
|
if t:
|
|
# This may not be the end of the file, it can be sparse.
|
|
data_track_sectors.append((t, s))
|
|
# TODO: should I append a hole here if this is not the last entry?
|
|
offset += 16
|
|
|
|
self.next_track = next_track
|
|
self.next_sector = next_sector
|
|
self.sector_offset = sector_offset
|
|
self.data_track_sectors = data_track_sectors
|
|
|
|
|
|
class FileDataSector(disklib.Sector):
|
|
|
|
def __init__(self, disk, track, sector, data, filename):
|
|
super(FileDataSector, self).__init__(disk, track, sector, data)
|
|
|
|
self.filename = filename
|
|
self.TYPE = 'DOS 3.3 File Contents (%s)' % filename
|
|
|
|
|
|
class FreeSector(disklib.Sector):
|
|
TYPE = "DOS 3.3 Free Sector"
|
|
|
|
def __init__(self, disk, track, sector, data):
|
|
super(FreeSector, self).__init__(disk, track, sector, data)
|
|
|
|
|
|
class Dos33Disk(disklib.Disk):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Dos33Disk, self).__init__(*args, **kwargs)
|
|
# TODO: read DOS tracks and compare to known images
|
|
|
|
self.vtoc = self._ReadVTOC()
|
|
|
|
self.catalog_track = self.vtoc.catalog_track
|
|
self.catalog_sector = self.vtoc.catalog_sector
|
|
|
|
# TODO: why does DOS 3.3 sometimes display e.g. volume 254 when the VTOC says 178
|
|
self.volume = self.vtoc.volume
|
|
|
|
self.ReadCatalog()
|
|
for catalog_entry in self.catalog.itervalues():
|
|
self.ReadCatalogEntry(catalog_entry)
|
|
|
|
def _ReadVTOC(self):
|
|
return VTOCSector.fromSector(self.ReadSector(0x11, 0x0))
|
|
|
|
def ReadCatalog(self):
|
|
next_track = self.catalog_track
|
|
next_sector = self.catalog_sector
|
|
|
|
catalog = {}
|
|
catalog_entries = []
|
|
while next_track and next_sector:
|
|
cs = CatalogSector.fromSector(self.ReadSector(next_track, next_sector))
|
|
(next_track, next_sector, new_entries) = (cs.next_track, cs.next_sector, cs.catalog_entries)
|
|
catalog_entries.extend(new_entries)
|
|
|
|
filenames = []
|
|
for entry in catalog_entries:
|
|
filename = entry.FileName().rstrip()
|
|
catalog[entry.FileName().rstrip()] = entry
|
|
filenames.append(filename)
|
|
|
|
self.filenames = filenames
|
|
self.catalog = catalog
|
|
|
|
def ReadCatalogEntry(self, entry):
|
|
next_track = entry.track
|
|
next_sector = entry.sector
|
|
|
|
sector_list = [None] * entry.length
|
|
# entry.length counts the number of data sectors as well as track/sector list sectors
|
|
track_sector_count = 0
|
|
while next_track and next_sector:
|
|
track_sector_count += 1
|
|
if next_track == 0x00:
|
|
# This entry has never been used, skip it
|
|
break
|
|
if next_track == 0xff:
|
|
# Deleted file
|
|
# TODO: add sector type for this. What to do about sectors claimed by this file that are in use by another file? May discover this before or after this entry
|
|
print "Found deleted file %s" % entry.FileName()
|
|
break
|
|
fs = FileMetadataSector.fromSector(self.ReadSector(next_track, next_sector), entry.FileName())
|
|
(next_track, next_sector) = (fs.next_track, fs.next_sector)
|
|
|
|
num_sectors = len(fs.data_track_sectors)
|
|
sector_list[fs.sector_offset:fs.sector_offset+num_sectors] = fs.data_track_sectors
|
|
|
|
# TODO: Assert we didn't have any holes. Or is this fine e.g. for a sparse text file?
|
|
|
|
#print track_sector_count
|
|
# We allocated space up-front for an unknown number of t/s list sectors, trim them from the end
|
|
sector_list = sector_list[:entry.length - track_sector_count]
|
|
|
|
#print sector_list
|
|
contents = bitstring.BitString()
|
|
for ts in sector_list:
|
|
if not ts:
|
|
#print "XXX found a sparse sector?"
|
|
continue
|
|
(t, s) = ts
|
|
fds = FileDataSector.fromSector(self.ReadSector(t, s), entry.FileName())
|
|
contents.append(fds.data)
|
|
|
|
return 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]
|
|
except KeyError:
|
|
print "%s has unknown file type %02x" % (entry.FileName(), entry.file_type)
|
|
file_type = '?'
|
|
catalog.append(
|
|
'%s%s %03d %s' % (
|
|
'*' if entry.locked else ' ',
|
|
file_type, entry.length,
|
|
entry.FileName()
|
|
)
|
|
)
|
|
return '\n'.join(catalog)
|
|
|
|
|
|
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_name = file_name
|
|
self.length = length
|
|
# TODO: handle deleted files (track = 0xff, original track in file_name[0x20])
|
|
|
|
def FileName(self):
|
|
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)
|