diff --git a/atrcopy/__init__.py b/atrcopy/__init__.py index ac254a8..d6b9993 100644 --- a/atrcopy/__init__.py +++ b/atrcopy/__init__.py @@ -13,7 +13,8 @@ from diskimages import AtrHeader, BootDiskImage, add_atr_header from kboot import KBootImage, add_xexboot_header from segments import SegmentData, SegmentSaver, DefaultSegment, EmptySegment, ObjSegment, RawSectorsSegment, user_bit_mask, match_bit_mask, comment_bit_mask, data_bit_mask, selected_bit_mask, diff_bit_mask, not_user_bit_mask, interleave_segments from spartados import SpartaDosDiskImage -from parsers import SegmentParser, DefaultSegmentParser, guess_parser_for, known_segment_parsers, mime_parse_order +from cartridge import A8CartHeader +from parsers import SegmentParser, DefaultSegmentParser, guess_parser_for_mime, guess_parser_for_system, known_segment_parsers, mime_parse_order from utils import to_numpy @@ -77,7 +78,7 @@ def run(): for mime in mime_parse_order: if options.verbose: print "Trying MIME type %s" % mime - parser = guess_parser_for(mime, rawdata) + parser = guess_parser_for_mime(mime, rawdata) if parser is None: continue if options.verbose: diff --git a/atrcopy/cartridge.py b/atrcopy/cartridge.py new file mode 100644 index 0000000..99284ee --- /dev/null +++ b/atrcopy/cartridge.py @@ -0,0 +1,248 @@ +from collections import defaultdict + +import numpy as np + +from errors import * +from segments import SegmentData, EmptySegment, ObjSegment +from diskimages import DiskImageBase +from utils import to_numpy + +# From atari800 source +known_cart_types = [ +# (note: all size units in KB) +# atari800 index number +# name +# total size +# static size +# static offset +# static address +# banked size +# banked offset (for bank zero) +# banked address + (0, "", 0,), + (1, "Standard 8K", 8, 8, 0, 0xa000), + (2, "Standard 16K", 16, 16, 0, 0x8000), + (3, "OSS 16K", 16, 4, 12, 0xb000, 4, 0, 0xa000), + (4, "5200 32K", 32, 32, 0, 0x4000), + (5, "DB 32K", 32,), + (6, "5200 EE 16K", 16,), + (7, "5200 BBSB 40K", 40,), + (8, "WILL 64K", 64,), + (9, "EXP_64K", 64,), + (10, "DIAMOND_64K", 64,), + (11, "SDX_64K", 64,), + (12, "XEGS_32K", 32, 8, 24, 0xa000, 8, 0, 0x8000), + (13, "XEGS_64_07K", 64, 8, 56, 0xa000, 8, 0, 0x8000), + (14, "XEGS_128K", 128, 8, 120, 0xa000, 8, 0, 0x8000), + (15, "OSS_M091_16K", 16,), + (16, "5200_NS_16K", 16,), + (17, "ATRAX_128K", 128,), + (18, "BBSB_40K", 40,), + (19, "5200_8K", 8, 8, 0, 0x8000), + (20, "5200_4K", 4, 4, 0, 0x8000), + (21, "RIGHT_8K", 8,), + (22, "WILL_32K", 32,), + (23, "XEGS_256K", 256, 8, 248, 0xa000, 8, 0, 0x8000), + (24, "XEGS_512K", 512, 8, 504, 0xa000, 8, 0, 0x8000), + (25, "XEGS_1024K", 1024, 8, 1016, 0xa000, 8, 0, 0x8000 ), + (26, "MEGA_16K", 16,), + (27, "MEGA_32K", 32,), + (28, "MEGA_64K", 64,), + (29, "MEGA_128K", 128,), + (30, "MEGA_256K", 256,), + (31, "MEGA_512K", 512,), + (32, "MEGA_1024K", 1024,), + (33, "SWXEGS_32K", 32, 8, 24, 0xa000, 8, 0, 0x8000), + (34, "SWXEGS_64K", 64, 8, 56, 0xa000, 8, 0, 0x8000), + (35, "SWXEGS_128K", 128, 8, 120, 0xa000, 8, 0, 0x8000), + (36, "SWXEGS_256K", 256, 8, 248, 0xa000, 8, 0, 0x8000), + (37, "SWXEGS_512K", 512, 8, 504, 0xa000, 8, 0, 0x8000), + (38, "SWXEGS_1024K", 1024, 8, 1016, 0xa000, 8, 0, 0x8000 ), + (39, "PHOENIX_8K", 8,), + (40, "BLIZZARD_16K", 16, 16, 0, 0x8000), + (41, "ATMAX_128K", 128,), + (42, "ATMAX_1024K", 1024,), + (43, "SDX_128K", 128,), + (44, "OSS_8K", 8,), + (45, "OSS_043M_16K", 16, 4, 12, 0xb000, 4, 0, 0xa000), + (46, "BLIZZARD_4K", 4,), + (47, "AST_32K", 32,), + (48, "ATRAX_SDX_64K", 64,), + (49, "ATRAX_SDX_128K", 128,), + (50, "TURBOSOFT_64K", 64,), + (51, "TURBOSOFT_128K", 128,), + (52, "ULTRACART_32K", 32,), + (53, "LOW_BANK_8K", 8, 8, 0, 0x8000), + (54, "SIC_128K", 128,), + (55, "SIC_256K", 256,), + (56, "SIC_512K", 512,), + (57, "Standard 2K", 2, 2, 0, 0xb800), + (58, "Standard 4K", 4, 4, 0, 0xb000), + (59, "Right 4K", 4, 4, 4, 0, 0x9000), + (60, "TURBO_HIT_32K", 32,), + (61, "MEGA_2048K", 2048,), + (62, "THECART_128MK", 128*1024,), + (63, "MEGA_4096K", 4096,), + (64, "MEGA_2048K", 2048,), + (65, "THECART_32MK", 32*1024,), + (66, "THECART_64MK", 64*1024,), + (67, "XEGS_64_8FK", 64), +] + +def get_known_carts(): + grouped = defaultdict(list) + for i, c in enumerate(known_cart_types[1:], 1): + size = c[2] + grouped[size].append((i, c)) + return grouped + + +class A8CartHeader(object): + # Atari Cart format described by https://sourceforge.net/p/atari800/source/ci/master/tree/DOC/cart.txt NOTE: Big endian! + format = np.dtype([ + ('magic', '|S4'), + ('format', '>u4'), + ('checksum', '>u4'), + ('unused','>u4') + ]) + file_format = "Cart" + + def __init__(self, bytes=None, create=False): + self.image_size = 0 + self.cart_type = -1 + self.cart_name = "" + self.cart_size = 0 + self.crc = 0 + self.unused = 0 + self.header_offset = 0 + self.num_banks = 0 + self.banks = [] + self.bank_size = 0 + self.bank_origin = 0 + self.main_size = 0 + self.main_offset = 0 + self.main_origin = 0 + self.possible_types = set() + if create: + self.header_offset = 16 + self.check_size(0) + if bytes is None: + return + + if len(bytes) == 16: + values = bytes.view(dtype=self.format)[0] + if values[0] != 'CART': + raise InvalidCartHeader + self.cart_type = int(values[1]) + self.crc = int(values[2]) + self.header_offset = 16 + self.set_type(self.cart_type) + else: + raise InvalidCartHeader + + def __str__(self): + return "%s Cartridge (atari800 type=%d size=%d, %d banks, crc=%d)" % (self.cart_name, self.cart_type, self.cart_size, self.bank_size, self.crc) + + def __len__(self): + return self.header_offset + + def to_array(self): + raw = np.zeros([16], dtype=np.uint8) + values = raw.view(dtype=self.format)[0] + values[0] = 'CART' + values[1] = self.cart_type + values[2] = self.crc + values[3] = 0 + return raw + + def set_type(self, cart_type): + print "TYPE", cart_type + self.cart_type = cart_type + c = known_cart_types[cart_type] + self.cart_name = c[1] + self.cart_size = c[2] + if len(c) >= 6: + self.main_size, self.main_offset, self.main_origin = c[3:6] + if len(c) >= 9: + self.banks = [] + self.bank_size, offset, self.bank_origin = c[6:9] + s = self.cart_size - self.main_size + while s > 0: + self.banks.append(offset) + offset += self.bank_size + s -= self.bank_size + + def check_size(self, size): + self.possible_types = set() + k, r = divmod(size, 1024) + if r == 0 or r == 16: + for i, t in enumerate(known_cart_types): + valid_size = t[0] + if k == valid_size: + self.possible_types.add(i) + + +class AtariCartImage(DiskImageBase): + def __init__(self, rawdata, cart_type, filename=""): + self.cart_type = cart_type + DiskImageBase.__init__(self, rawdata, filename) + + def __str__(self): + return str(self.header) + + def read_header(self): + bytes = self.bytes[0:16] + try: + self.header = A8CartHeader(bytes) + except InvalidCartHeader: + self.header = A8CartHeader() + self.header.set_type(self.cart_type) + if self.header.cart_type != self.cart_type: + raise InvalidDiskImage("Cart type doesn't match type defined in header") + + def check_size(self): + if self.header is None: + return + k, rem = divmod((len(self) - len(self.header)), 1024) + c = known_cart_types[self.cart_type] + print "checking %s:" % c[1], k, rem, c[2] + if rem > 0: + raise InvalidDiskImage("Cart not multiple of 1K") + if k != c[2]: + raise InvalidDiskImage("Image size %d doesn't match cart type %d size %d" % (k, self.cart_type, c[2])) + + def parse_segments(self): + r = self.rawdata + i = self.header.header_offset + if i > 0: + self.segments.append(ObjSegment(r[0:i], 0, 0, 0, i, name="Cart Header")) + self.segments.extend(self.get_main_segment()) + self.segments.extend(self.get_banked_segments()) + + def get_main_segment(self): + r = self.rawdata + start = self.header.header_offset + self.header.main_offset * 1024 + end = start + (self.header.main_size * 1024) + s = ObjSegment(r[start:end], 0, 0, self.header.main_origin, name="Main Bank") + return [s] + + def get_banked_segments(self): + print "HI", self.header.banks + segments = [] + r = self.rawdata + for i, offset in enumerate(self.header.banks): + start = self.header.header_offset + offset * 1024 + end = start + (self.header.bank_size * 1024) + s = ObjSegment(r[start:end], 0, 0, self.header.bank_origin, name="Bank #%d" % (i + 1)) + segments.append(s) + return segments + + +def add_cart_header(bytes): + header = A8CartHeader(create=True) + header.check_size(len(bytes)) + hlen = len(header) + data = np.empty([hlen + len(bytes)], dtype=np.uint8) + data[0:hlen] = header.to_array() + data[hlen:] = bytes + return data diff --git a/atrcopy/diskimages.py b/atrcopy/diskimages.py index e91a999..6df9f4e 100644 --- a/atrcopy/diskimages.py +++ b/atrcopy/diskimages.py @@ -132,6 +132,9 @@ class DiskImageBase(object): self.segments = [] self.all_sane = True self.setup() + + def __len__(self): + return len(self.rawdata) def set_filename(self, filename): if "." in filename: diff --git a/atrcopy/parsers.py b/atrcopy/parsers.py index b5dc34f..838bc6d 100644 --- a/atrcopy/parsers.py +++ b/atrcopy/parsers.py @@ -5,6 +5,7 @@ from diskimages import BootDiskImage from kboot import KBootImage from ataridos import AtariDosDiskImage, AtariDosFile from spartados import SpartaDosDiskImage +from cartridge import AtariCartImage, get_known_carts from errors import * @@ -20,12 +21,15 @@ class SegmentParser(object): def parse(self, r): self.segments.append(DefaultSegment(r, 0)) try: - self.image = self.image_type(r) + self.image = self.get_image(r) self.image.parse_segments() except AtrError: raise InvalidSegmentParser self.segments.extend(self.image.segments) + def get_image(self, r): + return self.image_type(r) + class DefaultSegmentParser(SegmentParser): menu_name = "Raw Data" @@ -59,7 +63,17 @@ class XexSegmentParser(SegmentParser): image_type = AtariDosFile -def guess_parser_for(mime, r): +class AtariCartSegmentParser(SegmentParser): + menu_name = "temp" + image_type = AtariCartImage + cart_index = 0 + cart_info = None + + def get_image(self, r): + return self.image_type(r, self.cart_index) + + +def guess_parser_for_mime(mime, r): parsers = mime_parsers[mime] found = None for parser in parsers: @@ -70,6 +84,14 @@ def guess_parser_for(mime, r): pass return found +def guess_parser_for_system(mime_base, r): + for mime in mime_parse_order: + if mime.startswith(mime_base): + p = guess_parser_for_mime(mime, r) + if p is not None: + return mime, p + return None, None + mime_parsers = { "application/vnd.atari8bit.atr": [ @@ -88,6 +110,21 @@ mime_parse_order = [ "application/vnd.atari8bit.xex", ] +grouped_carts = get_known_carts() +sizes = sorted(grouped_carts.keys()) +print sizes +for k in sizes: + if k >= 1024: + key = "application/vnd.atari8bit.%dmb_cart" % (k / 1024) + else: + key = "application/vnd.atari8bit.%dkb_cart" % k + mime_parse_order.append(key) + mime_parsers[key] = [] + for i, c in grouped_carts[k]: + kclass = type("AtariCartSegmentParser%d" % i, (AtariCartSegmentParser,), {'cart_index': i, 'cart_info': c, 'menu_name': "%s Cartridge" % c[1]}) + mime_parsers[key].append(kclass) + + known_segment_parsers = [DefaultSegmentParser] for mime in mime_parse_order: known_segment_parsers.extend(mime_parsers[mime]) diff --git a/atrcopy/segments.py b/atrcopy/segments.py index 7ba2cf8..5243a17 100644 --- a/atrcopy/segments.py +++ b/atrcopy/segments.py @@ -541,7 +541,7 @@ class EmptySegment(DefaultSegment): class ObjSegment(DefaultSegment): - def __init__(self, rawdata, metadata_start, data_start, start_addr, end_addr, name="", **kwargs): + def __init__(self, rawdata, metadata_start, data_start, start_addr, end_addr=0, name="", **kwargs): DefaultSegment.__init__(self, rawdata, start_addr, name, **kwargs) self.metadata_start = metadata_start self.data_start = data_start