2017-05-07 20:28:15 +00:00
|
|
|
from __future__ import print_function
|
|
|
|
from __future__ import absolute_import
|
2016-07-20 15:13:44 +00:00
|
|
|
import numpy as np
|
|
|
|
|
2017-05-07 20:28:15 +00:00
|
|
|
from .errors import *
|
|
|
|
from .diskimages import BaseHeader, DiskImageBase
|
|
|
|
from .utils import Directory, VTOC, WriteableSector, BaseSectorList, Dirent
|
|
|
|
from .segments import DefaultSegment, EmptySegment, ObjSegment, RawTrackSectorSegment, SegmentSaver, get_style_bits, SegmentData
|
2016-07-20 15:13:44 +00:00
|
|
|
|
|
|
|
import logging
|
|
|
|
log = logging.getLogger(__name__)
|
2017-05-07 19:23:51 +00:00
|
|
|
try: # Expensive debugging
|
|
|
|
_xd = _expensive_debugging
|
|
|
|
except NameError:
|
|
|
|
_xd = False
|
2016-07-20 15:13:44 +00:00
|
|
|
|
|
|
|
|
2017-02-24 03:13:48 +00:00
|
|
|
class Dos33TSSector(WriteableSector):
|
2017-02-24 06:16:18 +00:00
|
|
|
def __init__(self, header, sector_list=None, start=None, end=None, data=None):
|
|
|
|
WriteableSector.__init__(self, header.sector_size, data)
|
2017-02-24 03:13:48 +00:00
|
|
|
self.header = header
|
|
|
|
self.used = header.sector_size
|
2017-02-24 06:16:18 +00:00
|
|
|
if data is None:
|
|
|
|
self.set_tslist(sector_list, start, end)
|
2017-02-24 03:13:48 +00:00
|
|
|
|
|
|
|
def set_tslist(self, sector_list, start, end):
|
|
|
|
index = 0xc
|
|
|
|
for i in range(start, end):
|
|
|
|
sector = sector_list[i]
|
|
|
|
t, s = self.header.track_from_sector(sector.sector_num)
|
|
|
|
self.data[index] = t
|
|
|
|
self.data[index + 1] = s
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("tslist entry #%d: %d, %d" % (index, t, s))
|
2017-02-24 03:13:48 +00:00
|
|
|
index += 2
|
|
|
|
|
2017-02-24 06:16:18 +00:00
|
|
|
def get_tslist(self):
|
|
|
|
index = 0xc
|
|
|
|
sector_list = []
|
|
|
|
while index < self.header.sector_size:
|
|
|
|
t = self.data[index]
|
|
|
|
s = self.data[index + 1]
|
|
|
|
sector_list.append(self.header.sector_from_track(t, s))
|
|
|
|
index += 2
|
|
|
|
return sector_list
|
|
|
|
|
2017-02-24 03:13:48 +00:00
|
|
|
@property
|
|
|
|
def next_sector_num(self):
|
2017-02-24 06:16:18 +00:00
|
|
|
t = self.data[1]
|
|
|
|
s = self.data[2]
|
|
|
|
return self.header.sector_from_track(t, s)
|
2017-02-24 03:13:48 +00:00
|
|
|
|
|
|
|
@next_sector_num.setter
|
|
|
|
def next_sector_num(self, value):
|
|
|
|
self._next_sector_num = value
|
|
|
|
t, s = self.header.track_from_sector(value)
|
|
|
|
self.data[1] = t
|
|
|
|
self.data[2] = s
|
|
|
|
|
|
|
|
|
2017-02-22 15:19:52 +00:00
|
|
|
class Dos33VTOC(VTOC):
|
2017-02-23 07:19:12 +00:00
|
|
|
max_tracks = (256 - 0x38) / 4 # 50, but kept here in case sector size changed
|
|
|
|
max_sectors = max_tracks * 16
|
2017-02-23 20:04:14 +00:00
|
|
|
vtoc_bit_reorder_index = np.tile(np.arange(15, -1, -1), max_tracks) + (np.repeat(np.arange(max_tracks), 16) * 16)
|
2017-02-23 07:19:12 +00:00
|
|
|
|
2017-02-22 15:19:52 +00:00
|
|
|
def parse_segments(self, segments):
|
2017-02-23 07:19:12 +00:00
|
|
|
# VTOC stored in groups of 4 bytes starting at 0x38
|
|
|
|
# in bits, the sector used data is stored by track:
|
|
|
|
#
|
|
|
|
# FEDCBA98 76543210 xxxxxxxx xxxxxxxx
|
|
|
|
#
|
|
|
|
# where the x values are ignored (should be zeros). Track 0 info is
|
|
|
|
# found starting at 0x38, track 1 is found at 0x3c, etc.
|
|
|
|
#
|
|
|
|
# Want to convert this to an array that is a list of bits by
|
|
|
|
# track/sector number, i.e.:
|
|
|
|
#
|
|
|
|
# t0s0 t0s1 t0s2 t0s3 t0s4 t0s5 t0s6 t0s7 ... t1s0 t1s1 ... etc
|
|
|
|
#
|
|
|
|
# Problem: the bits are stored backwards, so a straight unpackbits will
|
|
|
|
# produce:
|
|
|
|
#
|
|
|
|
# t0sf t0se t0sd ...
|
|
|
|
#
|
|
|
|
# i.e. each group of 16 bits needs to be reversed.
|
|
|
|
self.vtoc = segments[0].data
|
|
|
|
|
2017-02-23 20:04:14 +00:00
|
|
|
# create a view starting at 0x38 where out of every 4 bytes, the first
|
|
|
|
# two are used and the second 2 are skipped. Regular slicing doesn't
|
|
|
|
# work like this, so thanks to stackoverflow.com/questions/33801170,
|
|
|
|
# reshaping it to a 2d array with 4 elements in each row, doing a slice
|
|
|
|
# *there* to skip the last 2 entries in each row, then flattening it
|
|
|
|
# gives us what we need.
|
|
|
|
usedbytes = self.vtoc[0x38:].reshape((-1, 4))[:,:2].flatten()
|
2017-02-23 07:19:12 +00:00
|
|
|
|
2017-02-23 20:04:14 +00:00
|
|
|
# The bits here are still ordered backwards for each track, e.g. F E D
|
|
|
|
# C B A 9 8 7 6 5 4 3 2 1 0
|
2017-02-23 07:19:12 +00:00
|
|
|
bits = np.unpackbits(usedbytes)
|
|
|
|
|
2017-02-23 20:04:14 +00:00
|
|
|
# so we need to reorder them using numpy's indexing before stuffing
|
|
|
|
# them into the sector map
|
|
|
|
self.sector_map[0:self.max_sectors] = bits[self.vtoc_bit_reorder_index]
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("vtoc before:\n%s" % str(self)) # expensive debugging call
|
2017-02-22 15:19:52 +00:00
|
|
|
|
|
|
|
def calc_bitmap(self):
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("vtoc after:\n%s" % str(self)) # expensive debugging call
|
2017-02-23 07:19:12 +00:00
|
|
|
|
2017-02-23 20:04:14 +00:00
|
|
|
# reverse the process from above, so swap the order of every 16 bits,
|
|
|
|
# turn them into bytes, then stuff them back into the vtoc. The bit
|
|
|
|
# reorder list is commutative, so we don't need another order here.
|
|
|
|
packed = np.packbits(self.sector_map[self.vtoc_bit_reorder_index])
|
|
|
|
vtoc = self.vtoc[0x38:].reshape((-1, 4))
|
|
|
|
packed = packed.reshape((-1, 2))
|
|
|
|
vtoc[:,:2] = packed[:,:]
|
|
|
|
|
2017-02-23 07:19:12 +00:00
|
|
|
# FIXME
|
2017-02-23 20:04:14 +00:00
|
|
|
self.vtoc[0x38:] = vtoc.flatten()
|
2017-02-23 22:23:29 +00:00
|
|
|
s = WriteableSector(self.sector_size, self.vtoc)
|
2017-02-23 20:04:14 +00:00
|
|
|
s.sector_num = 17 * 16
|
2017-02-22 15:19:52 +00:00
|
|
|
self.sectors.append(s)
|
|
|
|
|
|
|
|
|
|
|
|
class Dos33Directory(Directory):
|
|
|
|
@property
|
|
|
|
def dirent_class(self):
|
|
|
|
return Dos33Dirent
|
|
|
|
|
|
|
|
def get_dirent_sector(self):
|
2017-02-23 22:23:29 +00:00
|
|
|
s = self.sector_class(self.sector_size)
|
2017-02-22 15:19:52 +00:00
|
|
|
data = np.zeros([0x0b], dtype=np.uint8)
|
|
|
|
s.add_data(data)
|
|
|
|
return s
|
|
|
|
|
|
|
|
def encode_empty(self):
|
|
|
|
return np.zeros([Dos33Dirent.format.itemsize], dtype=np.uint8)
|
|
|
|
|
|
|
|
def encode_dirent(self, dirent):
|
|
|
|
data = dirent.encode_dirent()
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("encoded dirent: %s" % data)
|
2017-02-22 15:19:52 +00:00
|
|
|
return data
|
|
|
|
|
|
|
|
def set_sector_numbers(self, image):
|
2017-02-24 03:51:22 +00:00
|
|
|
current_sector = -1
|
2017-02-22 15:19:52 +00:00
|
|
|
for sector in self.sectors:
|
2017-02-24 03:51:22 +00:00
|
|
|
current_sector, next_sector = image.get_directory_sector_links(current_sector)
|
|
|
|
sector.sector_num = current_sector
|
|
|
|
t, s = image.header.track_from_sector(next_sector)
|
2017-02-22 15:19:52 +00:00
|
|
|
sector.data[1] = t
|
|
|
|
sector.data[2] = s
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("directory sector %d -> next = %d" % (sector.sector_num, next_sector))
|
2017-02-24 03:51:22 +00:00
|
|
|
current_sector = next_sector
|
2017-02-22 15:19:52 +00:00
|
|
|
|
|
|
|
|
2017-02-26 22:06:29 +00:00
|
|
|
class Dos33Dirent(Dirent):
|
2016-07-21 00:37:38 +00:00
|
|
|
format = np.dtype([
|
|
|
|
('track', 'u1'),
|
|
|
|
('sector', 'u1'),
|
2017-02-22 20:11:56 +00:00
|
|
|
('flag', 'u1'),
|
2016-07-21 00:37:38 +00:00
|
|
|
('name','S30'),
|
|
|
|
('num_sectors','<u2'),
|
|
|
|
])
|
|
|
|
|
|
|
|
def __init__(self, image, file_num=0, bytes=None):
|
2017-02-26 22:06:29 +00:00
|
|
|
Dirent.__init__(self, file_num)
|
2017-02-25 07:36:08 +00:00
|
|
|
self._file_type = 0
|
2016-07-21 00:37:38 +00:00
|
|
|
self.locked = False
|
|
|
|
self.deleted = False
|
|
|
|
self.track = 0
|
|
|
|
self.sector = 0
|
|
|
|
self.filename = ""
|
|
|
|
self.num_sectors = 0
|
|
|
|
self.is_sane = True
|
|
|
|
self.current_sector_index = 0
|
|
|
|
self.current_read = 0
|
|
|
|
self.sectors_seen = None
|
|
|
|
self.sector_map = None
|
|
|
|
self.parse_raw_dirent(image, bytes)
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-07-21 00:37:38 +00:00
|
|
|
def __str__(self):
|
2017-02-25 07:36:08 +00:00
|
|
|
return "File #%-2d (%s) %03d %-30s %03d %03d" % (self.file_num, self.summary, self.num_sectors, self.filename, self.track, self.sector)
|
2017-02-26 22:06:29 +00:00
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return self.__class__ == other.__class__ and self.filename == other.filename and self.track == other.track and self.sector == other.sector and self.num_sectors == other.num_sectors
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2017-02-27 05:43:20 +00:00
|
|
|
type_to_text = {
|
2017-02-25 07:36:08 +00:00
|
|
|
0x0: "T", # text
|
|
|
|
0x1: "I", # integer basic
|
|
|
|
0x2: "A", # applesoft basic
|
|
|
|
0x4: "B", # binary
|
|
|
|
0x8: "S", # ?
|
|
|
|
0x10: "R", # relocatable object module
|
|
|
|
0x20: "a", # ?
|
|
|
|
0x40: "b", # ?
|
2016-07-21 00:37:38 +00:00
|
|
|
}
|
2017-02-27 05:43:20 +00:00
|
|
|
text_to_type = {v: k for k, v in type_to_text.iteritems()}
|
2016-07-21 00:37:38 +00:00
|
|
|
|
2017-02-25 07:36:08 +00:00
|
|
|
@property
|
|
|
|
def file_type(self):
|
|
|
|
"""User friendly version of file type, not the binary number"""
|
2017-02-27 05:43:20 +00:00
|
|
|
return self.type_to_text.get(self._file_type, "?")
|
2017-02-25 07:36:08 +00:00
|
|
|
|
|
|
|
@property
|
2016-07-21 00:37:38 +00:00
|
|
|
def summary(self):
|
2017-02-24 06:54:01 +00:00
|
|
|
if self.deleted:
|
|
|
|
locked = "D"
|
|
|
|
file_type = " "
|
|
|
|
else:
|
|
|
|
locked = "*" if self.locked else " "
|
2017-02-25 07:36:08 +00:00
|
|
|
file_type = self.file_type
|
2017-02-22 20:11:56 +00:00
|
|
|
flag = "%s%s" % (locked, file_type)
|
|
|
|
return flag
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-07-21 00:37:38 +00:00
|
|
|
@property
|
|
|
|
def verbose_info(self):
|
|
|
|
return self.summary
|
2017-02-22 20:11:56 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def in_use(self):
|
|
|
|
return not self.deleted
|
|
|
|
|
|
|
|
@property
|
|
|
|
def flag(self):
|
2017-02-25 07:36:08 +00:00
|
|
|
return 0xff if self.deleted else self._file_type | (0x80 * int(self.locked))
|
2017-02-27 01:08:55 +00:00
|
|
|
|
|
|
|
def extra_metadata(self, image):
|
|
|
|
lines = []
|
|
|
|
ts = self.get_track_sector_list(image)
|
|
|
|
lines.append("track/sector list at: " + str(ts))
|
|
|
|
lines.append("sector map: " + str(self.sector_map))
|
|
|
|
return "\n".join(lines)
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-07-21 00:37:38 +00:00
|
|
|
def parse_raw_dirent(self, image, bytes):
|
|
|
|
if bytes is None:
|
|
|
|
return
|
|
|
|
values = bytes.view(dtype=self.format)[0]
|
|
|
|
self.track = values[0]
|
|
|
|
if self.track == 0xff:
|
|
|
|
self.deleted = True
|
2017-02-22 20:11:56 +00:00
|
|
|
self.track = bytes[0x20]
|
2016-07-21 00:37:38 +00:00
|
|
|
else:
|
|
|
|
self.deleted = False
|
|
|
|
self.sector = values[1]
|
2017-02-25 07:36:08 +00:00
|
|
|
self._file_type = values[2] & 0x7f
|
2017-02-22 20:11:56 +00:00
|
|
|
self.locked = values[2] & 0x80
|
2016-07-21 00:37:38 +00:00
|
|
|
self.filename = (bytes[3:0x20] - 0x80).tostring().rstrip()
|
|
|
|
self.num_sectors = int(values[4])
|
|
|
|
self.is_sane = self.sanity_check(image)
|
2017-02-22 15:19:52 +00:00
|
|
|
|
|
|
|
def encode_dirent(self):
|
|
|
|
data = np.zeros([self.format.itemsize], dtype=np.uint8)
|
|
|
|
values = data.view(dtype=self.format)[0]
|
2017-02-24 06:54:01 +00:00
|
|
|
values[0] = 0xff if self.deleted else self.track
|
2017-02-22 20:11:56 +00:00
|
|
|
values[1] = self.sector
|
|
|
|
values[2] = self.flag
|
2017-02-22 22:01:54 +00:00
|
|
|
n = min(len(self.filename), 30)
|
|
|
|
data[3:3+n] = np.fromstring(self.filename, dtype=np.uint8) | 0x80
|
|
|
|
data[3+n:] = ord(' ') | 0x80
|
2017-02-24 06:54:01 +00:00
|
|
|
if self.deleted:
|
|
|
|
data[0x20] = self.track
|
2017-02-22 20:11:56 +00:00
|
|
|
values[4] = self.num_sectors
|
2017-02-22 15:19:52 +00:00
|
|
|
return data
|
|
|
|
|
|
|
|
def mark_deleted(self):
|
|
|
|
self.deleted = True
|
|
|
|
|
|
|
|
def update_sector_info(self, sector_list):
|
|
|
|
self.num_sectors = sector_list.num_sectors
|
|
|
|
self.starting_sector = sector_list.first_sector
|
2017-02-24 03:13:48 +00:00
|
|
|
|
|
|
|
def add_metadata_sectors(self, vtoc, sector_list, header):
|
|
|
|
"""Add track/sector list
|
|
|
|
"""
|
2017-02-26 21:34:07 +00:00
|
|
|
tslist = BaseSectorList(header)
|
2017-02-24 03:13:48 +00:00
|
|
|
for start in range(0, len(sector_list), header.ts_pairs):
|
|
|
|
end = min(start + header.ts_pairs, len(sector_list))
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("ts: %d-%d" % (start, end))
|
2017-02-24 03:13:48 +00:00
|
|
|
s = Dos33TSSector(header, sector_list, start, end)
|
|
|
|
s.ts_start, s.ts_end = start, end
|
|
|
|
tslist.append(s)
|
|
|
|
self.num_tslists = len(tslist)
|
|
|
|
vtoc.assign_sector_numbers(self, tslist)
|
|
|
|
sector_list.extend(tslist)
|
|
|
|
self.track, self.sector = header.track_from_sector(tslist[0].sector_num)
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("track/sector lists:\n%s" % str(tslist))
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-07-21 00:37:38 +00:00
|
|
|
def sanity_check(self, image):
|
|
|
|
if self.deleted:
|
|
|
|
return True
|
|
|
|
if self.track == 0:
|
|
|
|
return False
|
|
|
|
s = image.header.sector_from_track(self.track, self.sector)
|
|
|
|
if not image.header.sector_is_valid(s):
|
|
|
|
return False
|
|
|
|
if self.num_sectors < 0 or self.num_sectors > image.header.max_sectors:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def get_track_sector_list(self, image):
|
2017-02-26 21:34:07 +00:00
|
|
|
tslist = BaseSectorList(image.header)
|
2017-02-22 15:19:52 +00:00
|
|
|
sector_num = image.header.sector_from_track(self.track, self.sector)
|
2017-02-24 06:16:18 +00:00
|
|
|
sector_map = []
|
2017-02-22 15:19:52 +00:00
|
|
|
while sector_num > 0:
|
2017-02-24 06:16:18 +00:00
|
|
|
image.assert_valid_sector(sector_num)
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("reading track/sector list at %d for %s" % (sector_num, self))
|
2017-02-24 06:16:18 +00:00
|
|
|
data, _ = image.get_sectors(sector_num)
|
|
|
|
sector = Dos33TSSector(image.header, data=data)
|
2017-02-27 01:08:55 +00:00
|
|
|
sector.sector_num = sector_num
|
2017-02-24 06:16:18 +00:00
|
|
|
sector_map.extend(sector.get_tslist())
|
|
|
|
tslist.append(sector)
|
|
|
|
sector_num = sector.next_sector_num
|
2017-02-24 06:54:01 +00:00
|
|
|
self.sector_map = sector_map[0:self.num_sectors - len(tslist)]
|
2017-02-24 06:16:18 +00:00
|
|
|
self.track_sector_list = tslist
|
2017-02-27 01:08:55 +00:00
|
|
|
return tslist
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2017-02-24 06:16:18 +00:00
|
|
|
def get_sectors_in_vtoc(self, image):
|
|
|
|
self.get_track_sector_list(image)
|
2017-02-27 01:08:55 +00:00
|
|
|
sectors = BaseSectorList(image.header)
|
2017-02-24 06:16:18 +00:00
|
|
|
sectors.extend(self.track_sector_list)
|
|
|
|
for sector_num in self.sector_map:
|
2017-02-23 22:23:29 +00:00
|
|
|
sector = WriteableSector(image.header.sector_size, None, sector_num)
|
2017-02-24 06:16:18 +00:00
|
|
|
sectors.append(sector)
|
|
|
|
return sectors
|
2016-07-21 01:12:07 +00:00
|
|
|
|
2016-07-21 00:37:38 +00:00
|
|
|
def start_read(self, image):
|
|
|
|
if not self.is_sane:
|
|
|
|
raise InvalidDirent("Invalid directory entry '%s'" % str(self))
|
|
|
|
self.get_track_sector_list(image)
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("start_read: %s, t/s list: %s" % (str(self), str(self.sector_map)))
|
2016-07-21 00:37:38 +00:00
|
|
|
self.current_sector_index = 0
|
|
|
|
self.current_read = self.num_sectors
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-07-21 00:37:38 +00:00
|
|
|
def read_sector(self, image):
|
2016-07-29 02:37:40 +00:00
|
|
|
try:
|
|
|
|
sector = self.sector_map[self.current_sector_index]
|
|
|
|
except IndexError:
|
2017-02-24 06:54:01 +00:00
|
|
|
sector = -1 # force ByteNotInFile166 error at next read
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("read_sector: index %d=%d in %s" % (self.current_sector_index,sector, str(self)))
|
2016-07-21 00:37:38 +00:00
|
|
|
last = (self.current_sector_index == len(self.sector_map) - 1)
|
|
|
|
raw, pos, size = image.get_raw_bytes(sector)
|
|
|
|
bytes, num_data_bytes = self.process_raw_sector(image, raw)
|
|
|
|
return bytes, last, pos, num_data_bytes
|
|
|
|
|
|
|
|
def process_raw_sector(self, image, raw):
|
|
|
|
self.current_sector_index += 1
|
|
|
|
num_bytes = len(raw)
|
|
|
|
return raw[0:num_bytes], num_bytes
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-07-21 00:37:38 +00:00
|
|
|
def get_filename(self):
|
|
|
|
return self.filename
|
|
|
|
|
2017-02-22 15:19:52 +00:00
|
|
|
def set_values(self, filename, filetype, index):
|
|
|
|
self.filename = "%-30s" % filename[0:30]
|
2017-02-27 05:43:20 +00:00
|
|
|
self._file_type = self.text_to_type.get(filetype, 0x04)
|
2017-02-22 15:19:52 +00:00
|
|
|
self.locked = False
|
2017-02-22 20:11:56 +00:00
|
|
|
self.deleted = False
|
2017-02-22 15:19:52 +00:00
|
|
|
|
2017-02-25 07:36:08 +00:00
|
|
|
def get_binary_start_address(self, image):
|
|
|
|
self.start_read(image)
|
|
|
|
data, _, _, _ = self.read_sector(image)
|
|
|
|
addr = int(data[0]) + 256 * int(data[1])
|
|
|
|
return addr
|
|
|
|
|
2016-07-21 00:37:38 +00:00
|
|
|
|
2017-02-23 21:02:56 +00:00
|
|
|
class Dos33Header(BaseHeader):
|
2016-07-20 15:13:44 +00:00
|
|
|
file_format = "DOS 3.3"
|
|
|
|
|
|
|
|
def __init__(self):
|
2017-02-23 21:02:56 +00:00
|
|
|
BaseHeader.__init__(self, 256)
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-07-20 15:13:44 +00:00
|
|
|
def __str__(self):
|
2017-05-07 02:53:15 +00:00
|
|
|
return "%s Disk Image (size=%d (%dx%dB)" % (self.file_format, self.image_size, self.max_sectors, self.sector_size)
|
2016-07-20 15:13:44 +00:00
|
|
|
|
|
|
|
def check_size(self, size):
|
2016-07-20 17:03:29 +00:00
|
|
|
if size != 143360:
|
2016-07-20 15:13:44 +00:00
|
|
|
raise InvalidDiskImage("Incorrect size for DOS 3.3 image")
|
2017-02-23 23:59:42 +00:00
|
|
|
self.image_size = size
|
2017-02-23 21:02:56 +00:00
|
|
|
self.first_vtoc = 17 * 16
|
|
|
|
self.num_vtoc = 1
|
|
|
|
self.first_directory = self.first_vtoc + 15
|
|
|
|
self.num_directory = 8
|
2017-02-23 07:19:12 +00:00
|
|
|
self.tracks_per_disk = 35
|
|
|
|
self.sectors_per_track = 16
|
|
|
|
self.max_sectors = self.tracks_per_disk * self.sectors_per_track
|
|
|
|
|
2016-07-20 15:13:44 +00:00
|
|
|
|
|
|
|
class Dos33DiskImage(DiskImageBase):
|
|
|
|
def __init__(self, rawdata, filename=""):
|
|
|
|
DiskImageBase.__init__(self, rawdata, filename)
|
2017-02-24 19:57:51 +00:00
|
|
|
self.default_filetype = "B"
|
2016-07-20 15:13:44 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return str(self.header)
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-07-20 15:13:44 +00:00
|
|
|
def read_header(self):
|
|
|
|
self.header = Dos33Header()
|
2017-02-22 03:25:47 +00:00
|
|
|
|
2017-02-22 20:11:56 +00:00
|
|
|
@property
|
|
|
|
def vtoc_class(self):
|
|
|
|
return Dos33VTOC
|
|
|
|
|
|
|
|
@property
|
|
|
|
def directory_class(self):
|
|
|
|
return Dos33Directory
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2017-02-22 20:12:52 +00:00
|
|
|
@property
|
|
|
|
def raw_sector_class(self):
|
|
|
|
return RawTrackSectorSegment
|
|
|
|
|
2016-07-20 17:03:29 +00:00
|
|
|
def get_boot_sector_info(self):
|
|
|
|
# based on logic from a2server
|
|
|
|
data, style = self.get_sectors(0)
|
|
|
|
magic = data[0:4]
|
|
|
|
if (magic == [1, 56, 176, 3]).all():
|
|
|
|
raise InvalidDiskImage("ProDOS format found; not DOS 3.3 image")
|
|
|
|
swap_order = False
|
2017-02-23 21:02:56 +00:00
|
|
|
data, style = self.get_sectors(self.header.first_vtoc)
|
2016-07-20 17:03:29 +00:00
|
|
|
if data[3] == 3:
|
|
|
|
if data[1] < 35 and data[2] < 16:
|
2017-02-23 21:02:56 +00:00
|
|
|
data, style = self.get_sectors(self.header.first_vtoc + 14)
|
2016-07-20 17:03:29 +00:00
|
|
|
if data[2] != 13:
|
2017-05-07 19:23:51 +00:00
|
|
|
log.warning("DOS 3.3 byte swap needed!")
|
2016-07-20 17:03:29 +00:00
|
|
|
swap_order = True
|
|
|
|
else:
|
|
|
|
raise InvalidDiskImage("Invalid VTOC location for DOS 3.3")
|
|
|
|
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-07-21 00:37:38 +00:00
|
|
|
vtoc_type = np.dtype([
|
|
|
|
('unused1', 'S1'),
|
|
|
|
('cat_track','u1'),
|
|
|
|
('cat_sector','u1'),
|
|
|
|
('dos_release', 'u1'),
|
|
|
|
('unused2', 'S2'),
|
|
|
|
('vol_num', 'u1'),
|
|
|
|
('unused3', 'S32'),
|
|
|
|
('max_pairs', 'u1'),
|
|
|
|
('unused4', 'S8'),
|
|
|
|
('last_track', 'u1'),
|
|
|
|
('track_dir', 'i1'),
|
|
|
|
('unused5', 'S2'),
|
|
|
|
('num_tracks', 'u1'),
|
|
|
|
('sectors_per_track', 'u1'),
|
2017-02-23 22:23:29 +00:00
|
|
|
('sector_size', 'u2'),
|
2016-07-21 00:37:38 +00:00
|
|
|
])
|
|
|
|
|
|
|
|
def get_vtoc(self):
|
2017-02-23 21:02:56 +00:00
|
|
|
data, style = self.get_sectors(self.header.first_vtoc)
|
2017-02-23 07:19:12 +00:00
|
|
|
values = data[0:self.vtoc_type.itemsize].view(dtype=self.vtoc_type)[0]
|
2017-02-23 21:53:32 +00:00
|
|
|
self.header.first_directory = self.header.sector_from_track(values['cat_track'], values['cat_sector'])
|
2017-02-23 22:23:29 +00:00
|
|
|
self.header.sector_size = int(values['sector_size'])
|
2017-02-23 21:53:32 +00:00
|
|
|
self.header.max_sectors = int(values['num_tracks']) * int(values['sectors_per_track'])
|
|
|
|
self.header.ts_pairs = int(values['max_pairs'])
|
|
|
|
self.header.dos_release = values['dos_release']
|
|
|
|
self.header.last_track_num = values['last_track']
|
|
|
|
self.header.track_alloc_dir = values['track_dir']
|
2017-02-23 23:59:42 +00:00
|
|
|
self.assert_valid_sector(self.header.first_directory)
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2017-02-22 20:11:56 +00:00
|
|
|
def get_directory(self, directory=None):
|
2017-02-23 23:59:42 +00:00
|
|
|
sector = self.header.first_directory
|
2016-07-21 00:37:38 +00:00
|
|
|
num = 0
|
|
|
|
files = []
|
|
|
|
while sector > 0:
|
|
|
|
self.assert_valid_sector(sector)
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("reading catalog sector: %d" % sector)
|
2016-07-21 00:37:38 +00:00
|
|
|
values, style = self.get_sectors(sector)
|
|
|
|
sector = self.header.sector_from_track(values[1], values[2])
|
|
|
|
i = 0xb
|
|
|
|
while i < 256:
|
|
|
|
dirent = Dos33Dirent(self, num, values[i:i+0x23])
|
2017-02-22 20:11:56 +00:00
|
|
|
if dirent.flag == 0:
|
2016-07-21 00:37:38 +00:00
|
|
|
break
|
2017-02-22 20:11:56 +00:00
|
|
|
if not dirent.is_sane:
|
2017-02-24 18:39:47 +00:00
|
|
|
log.warning("Illegally formatted directory entry %s" % dirent)
|
2017-02-22 20:11:56 +00:00
|
|
|
self.all_sane = False
|
2017-02-24 06:54:01 +00:00
|
|
|
elif not dirent.deleted:
|
2017-02-22 20:11:56 +00:00
|
|
|
files.append(dirent)
|
|
|
|
if directory is not None:
|
|
|
|
directory.set(num, dirent)
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("valid directory entry %s" % dirent)
|
2016-07-21 00:37:38 +00:00
|
|
|
i += 0x23
|
|
|
|
num += 1
|
|
|
|
self.files = files
|
2016-07-20 17:03:29 +00:00
|
|
|
|
2016-07-20 15:13:44 +00:00
|
|
|
def get_boot_segments(self):
|
2016-07-21 01:55:05 +00:00
|
|
|
segments = []
|
|
|
|
s = self.get_sector_slice(0, 0)
|
|
|
|
r = self.rawdata[s]
|
|
|
|
boot1 = ObjSegment(r, 0, 0, 0x800, name="Boot 1")
|
|
|
|
s = self.get_sector_slice(1, 9)
|
|
|
|
r = self.rawdata[s]
|
|
|
|
boot2 = ObjSegment(r, 0, 0, 0x3700, name="Boot 2")
|
|
|
|
s = self.get_sector_slice(0x0a, 0x0b)
|
|
|
|
r = self.rawdata[s]
|
|
|
|
relocator = ObjSegment(r, 0, 0, 0x1b00, name="Relocator")
|
|
|
|
s = self.get_sector_slice(0x0c, 0x0c + 25)
|
|
|
|
r = self.rawdata[s]
|
|
|
|
boot3 = ObjSegment(r, 0, 0, 0x1d00, name="Boot 3")
|
|
|
|
return [boot1, boot2, relocator, boot3]
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-07-21 00:37:38 +00:00
|
|
|
def get_vtoc_segments(self):
|
|
|
|
r = self.rawdata
|
|
|
|
segments = []
|
|
|
|
addr = 0
|
2017-02-23 21:02:56 +00:00
|
|
|
start, count = self.get_contiguous_sectors(self.header.first_vtoc, 1)
|
|
|
|
segment = RawTrackSectorSegment(r[start:start+count], self.header.first_vtoc, 1, count, 0, 0, self.header.sector_size, name="VTOC")
|
2017-03-20 21:29:08 +00:00
|
|
|
segment.style[:] = get_style_bits(data=True)
|
2017-03-21 05:50:39 +00:00
|
|
|
segment.set_comment_at(0x00, "unused")
|
|
|
|
segment.set_comment_at(0x01, "Track number of next catalog sector")
|
|
|
|
segment.set_comment_at(0x02, "Sector number of next catalog sector")
|
|
|
|
segment.set_comment_at(0x03, "Release number of DOS used to format")
|
|
|
|
segment.set_comment_at(0x04, "unused")
|
|
|
|
segment.set_comment_at(0x06, "Volume number")
|
|
|
|
segment.set_comment_at(0x07, "unused")
|
|
|
|
segment.set_comment_at(0x27, "Number of track/sector pairs per t/s list sector")
|
|
|
|
segment.set_comment_at(0x28, "unused")
|
|
|
|
segment.set_comment_at(0x30, "Last track that sectors allocated")
|
|
|
|
segment.set_comment_at(0x31, "Track allocation direction")
|
|
|
|
segment.set_comment_at(0x32, "unused")
|
|
|
|
segment.set_comment_at(0x34, "Tracks per disk")
|
|
|
|
segment.set_comment_at(0x35, "Sectors per track")
|
|
|
|
segment.set_comment_at(0x36, "Bytes per sector")
|
|
|
|
index = 0x38
|
|
|
|
for track in range(35):
|
|
|
|
segment.set_comment_at(index, "Free sectors in track %d" % track)
|
|
|
|
index += 4
|
2016-07-21 00:37:38 +00:00
|
|
|
segments.append(segment)
|
|
|
|
return segments
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-07-21 00:37:38 +00:00
|
|
|
def get_directory_segments(self):
|
|
|
|
byte_order = []
|
|
|
|
r = self.rawdata
|
|
|
|
segments = []
|
2017-02-23 23:59:42 +00:00
|
|
|
sector = self.header.first_directory
|
2016-07-21 00:37:38 +00:00
|
|
|
while sector > 0:
|
|
|
|
self.assert_valid_sector(sector)
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("loading directory segment from catalog sector %d" % sector)
|
2016-07-21 00:37:38 +00:00
|
|
|
raw, pos, size = self.get_raw_bytes(sector)
|
|
|
|
byte_order.extend(range(pos, pos + size))
|
|
|
|
sector = self.header.sector_from_track(raw[1], raw[2])
|
|
|
|
raw = self.rawdata.get_indexed(byte_order)
|
|
|
|
segment = DefaultSegment(raw, name="Catalog")
|
2017-03-20 21:29:08 +00:00
|
|
|
segment.style[:] = get_style_bits(data=True)
|
2017-03-21 05:50:39 +00:00
|
|
|
index = 0
|
|
|
|
filenum = 0
|
|
|
|
while index < len(segment):
|
|
|
|
segment.set_comment_at(index + 0x00, "unused")
|
|
|
|
segment.set_comment_at(index + 0x01, "Track number of next catalog sector")
|
|
|
|
segment.set_comment_at(index + 0x02, "Sector number of next catalog sector")
|
|
|
|
segment.set_comment_at(index + 0x03, "unused")
|
|
|
|
index += 0x0b
|
|
|
|
for i in range(7):
|
|
|
|
segment.set_comment_at(index + 0x00, "FILE #%d: Track number of next catalog sector" % filenum)
|
|
|
|
segment.set_comment_at(index + 0x01, "FILE #%d: Sector number of next catalog sector" % filenum)
|
|
|
|
segment.set_comment_at(index + 0x02, "FILE #%d: File type" % filenum)
|
|
|
|
segment.set_comment_at(index + 0x03, "FILE #%d: Filename" % filenum)
|
|
|
|
segment.set_comment_at(index + 0x21, "FILE #%d: Number of sectors in file" % filenum)
|
|
|
|
index += 0x23
|
|
|
|
filenum += 1
|
2016-07-21 00:37:38 +00:00
|
|
|
segments.append(segment)
|
|
|
|
return segments
|
2017-02-22 15:19:52 +00:00
|
|
|
|
2017-02-24 03:51:22 +00:00
|
|
|
def get_directory_sector_links(self, sector_num):
|
|
|
|
if sector_num == -1:
|
|
|
|
sector_num = self.header.first_directory
|
|
|
|
self.assert_valid_sector(sector_num)
|
|
|
|
raw, _, _ = self.get_raw_bytes(sector_num)
|
2017-02-22 15:19:52 +00:00
|
|
|
next_sector = self.header.sector_from_track(raw[1], raw[2])
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("checking catalog sector %d, next catalog sector: %d" % (sector_num, next_sector))
|
2017-02-22 15:19:52 +00:00
|
|
|
if next_sector == 0:
|
2017-02-22 20:11:56 +00:00
|
|
|
raise NoSpaceInDirectory("No space left in catalog")
|
2017-02-24 03:51:22 +00:00
|
|
|
return sector_num, next_sector
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-07-21 00:37:38 +00:00
|
|
|
def get_file_segment(self, dirent):
|
|
|
|
byte_order = []
|
|
|
|
dirent.start_read(self)
|
|
|
|
while True:
|
|
|
|
bytes, last, pos, size = dirent.read_sector(self)
|
|
|
|
byte_order.extend(range(pos, pos + size))
|
|
|
|
if last:
|
|
|
|
break
|
|
|
|
if len(byte_order) > 0:
|
2017-02-25 07:36:08 +00:00
|
|
|
name = "%s %03d %s" % (dirent.summary, dirent.num_sectors, dirent.filename)
|
2017-02-24 16:42:04 +00:00
|
|
|
verbose_name = "%s (%d sectors, first@%d) %s" % (dirent.filename, dirent.num_sectors, dirent.sector_map[0], dirent.verbose_info)
|
2016-07-21 00:37:38 +00:00
|
|
|
raw = self.rawdata.get_indexed(byte_order)
|
2017-02-25 07:36:08 +00:00
|
|
|
if dirent.file_type == "B":
|
|
|
|
addr = dirent.get_binary_start_address(self) - 4 # factor in 4 byte header
|
|
|
|
else:
|
|
|
|
addr = 0
|
2017-03-21 06:25:52 +00:00
|
|
|
segment = ObjSegment(raw, 0, 0, start_addr=addr, name=name, verbose_name=verbose_name)
|
2017-02-25 07:36:08 +00:00
|
|
|
if addr > 0:
|
|
|
|
style = segment.get_style_bits(data=True)
|
|
|
|
segment.style[0:4] = style
|
2016-07-21 00:37:38 +00:00
|
|
|
else:
|
2017-02-24 16:42:04 +00:00
|
|
|
segment = EmptySegment(self.rawdata, name=dirent.filename)
|
2016-07-21 00:37:38 +00:00
|
|
|
return segment
|
2016-07-20 17:03:29 +00:00
|
|
|
|
2017-05-02 18:17:32 +00:00
|
|
|
def create_executable_file_image(self, segments, run_addr=None):
|
|
|
|
# Apple 2 executables get executed at the first address loaded. If the
|
|
|
|
# run_addr is not the first byte of the combined data, have to create a
|
|
|
|
# new 3-byte segment with a "JMP run_addr" to go at the beginning
|
2017-02-26 20:14:23 +00:00
|
|
|
origin = 100000000
|
|
|
|
last = -1
|
2017-05-02 18:17:32 +00:00
|
|
|
|
2017-02-26 20:14:23 +00:00
|
|
|
for s in segments:
|
|
|
|
origin = min(origin, s.start_addr)
|
|
|
|
last = max(last, s.start_addr + len(s))
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("contiguous bytes needed: %04x - %04x" % (origin, last))
|
2017-05-02 18:17:32 +00:00
|
|
|
if run_addr and run_addr != origin:
|
2017-05-03 18:43:14 +00:00
|
|
|
# check if run_addr points to some location that has data
|
|
|
|
found = False
|
|
|
|
for s in segments:
|
|
|
|
if run_addr >= s.start_addr and run_addr < s.start_addr + len(s):
|
|
|
|
found = True
|
|
|
|
break
|
|
|
|
if not found:
|
|
|
|
raise InvalidBinaryFile("Run address points outside data segments")
|
2017-05-02 18:17:32 +00:00
|
|
|
origin -= 3
|
|
|
|
hi, lo = divmod(run_addr, 256)
|
|
|
|
raw = SegmentData([0x4c, lo, hi])
|
|
|
|
all_segments = [DefaultSegment(raw, start_addr=origin)]
|
|
|
|
all_segments.extend(segments)
|
|
|
|
else:
|
|
|
|
all_segments = segments
|
2017-02-26 20:14:23 +00:00
|
|
|
size = last - origin
|
|
|
|
image = np.zeros([size + 4], dtype=np.uint8)
|
|
|
|
words = image[0:4].view(dtype="<u2") # always little endian
|
|
|
|
words[0] = origin
|
|
|
|
words[1] = size
|
2017-05-02 18:17:32 +00:00
|
|
|
for s in all_segments:
|
2017-02-26 20:14:23 +00:00
|
|
|
index = s.start_addr - origin + 4
|
2017-05-07 20:28:15 +00:00
|
|
|
print("setting data for $%04x - $%04x at index $%04x" % (s.start_addr, s.start_addr + len(s), index))
|
2017-02-26 20:14:23 +00:00
|
|
|
image[index:index + len(s)] = s.data
|
|
|
|
return image, 'B'
|
|
|
|
|
|
|
|
|
2017-05-06 23:49:42 +00:00
|
|
|
class Dos33BinFile(object):
|
|
|
|
"""Parse a binary chunk into segments according to the DOS 3.3 binary
|
|
|
|
dump format
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, rawdata):
|
|
|
|
self.rawdata = rawdata
|
|
|
|
self.size = len(rawdata)
|
|
|
|
self.segments = []
|
|
|
|
self.files = []
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return "\n".join(str(s) for s in self.segments) + "\n"
|
|
|
|
|
|
|
|
def strict_check(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def relaxed_check(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def parse_segments(self):
|
|
|
|
r = self.rawdata
|
|
|
|
b = r.get_data()
|
|
|
|
s = r.get_style()
|
|
|
|
pos = 0
|
|
|
|
style_pos = 0
|
|
|
|
first = True
|
2017-05-07 19:23:51 +00:00
|
|
|
if _xd: log.debug("Initial parsing: size=%d" % self.size)
|
2017-05-06 23:49:42 +00:00
|
|
|
if len(b[pos:pos + 4]) == 4:
|
|
|
|
start, count = b[pos:pos + 4].view(dtype='<u2')
|
|
|
|
s[pos:pos + 4] = get_style_bits(data=True)
|
|
|
|
data = b[pos + 4:pos + 4 + count]
|
|
|
|
if len(data) == count:
|
|
|
|
name = "BSAVE data" % start
|
|
|
|
else:
|
|
|
|
name = "Incomplete data: expected %04x, loaded %04x" % (count, len(data))
|
|
|
|
self.segments.append(ObjSegment(r[pos + 4:pos + 4 + count], pos, pos + 4, start, start + len(data), name))
|
|
|
|
|
|
|
|
else:
|
|
|
|
self.segments.append(ObjSegment(r[pos:pos + 4], 0, 0, 0, len(b[pos:pos + 4]), "Short Segment Header"))
|
|
|
|
|
|
|
|
|
2016-07-20 17:03:29 +00:00
|
|
|
class ProdosHeader(Dos33Header):
|
|
|
|
file_format = "ProDOS"
|
|
|
|
|
2017-02-25 06:20:35 +00:00
|
|
|
def __str__(self):
|
|
|
|
return "%s Disk Image (size=%d) THIS FORMAT IS NOT SUPPORTED YET!" % (self.file_format, self.image_size)
|
|
|
|
|
|
|
|
|
|
|
|
class ProdosDiskImage(DiskImageBase):
|
|
|
|
def __str__(self):
|
|
|
|
return str(self.header)
|
2016-07-20 17:03:29 +00:00
|
|
|
|
|
|
|
def read_header(self):
|
|
|
|
self.header = ProdosHeader()
|
|
|
|
|
|
|
|
def get_boot_sector_info(self):
|
|
|
|
# based on logic from a2server
|
|
|
|
data, style = self.get_sectors(0)
|
|
|
|
magic = data[0:4]
|
|
|
|
swap_order = False
|
|
|
|
if (magic == [1, 56, 176, 3]).all():
|
|
|
|
data, style = self.get_sectors(1)
|
|
|
|
prodos = data[3:9].tostring()
|
|
|
|
if prodos == "PRODOS":
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
data, style = self.get_sectors(14)
|
|
|
|
prodos = data[3:9].tostring()
|
|
|
|
if prodos == "PRODOS":
|
|
|
|
swap_order = True
|
|
|
|
else:
|
2017-03-07 18:43:46 +00:00
|
|
|
# FIXME: this doesn't seem to be the only way to identify a
|
|
|
|
# PRODOS disk. I have example images where PRODOS occurs at
|
|
|
|
# 0x21 - 0x27 in t0s14 and 0x11 - 0x16 in t0s01. Using 3 -
|
|
|
|
# 9 as magic bytes was from the cppo script from
|
|
|
|
# https://github.com/RasppleII/a2server but it seems that
|
|
|
|
# more magic bytes might be acceptable?
|
|
|
|
|
|
|
|
#raise InvalidDiskImage("No ProDOS header info found")
|
|
|
|
pass
|
|
|
|
raise UnsupportedDiskImage("ProDOS format found but not supported")
|