commit 7fd983b8ae7de5129db452f5a46850cdd8a14e2e Author: Rob McMullen Date: Wed Jun 18 16:01:35 2014 -0700 Initial code to support reading atr and xfd images diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..9ba8956 --- /dev/null +++ b/README.rst @@ -0,0 +1,12 @@ +ATRCopy +======= + +Support Atari 8-bit emulator disk images in pyfilesystem + +References +========== + +* http://www.atariarchives.org/dere/chapt09.php +* http://atari.kensclassics.org/dos.htm +* http://www.crowcastle.net/preston/atari/ +* http://www.atarimax.com/jindroush.atari.org/afmtatr.html diff --git a/atrcopy.py b/atrcopy.py new file mode 100644 index 0000000..c3c9959 --- /dev/null +++ b/atrcopy.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +from atrutil import * + +if __name__ == "__main__": + import sys + + image = sys.argv[1] + print image + with open(image, "rb") as fh: + atr = AtrDiskImage(fh) + print atr + for filename in sys.argv[2:]: + print filename + bytes = atr.find_file(filename) + with open(filename, "wb") as fh: + fh.write(bytes) diff --git a/atrdump.py b/atrdump.py new file mode 100644 index 0000000..aba757c --- /dev/null +++ b/atrdump.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +from atrutil import * + +if __name__ == "__main__": + import sys + + for args in sys.argv: + print args + with open(args, "rb") as fh: + atr = AtrDiskImage(fh) + print atr + for dirent in atr.files: + bytes = atr.get_file(dirent) + filename = dirent.get_filename() + with open(filename, "wb") as fh: + fh.write(bytes) + diff --git a/atrutil.py b/atrutil.py new file mode 100644 index 0000000..b573377 --- /dev/null +++ b/atrutil.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python + +import struct +from cStringIO import StringIO + +class AtrError(RuntimeError): + pass + +class InvalidAtrHeader(AtrError): + pass + +class LastDirent(AtrError): + pass + +class FileNumberMismatchError164(AtrError): + pass + +class AtrHeader(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() + + def __str__(self): + locked = "*" if self.locked else "" + dos = "(mydos)" if self.mydos else "" + if self.in_use: + return "File #%-2d: %1s%-8s%-3s %03d %s" % (self.file_num, locked, self.filename, self.ext, self.num_sectors, dos) + return + + def process_raw_sector(self, bytes): + file_num = ord(bytes[-3]) >> 2 + if file_num != self.file_num: + raise FileNumberMismatchError164() + sector = ((ord(bytes[-3]) & 0x3) << 8) + ord(bytes[-2]) + num_bytes = ord(bytes[-1]) + return sector, bytes[0:num_bytes] + + def get_filename(self): + ext = ("." + self.ext) if self.ext else "" + return self.filename + ext + +class AtrFile(object): + pass + +class AtrDiskImage(object): + def __init__(self, fh): + self.fh = fh + self.header = None + self.files = [] + self.setup() + + def __str__(self): + lines = [] + lines.append("ATR Disk Image (%s) %d files" % (self.header, len(self.files))) + for dirent in self.files: + if dirent.in_use: + lines.append(str(dirent)) + return "\n".join(lines) + + def setup(self): + self.fh.seek(0, 2) + self.size = self.fh.tell() + + self.read_atr_header() + self.check_size() + self.get_directory() + + def read_atr_header(self): + self.fh.seek(0) + bytes = self.fh.read(16) + try: + self.header = AtrHeader(bytes) + except InvalidAtrHeader: + self.header = AtrHeader() + + def check_size(self): + if self.header.size_in_bytes == 0: + if self.size == 92160: + self.header.size_in_bytes = self.size + self.header.sector_size = 128 + elif self.size == 184320: + self.header.size_in_bytes = self.size + self.header.sector_size = 256 + self.initial_sector_size = self.header.sector_size + self.num_initial_sectors = 0 + + def get_pos(self, 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.header.sector_size + size = self.header.sector_size + pos += self.header.atr_header_offset + return pos, size + + def get_sectors(self, start, end): + """ Get contiguous sectors + + :param start: first sector number to read (note: numbering starts from 1) + :param end: last sector number to read + :returns: bytes + """ + output = StringIO() + pos, size = self.get_pos(start) + self.fh.seek(pos) + while start <= end: + bytes = self.fh.read(size) + output.write(bytes) + start += 1 + pos, size = self.get_pos(start) + return output.getvalue() + + def get_directory(self): + dir_bytes = self.get_sectors(361, 368) + i = 0 + num = 0 + files = [] + while i < len(dir_bytes): + dirent = AtrDirent(num, dir_bytes[i:i+16]) + if dirent.in_use: + files.append(dirent) + elif dirent.flag == 0: + break + i += 16 + num += 1 + self.files = files + + def get_file(self, dirent): + output = StringIO() + sector = dirent.starting_sector + while sector > 0: + pos, size = self.get_pos(sector) + self.fh.seek(pos) + raw = self.fh.read(size) + sector, bytes = dirent.process_raw_sector(raw) + output.write(bytes) + return output.getvalue() + + def find_file(self, filename): + for dirent in self.files: + if filename == dirent.get_filename(): + bytes = self.get_file(dirent) + return bytes + return "" + + +if __name__ == "__main__": + import sys + + for args in sys.argv: + print args + with open(args, "rb") as fh: + atr = AtrDiskImage(fh) + print atr