#!/usr/bin/env python __version__ = "1.1.1" import struct from cStringIO import StringIO class AtrError(RuntimeError): pass class InvalidAtrHeader(AtrError): pass class LastDirent(AtrError): pass class FileNumberMismatchError164(AtrError): pass class ByteNotInFile166(AtrError): pass class AtrHeader(object): format = " 0 and sector <= self.max_sectors def get_pos(self, sector): if not self.sector_is_valid(sector): raise ByteNotInFile166("Sector %d out of range" % sector) if sector <= self.num_initial_sectors: pos = self.num_initial_sectors * (sector - 1) size = self.initial_sector_size else: pos = self.num_initial_sectors * self.initial_sector_size + (sector - 1 - self.num_initial_sectors) * self.sector_size size = self.sector_size pos += self.atr_header_offset return pos, size class XfdHeader(AtrHeader): file_format = "XFD" def __str__(self): return "%s Disk Image (size=%d (%dx%db)" % (self.file_format, self.size_in_bytes, self.max_sectors, self.sector_size) class AtrDirent(object): format = " 0 self.dos_2 = (flag&0x02) > 0 self.mydos = (flag&0x04) > 0 self.is_dir = (flag&0x10) > 0 self.locked = (flag&0x20) > 0 self.in_use = (flag&0x40) > 0 self.deleted = (flag&0x80) > 0 self.num_sectors = values[1] self.starting_sector = values[2] self.filename = values[3].rstrip() self.ext = values[4].rstrip() self.current_sector = 0 self.is_sane = self.sanity_check(disk) def __str__(self): output = "o" if self.opened_output else "." dos2 = "2" if self.dos_2 else "." mydos = "m" if self.mydos else "." in_use = "u" if self.in_use else "." deleted = "d" if self.deleted else "." locked = "*" if self.locked else " " flags = "%s%s%s%s%s%s %03d" % (output, dos2, mydos, in_use, deleted, locked, self.starting_sector) if self.in_use: return "File #%-2d (%s) %-8s%-3s %03d" % (self.file_num, flags, self.filename, self.ext, self.num_sectors) return def sanity_check(self, disk): if not self.in_use: return True if not disk.header.sector_is_valid(self.starting_sector): return False if self.num_sectors < 0 or self.num_sectors > disk.header.max_sectors: return False return True def start_read(self): self.current_sector = self.starting_sector self.current_read = self.num_sectors def read_sector(self, disk): raw = disk.get_raw_bytes(self.current_sector) bytes = self.process_raw_sector(disk, raw) return (bytes, self.current_sector == 0) def process_raw_sector(self, disk, raw): file_num = ord(raw[-3]) >> 2 if file_num != self.file_num: raise FileNumberMismatchError164() self.current_sector = ((ord(raw[-3]) & 0x3) << 8) + ord(raw[-2]) num_bytes = ord(raw[-1]) return raw[0:num_bytes] def get_filename(self): ext = ("." + self.ext) if self.ext else "" return self.filename + ext class MydosDirent(AtrDirent): def process_raw_sector(self, disk, raw): self.current_read -= 1 if self.current_read == 0: self.current_sector = 0 else: self.current_sector += 1 if self.current_sector == disk.first_vtoc: self.current_sector = disk.first_data_after_vtoc return raw class InvalidBinaryFile(AtrError): pass class ObjSegment(object): def __init__(self, metadata_start, data_start, start_addr, end_addr, data, name="", error=None): self.name = name self.metadata_start = metadata_start self.data_start = data_start self.start_addr = start_addr self.end_addr = end_addr self.data = data self.error = error if name and not name.endswith(" "): name += " " self.name = name self.page_size = -1 def __str__(self): s = "%s%04x-%04x (%04x @ %04x)" % (self.name, self.start_addr, self.end_addr, len(self.data), self.data_start) if self.error: s += " " + self.error return s def __len__(self): return len(self.data) def __getitem__(self, val): return self.data[val] def label(self, index): return "%04x" % (index + self.start_addr) class RawSectorsSegment(ObjSegment): def __init__(self, first_sector, num_sectors, count, data, **kwargs): ObjSegment.__init__(self, 0, 0, 0, count, data, **kwargs) self.page_size = 128 self.first_sector = first_sector self.num_sectors = num_sectors def __str__(self): if self.num_sectors > 1: s = "%s (sectors %d-%d)" % (self.name, self.first_sector, self.first_sector + self.num_sectors - 1) else: s = "%s (sector %d)" % (self.name, self.first_sector) if self.error: s += " " + self.error return s def label(self, index): sector, byte = divmod(index, self.page_size) return "s%03d:%02x" % (sector + self.first_sector, byte) class AtariDosFile(object): """Parse a binary chunk into segments according to the Atari DOS object file format. Ref: http://www.atarimax.com/jindroush.atari.org/afmtexe.html """ def __init__(self, data): self.data = data self.size = len(data) self.segments = [] self.parse_segments() def __str__(self): return "\n".join(str(s) for s in self.segments) + "\n" def parse_segments(self): bytes = self.data pos = 0 first = True while pos < self.size: header, = struct.unpack(" 0: self.segments.append(ObjSegment(0, 0, 0, self.header.atr_header_offset, self.bytes[0:self.header.atr_header_offset], name="%s Header" % self.header.file_format)) self.segments.append(RawSectorsSegment(1, self.header.max_sectors, self.header.size_in_bytes, self.bytes[self.header.atr_header_offset:], name="Raw disk sectors")) self.segments.extend(self.get_boot_segments()) self.segments.extend(self.get_vtoc_segments()) self.segments.extend(self.get_directory_segments()) # for dirent in self.atr.files: # try: # bytes = self.get_file(dirent) # error = None # except atrcopy.FileNumberMismatchError164: # bytes = None # error = "Error 164" # except atrcopy.ByteNotInFile166: # bytes = None # error = "Invalid sector" # a = AtrFileSegment(dirent, bytes, error) # self.segments.append(AtrSegment(dirent)) def process(dirent, options): skip = False action = "copying to" filename = dirent.get_filename() outfilename = filename if options.no_sys: if dirent.ext == "SYS": skip = True action = "skipping system file" if not skip: if options.xex: outfilename = "%s%s.XEX" % (dirent.filename, dirent.ext) if options.lower: outfilename = outfilename.lower() if options.dry_run: action = "DRY_RUN: %s" % action skip = True if not skip: bytes = atr.get_file(dirent) with open(outfilename, "wb") as fh: fh.write(bytes) if options.extract: print "%s: %s %s" % (dirent, action, outfilename) else: print dirent if __name__ == "__main__": import sys import argparse parser = argparse.ArgumentParser(description="Extract images off ATR format disks") parser.add_argument("-v", "--verbose", default=0, action="count") parser.add_argument("-l", "--lower", action="store_true", default=False, help="convert filenames to lower case") parser.add_argument("--dry-run", action="store_true", default=False, help="don't extract, just show what would have been extracted") parser.add_argument("-n", "--no-sys", action="store_true", default=False, help="only extract things that look like games (no DOS or .SYS files)") parser.add_argument("-x", "--extract", action="store_true", default=False, help="extract files") parser.add_argument("--xex", action="store_true", default=False, help="add .xex extension") parser.add_argument("-f", "--force", action="store_true", default=False, help="force operation on disk images that have bad directory entries or look like boot disks") parser.add_argument("files", metavar="ATR", nargs="+", help="an ATR image file [or a list of them]") parser.add_argument("-s", "--segments", action="store_true", default=False, help="display segments") options, extra_args = parser.parse_known_args() for filename in options.files: with open(filename, "rb") as fh: data = fh.read() try: atr = AtrDiskImage(data) print "%s: %s" % (filename, atr) except: print "%s: Doesn't look like a supported disk image" % filename try: xex = AtariDosFile(data) print xex except InvalidBinaryFile: print "%s: Doesn't look like an XEX either" % filename continue if options.segments: atr.parse_segments() print "\n".join([str(a) for a in atr.segments]) elif atr.all_sane or options.force: for dirent in atr.files: try: process(dirent, options) except FileNumberMismatchError164: print "Error 164: %s" % str(dirent) except ByteNotInFile166: print "Invalid sector for: %s" % str(dirent)