diff --git a/atrcopy/__init__.py b/atrcopy/__init__.py index 5bda941..05c0f4d 100644 --- a/atrcopy/__init__.py +++ b/atrcopy/__init__.py @@ -32,7 +32,7 @@ def process(image, dirent, options): 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 @@ -45,6 +45,7 @@ def process(image, dirent, options): else: print dirent + def find_diskimage(filename): try: with open(filename, "rb") as fh: @@ -72,6 +73,7 @@ def find_diskimage(filename): parser.image.ext = "" return parser + def extract_files(image, files): for name in files: try: @@ -85,6 +87,7 @@ def extract_files(image, files): with open(dirent.filename, "wb") as fh: fh.write(data) + def save_file(image, name, filetype, data): try: dirent = image.find_dirent(name) @@ -114,6 +117,7 @@ def add_files(image, files): if changed: image.save() + def remove_files(image, files): changed = False for name in files: @@ -129,6 +133,7 @@ def remove_files(image, files): if changed: image.save() + def list_files(image, files): files = set(files) for dirent in image.files: @@ -137,6 +142,7 @@ def list_files(image, files): if options.metadata: print dirent.extra_metadata(image) + def assemble(image, source_files, data_files): if source_files: try: @@ -170,6 +176,7 @@ def assemble(image, source_files, data_files): if changed: image.save() + def shred_image(image, value=0): print "shredding: free sectors from %s filled with %d" % (image, value) if not options.dry_run: @@ -182,7 +189,7 @@ def run(): import argparse global options - + parser = argparse.ArgumentParser(description="Manipulate files on several types of 8-bit computer disk images") parser.add_argument("-v", "--verbose", default=0, action="count") parser.add_argument("--dry-run", action="store_true", default=False, help="don't perform operation, just show what would have happened") @@ -214,7 +221,7 @@ def run(): log.setLevel(logging.DEBUG) else: log.setLevel(logging.INFO) - + file_list = [] if options.add or options.extract or options.delete: image = options.files.pop() diff --git a/atrcopy/ataridos.py b/atrcopy/ataridos.py index 9bda15b..d248c30 100644 --- a/atrcopy/ataridos.py +++ b/atrcopy/ataridos.py @@ -91,7 +91,7 @@ class AtariDosDirent(Dirent): self.current_read = 0 self.sectors_seen = None self.parse_raw_dirent(image, bytes) - + def __str__(self): return "File #%-2d (%s) %03d %-8s%-3s %03d" % (self.file_num, self.summary, self.starting_sector, self.basename, self.ext, self.num_sectors) @@ -102,7 +102,7 @@ class AtariDosDirent(Dirent): def filename(self): ext = ("." + self.ext) if self.ext else "" return self.basename + ext - + @property def summary(self): output = "o" if self.opened_output else "." @@ -113,7 +113,7 @@ class AtariDosDirent(Dirent): locked = "*" if self.locked else " " flags = "%s%s%s%s%s%s" % (output, dos2, mydos, in_use, deleted, locked) return flags - + @property def verbose_info(self): flags = [] @@ -131,7 +131,7 @@ class AtariDosDirent(Dirent): def parse_raw_dirent(self, image, bytes): if bytes is None: return - values = bytes.view(dtype=self.format)[0] + values = bytes.view(dtype=self.format)[0] flag = values[0] self.flag = flag self.opened_output = (flag&0x01) > 0 @@ -170,7 +170,7 @@ class AtariDosDirent(Dirent): # no extra sectors are needed for an Atari DOS file; the links to the # next sector is contained in the sector. pass - + def sanity_check(self, image): if not self.in_use: return True @@ -179,7 +179,7 @@ class AtariDosDirent(Dirent): if self.num_sectors < 0 or self.num_sectors > image.header.max_sectors: return False return True - + def get_sectors_in_vtoc(self, image): sector_list = BaseSectorList(image.header) self.start_read(image) @@ -197,7 +197,7 @@ class AtariDosDirent(Dirent): self.current_sector = self.starting_sector self.current_read = self.num_sectors self.sectors_seen = set() - + def read_sector(self, image): raw, pos, size = image.get_raw_bytes(self.current_sector) bytes, num_data_bytes = self.process_raw_sector(image, raw) @@ -256,12 +256,13 @@ class AtariDosFile(object): Ref: http://www.atarimax.com/jindroush.atari.org/afmtexe.html """ + 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" @@ -270,7 +271,7 @@ class AtariDosFile(object): def relaxed_check(self): pass - + def parse_segments(self): r = self.rawdata b = r.get_data() @@ -325,7 +326,7 @@ class AtrHeader(BaseHeader): ('btFlags','u1'), ]) file_format = "ATR" - + def __init__(self, bytes=None, sector_size=128, initial_sectors=3, create=False): BaseHeader.__init__(self, sector_size, initial_sectors, 360, 1) if create: @@ -333,7 +334,7 @@ class AtrHeader(BaseHeader): self.check_size(0) if bytes is None: return - + if len(bytes) == 16: values = bytes.view(dtype=self.format)[0] if values[0] != 0x296: @@ -346,10 +347,10 @@ class AtrHeader(BaseHeader): self.header_offset = 16 else: raise InvalidAtrHeader - + def __str__(self): return "%s Disk Image (size=%d (%dx%db), crc=%d flags=%d unused=%d)" % (self.file_format, self.image_size, self.max_sectors, self.sector_size, self.crc, self.flags, self.unused) - + def encode(self, raw): values = raw.view(dtype=self.format)[0] values[0] = 0x296 @@ -390,7 +391,7 @@ class AtrHeader(BaseHeader): self.payload_bytes = self.sector_size - 3 initial_bytes = self.initial_sector_size * self.num_initial_sectors self.max_sectors = ((self.image_size - initial_bytes) / self.sector_size) + self.num_initial_sectors - + def get_pos(self, sector): if not self.sector_is_valid(sector): raise ByteNotInFile166("Sector %d out of range" % sector) @@ -406,13 +407,13 @@ class AtrHeader(BaseHeader): class XfdHeader(AtrHeader): file_format = "XFD" - + def __str__(self): return "%s Disk Image (size=%d (%dx%db)" % (self.file_format, self.image_size, self.max_sectors, self.sector_size) - + def __len__(self): return 0 - + def to_array(self): raw = np.zeros([0], dtype=np.uint8) return raw @@ -446,7 +447,7 @@ class AtariDosDiskImage(DiskImageBase): def __str__(self): return "%s Atari DOS Format: %d usable sectors (%d free), %d files" % (self.header, self.total_sectors, self.unused_sectors, len(self.files)) - + @classmethod def new_header(cls, diskimage, format="ATR"): if format.lower() == "atr": @@ -455,7 +456,7 @@ class AtariDosDiskImage(DiskImageBase): else: raise RuntimeError("Unknown header type %s" % format) return header - + def as_new_format(self, format="ATR"): """ Create a new disk image in the specified format """ @@ -465,13 +466,13 @@ class AtariDosDiskImage(DiskImageBase): newraw = SegmentData(data) image = self.__class__(newraw) return image - + vtoc_type = np.dtype([ ('code', 'u1'), ('total',' self.calc_vtoc_code(): raise InvalidDiskImage("Invalid number of VTOC sectors: %d" % num) - + self.total_sectors = values[1] self.unused_sectors = values[2] if self.header.image_size == 133120: @@ -518,7 +519,7 @@ class AtariDosDiskImage(DiskImageBase): data, style = self.get_sectors(self.vtoc2) extra_free = data[122:124].view(dtype='u4') ]) file_format = "Cart" - + def __init__(self, bytes=None, create=False): self.image_size = 0 self.cart_type = -1 @@ -140,7 +142,7 @@ class A8CartHeader(object): self.check_size(0) if bytes is None: return - + if len(bytes) == 16: values = bytes.view(dtype=self.format)[0] if values[0] != 'CART': @@ -151,13 +153,13 @@ class A8CartHeader(object): 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] @@ -201,7 +203,7 @@ class AtariCartImage(DiskImageBase): def __str__(self): return str(self.header) - + def read_header(self): bytes = self.bytes[0:16] try: @@ -219,7 +221,7 @@ class AtariCartImage(DiskImageBase): # force the header to be the specified cart type self.header = A8CartHeader() self.header.set_type(self.cart_type) - + def check_size(self): if self.header is None: return @@ -230,7 +232,7 @@ class AtariCartImage(DiskImageBase): 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 diff --git a/atrcopy/diskimages.py b/atrcopy/diskimages.py index 00fce4d..2b0d4ac 100644 --- a/atrcopy/diskimages.py +++ b/atrcopy/diskimages.py @@ -31,10 +31,10 @@ class BaseHeader(object): self.extra_vtoc = [] self.first_directory = 0 self.num_directory = 0 - + def __len__(self): return self.header_offset - + def to_array(self): header_bytes = np.zeros([self.header_offset], dtype=np.uint8) self.encode(header_bytes) @@ -55,7 +55,7 @@ class BaseHeader(object): pos, size = self.get_pos(i) yield i, pos, size i += 1 - + def get_pos(self, sector): """Get index (into the raw data of the disk image) of start of sector @@ -119,13 +119,13 @@ class DiskImageBase(object): @property def directory_class(self): return Directory - + def set_filename(self, filename): if "." in filename: self.filename, self.ext = filename.rsplit(".", 1) else: self.filename, self.ext = filename, "" - + def dir(self): lines = [] lines.append(str(self)) @@ -156,16 +156,16 @@ class DiskImageBase(object): format. """ pass - + @classmethod def new_header(cls, diskimage, format="ATR"): raise NotImplementedError - + def as_new_format(self, format="ATR"): """ Create a new disk image in the specified format """ raise NotImplementedError - + def save(self, filename=""): if not filename: filename = self.filename @@ -176,35 +176,35 @@ class DiskImageBase(object): bytes = self.bytes[:] with open(filename, "wb") as fh: bytes.tofile(fh) - + def assert_valid_sector(self, sector): if not self.header.sector_is_valid(sector): raise ByteNotInFile166("Sector %d out of range" % sector) - + def check_sane(self): if not self.all_sane: raise InvalidDiskImage("Invalid directory entries; may be boot disk") - + def read_header(self): return BaseHeader() - + def check_size(self): pass - + def get_boot_sector_info(self): pass - + def get_vtoc(self): """Get information from VTOC and populate the VTOC object""" pass - + def get_directory(self, directory=None): pass - + def get_raw_bytes(self, sector): pos, size = self.header.get_pos(sector) return self.bytes[pos:pos + size], pos, size - + def get_sector_slice(self, start, end=None): """ Get contiguous sectors @@ -220,7 +220,7 @@ class DiskImageBase(object): _, more = self.header.get_pos(start) size += more return slice(pos, pos + size) - + def get_sectors(self, start, end=None): """ Get contiguous sectors @@ -230,7 +230,7 @@ class DiskImageBase(object): """ s = self.get_sector_slice(start, end) return self.bytes[s], self.style[s] - + def get_contiguous_sectors(self, sector, num): start = 0 count = 0 @@ -240,7 +240,7 @@ class DiskImageBase(object): start = pos count += size return start, count - + def parse_segments(self): r = self.rawdata i = self.header.header_offset @@ -251,16 +251,16 @@ class DiskImageBase(object): self.segments.extend(self.get_vtoc_segments()) self.segments.extend(self.get_directory_segments()) self.segments.extend(self.get_file_segments()) - + def get_boot_segments(self): return [] - + def get_vtoc_segments(self): return [] def get_directory_segments(self): return [] - + def find_dirent(self, filename): # check if we've been passed a dirent instead of a filename if hasattr(filename, "filename"): @@ -269,18 +269,18 @@ class DiskImageBase(object): if filename == dirent.filename: return dirent raise FileNotFound("%s not found on disk" % filename) - + def find_file(self, filename): dirent = self.find_dirent(filename) return self.get_file(dirent) - + def get_file(self, dirent): segment = self.get_file_segment(dirent) return segment.tostring() - + def get_file_segment(self, dirent): pass - + def get_file_segments(self): segments = [] for dirent in self.files: diff --git a/atrcopy/dos33.py b/atrcopy/dos33.py index 783d0ec..01d2d17 100644 --- a/atrcopy/dos33.py +++ b/atrcopy/dos33.py @@ -168,13 +168,13 @@ class Dos33Dirent(Dirent): self.sectors_seen = None self.sector_map = None self.parse_raw_dirent(image, bytes) - + def __str__(self): return "File #%-2d (%s) %03d %-30s %03d %03d" % (self.file_num, self.summary, self.num_sectors, self.filename, self.track, self.sector) 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 - + type_to_text = { 0x0: "T", # text 0x1: "I", # integer basic @@ -202,7 +202,7 @@ class Dos33Dirent(Dirent): file_type = self.file_type flag = "%s%s" % (locked, file_type) return flag - + @property def verbose_info(self): return self.summary @@ -221,7 +221,7 @@ class Dos33Dirent(Dirent): lines.append("track/sector list at: " + str(ts)) lines.append("sector map: " + str(self.sector_map)) return "\n".join(lines) - + def parse_raw_dirent(self, image, bytes): if bytes is None: return @@ -275,7 +275,7 @@ class Dos33Dirent(Dirent): sector_list.extend(tslist) self.track, self.sector = header.track_from_sector(tslist[0].sector_num) log.debug("track/sector lists:\n%s" % str(tslist)) - + def sanity_check(self, image): if self.deleted: return True @@ -304,7 +304,7 @@ class Dos33Dirent(Dirent): self.sector_map = sector_map[0:self.num_sectors - len(tslist)] self.track_sector_list = tslist return tslist - + def get_sectors_in_vtoc(self, image): self.get_track_sector_list(image) sectors = BaseSectorList(image.header) @@ -321,7 +321,7 @@ class Dos33Dirent(Dirent): log.debug("start_read: %s, t/s list: %s" % (str(self), str(self.sector_map))) self.current_sector_index = 0 self.current_read = self.num_sectors - + def read_sector(self, image): try: sector = self.sector_map[self.current_sector_index] @@ -337,7 +337,7 @@ class Dos33Dirent(Dirent): self.current_sector_index += 1 num_bytes = len(raw) return raw[0:num_bytes], num_bytes - + def get_filename(self): return self.filename @@ -359,7 +359,7 @@ class Dos33Header(BaseHeader): def __init__(self): BaseHeader.__init__(self, 256) - + def __str__(self): return "%s Disk Image (size=%d (%dx%db)" % (self.file_format, self.image_size, self.max_sectors, self.sector_size) @@ -383,7 +383,7 @@ class Dos33DiskImage(DiskImageBase): def __str__(self): return str(self.header) - + def read_header(self): self.header = Dos33Header() @@ -394,7 +394,7 @@ class Dos33DiskImage(DiskImageBase): @property def directory_class(self): return Dos33Directory - + @property def raw_sector_class(self): return RawTrackSectorSegment @@ -416,7 +416,7 @@ class Dos33DiskImage(DiskImageBase): raise InvalidDiskImage("Invalid VTOC location for DOS 3.3") log.debug("DOS 3.3 byte swap: %s" % swap_order) - + vtoc_type = np.dtype([ ('unused1', 'S1'), ('cat_track','u1'), @@ -446,7 +446,7 @@ class Dos33DiskImage(DiskImageBase): self.header.last_track_num = values['last_track'] self.header.track_alloc_dir = values['track_dir'] self.assert_valid_sector(self.header.first_directory) - + def get_directory(self, directory=None): sector = self.header.first_directory num = 0 @@ -488,7 +488,7 @@ class Dos33DiskImage(DiskImageBase): r = self.rawdata[s] boot3 = ObjSegment(r, 0, 0, 0x1d00, name="Boot 3") return [boot1, boot2, relocator, boot3] - + def get_vtoc_segments(self): r = self.rawdata segments = [] @@ -517,7 +517,7 @@ class Dos33DiskImage(DiskImageBase): index += 4 segments.append(segment) return segments - + def get_directory_segments(self): byte_order = [] r = self.rawdata @@ -561,7 +561,7 @@ class Dos33DiskImage(DiskImageBase): if next_sector == 0: raise NoSpaceInDirectory("No space left in catalog") return sector_num, next_sector - + def get_file_segment(self, dirent): byte_order = [] dirent.start_read(self) @@ -605,10 +605,6 @@ class Dos33DiskImage(DiskImageBase): return image, 'B' - - - - class ProdosHeader(Dos33Header): file_format = "ProDOS" diff --git a/atrcopy/errors.py b/atrcopy/errors.py index 72f4b00..f4ccfa5 100644 --- a/atrcopy/errors.py +++ b/atrcopy/errors.py @@ -1,12 +1,15 @@ class AtrError(RuntimeError): pass + class InvalidAtrHeader(AtrError): pass + class InvalidCartHeader(AtrError): pass + class InvalidDiskImage(AtrError): """ Disk image is not recognized by a parser. @@ -15,6 +18,7 @@ class InvalidDiskImage(AtrError): """ pass + class UnsupportedDiskImage(AtrError): """ Disk image is recognized by a parser but it isn't supported yet. @@ -22,32 +26,42 @@ class UnsupportedDiskImage(AtrError): """ pass + class InvalidDirent(AtrError): pass + class LastDirent(AtrError): pass + class InvalidFile(AtrError): pass + class FileNumberMismatchError164(InvalidFile): pass + class ByteNotInFile166(InvalidFile): pass + class InvalidBinaryFile(InvalidFile): pass + class InvalidSegmentParser(AtrError): pass + class NoSpaceInDirectory(AtrError): pass + class NotEnoughSpaceOnDisk(AtrError): pass + class FileNotFound(AtrError): pass diff --git a/atrcopy/kboot.py b/atrcopy/kboot.py index c33d1a7..311b9c0 100644 --- a/atrcopy/kboot.py +++ b/atrcopy/kboot.py @@ -25,7 +25,7 @@ class KBootDirent(AtariDosDirent): self.exe_size = count self.exe_start = start self.num_sectors = count / 128 + 1 - + def parse_raw_dirent(self, image, bytes): pass @@ -37,14 +37,14 @@ class KBootDirent(AtariDosDirent): class KBootImage(AtariDosDiskImage): def __str__(self): return "%s KBoot Format: %d byte executable" % (self.header, self.files[0].exe_size) - + def check_sane(self): if not self.all_sane: raise InvalidDiskImage("Doesn't seem to be KBoot header") def get_vtoc(self): pass - + def get_directory(self): dirent = KBootDirent(self) if not dirent.is_sane: @@ -57,8 +57,10 @@ class KBootImage(AtariDosDiskImage): raw = self.rawdata[start:end] return XexSegment(raw, 0, 0, start, end, name="KBoot Executable") + xexboot_header = '\x00\x03\x00\x07\r\x07L\r\x07\x1c[\x00\x00\xa0\x00\x8c\t\x03\x8c\x04\x03\x8cD\x02\x8c\xe2\x02\x8c\xe3\x02\xc8\x84\t\x8c\x01\x03\xce\x06\x03\xa91\x8d\x00\x03\xa9R\x8d\x02\x03\xa9\x80\x8d\x08\x03\xa9\x01\x8d\x05\x03\xa9\xe3\x8d0\x02\x8d\x02\xd4\xa9\x07\x8d1\x02\x8d\x03\xd4\xa9\x00\xaa\x8d\x0b\x03\xa9\x04\x8d\n\x03 \xbc\x07\xca \xa5\x07\x85C \xa5\x07\x85D%C\xc9\xff\xf0\xf0 \xa5\x07\x85E \xa5\x07\x85F \xa5\x07\x91C\xe6C\xd0\x02\xe6D\xa5E\xc5C\xa5F\xe5D\xb0\xeb\xad\xe2\x02\r\xe3\x02\xf0\xc9\x86\x19 \xa2\x07\xa6\x19\xa0\x00\x8c\xe2\x02\x8c\xe3\x02\xf0\xb8l\xe2\x02\xad\t\x07\xd0\x0b\xad\n\x07\xd0\x03l\xe0\x02\xce\n\x07\xce\t\x07\xe0\x80\x90"\xa9@\x8d\x03\x03 Y\xe4\x10\x06\xce\x01\x07\xd0\xf1\x00\xee\n\x03\xd0\x03\xee\x0b\x03\xad\n\x03\x8d\x19\xd0\xa0\x00\xa2\x00\xbd\x00\x01\xe8`pppppF\xf8\x07p\x07ppp\x06p\x06p\x06A\xe3\x07\x00\x00\x00\x00\x00,/!$).\'\x0e\x0e\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00&2/-' + def insert_string(data, offset, string, color): s = np.fromstring(string.upper(), dtype=np.uint8) - 32 # convert to internal s = s | color @@ -66,6 +68,7 @@ def insert_string(data, offset, string, color): tx = offset + (20 - count)/ 2 data[tx:tx+count] = s + def add_xexboot_header(bytes, bootcode=None, title="DEMO", author="an atari user"): sec_size = 128 xex_size = len(bytes) @@ -74,7 +77,7 @@ def add_xexboot_header(bytes, bootcode=None, title="DEMO", author="an atari user if xex_size < padded_size: bytes = np.append(bytes, np.zeros([padded_size - xex_size], dtype=np.uint8)) paragraphs = padded_size / 16 - + if bootcode is None: bootcode = np.fromstring(xexboot_header, dtype=np.uint8) else: @@ -85,7 +88,7 @@ def add_xexboot_header(bytes, bootcode=None, title="DEMO", author="an atari user bootsize = np.alen(bootcode) v = bootcode[9:11].view(dtype=" 0: origin = " @ %04x" % (self.start_addr) @@ -502,7 +505,7 @@ class DefaultSegment(object): if self.error: s += " " + self.error return s - + @property def verbose_info(self): name = self.verbose_name or self.name @@ -513,32 +516,32 @@ class DefaultSegment(object): if self.error: s += " error='%s'" % self.error return s - + def __len__(self): return len(self.rawdata) - + def __getitem__(self, index): return self.data[index] - + def __setitem__(self, index, value): self.data[index] = value self._search_copy = None - + def byte_bounds_offset(self): """Return start and end offsets of this segment's data into the base array's data """ return self.rawdata.byte_bounds_offset() - + def is_valid_index(self, i): return i >= 0 and i < len(self) - + def get_raw_index(self, i): """Get index into base array's raw data, given the index into this segment """ return self.rawdata.get_raw_index(i) - + def get_index_from_base_index(self, base_index): """Get index into this array's data given the index into the base array """ @@ -553,13 +556,13 @@ class DefaultSegment(object): def tostring(self): return self.data.tostring() - + def get_style_bits(self, **kwargs): return get_style_bits(**kwargs) - + def get_style_mask(self, **kwargs): return get_style_mask(**kwargs) - + def set_style_ranges(self, ranges, **kwargs): style_bits = self.get_style_bits(**kwargs) s = self.style @@ -567,7 +570,7 @@ class DefaultSegment(object): if end < start: start, end = end, start s[start:end] |= style_bits - + def clear_style_ranges(self, ranges, **kwargs): style_mask = self.get_style_mask(**kwargs) s = self.style @@ -575,14 +578,14 @@ class DefaultSegment(object): if end < start: start, end = end, start s[start:end] &= style_mask - + def get_style_ranges(self, **kwargs): """Return a list of start, end pairs that match the specified style """ style_bits = self.get_style_bits(**kwargs) matches = (self.style & style_bits) == style_bits return self.bool_to_ranges(matches) - + def get_comment_locations(self, **kwargs): style_bits = self.get_style_bits(**kwargs) r = self.rawdata.copy() @@ -655,7 +658,7 @@ class DefaultSegment(object): if np.alen(group) > 0: ranges.append((int(group[0]), int(group[-1]) + 1)) return ranges - + def find_next(self, index, **kwargs): ranges = self.get_style_ranges(**kwargs) if len(ranges) > 0: @@ -665,7 +668,7 @@ class DefaultSegment(object): match_index = 0 return ranges[match_index][0] return None - + def find_previous(self, index, **kwargs): ranges = self.get_style_ranges(**kwargs) if len(ranges) > 0: @@ -676,7 +679,7 @@ class DefaultSegment(object): match_index = len(ranges) - 1 return ranges[match_index][0] return None - + def get_rect_indexes(self, anchor_start, anchor_end): # determine row,col of upper left and lower right of selected # rectangle. The values are inclusive, so ul=(0,0) and lr=(1,2) @@ -714,7 +717,7 @@ class DefaultSegment(object): anchor_end = r2 * bpr + c2 r2 += 1 return anchor_start, anchor_end, (r1, c1), (r2, c2) - + def set_style_ranges_rect(self, ranges, **kwargs): style_bits = self.get_style_bits(**kwargs) s = self.style @@ -741,7 +744,7 @@ class DefaultSegment(object): c = c2 - c1 indexes = np.tile(np.arange(c), r) + np.repeat(np.arange(r) * self.map_width, c) + start s[indexes] |= style_bits - + def rects_to_ranges(self, rects): ranges = [] bpr = self.map_width @@ -750,25 +753,25 @@ class DefaultSegment(object): end = (r2 - 1) * bpr + c2 ranges.append((start, end)) return ranges - + def clear_style_bits(self, **kwargs): style_mask = self.get_style_mask(**kwargs) self.style &= style_mask - + def set_user_data(self, ranges, user_index, user_data): for start, end in ranges: # FIXME: this is slow for i in range(start, end): rawindex = self.get_raw_index(i) self.rawdata.extra.user_data[user_index][rawindex] = user_data - + def get_user_data(self, index, user_index): rawindex = self.get_raw_index(index) try: return self.rawdata.extra.user_data[user_index][rawindex] except KeyError: return 0 - + def get_sorted_user_data(self, user_index): d = self.rawdata.extra.user_data[user_index] indexes = sorted(d.keys()) @@ -787,7 +790,7 @@ class DefaultSegment(object): if start is not None: ranges.append([[start, end], current]) return ranges - + def get_style_at_indexes(self, indexes): return self.style[indexes] @@ -893,7 +896,7 @@ class DefaultSegment(object): self.set_style_ranges(ranges, comment=True) for start, end in ranges: self.set_comment_at(start, text) - + def get_comment(self, index): rawindex = self.get_raw_index(index) return self.rawdata.extra.comments.get(rawindex, "") @@ -904,19 +907,19 @@ class DefaultSegment(object): del self.rawdata.extra.comments[rawindex] except KeyError: pass - + def get_first_comment(self, ranges): start = reduce(min, [r[0] for r in ranges]) rawindex = self.get_raw_index(start) return self.rawdata.extra.comments.get(rawindex, "") - + def clear_comment(self, ranges): self.clear_style_ranges(ranges, comment=True) for start, end in ranges: rawindex = self.get_raw_index(start) if rawindex in self.rawdata.extra.comments: del self.rawdata.extra.comments[rawindex] - + def get_sorted_comments(self): return sorted([[k, v] for k, v in self.rawdata.extra.comments.iteritems()]) @@ -927,19 +930,19 @@ class DefaultSegment(object): for k, v in self.rawdata.extra.comments.iteritems(): if k >= start_index and k < end_index: yield self.rawdata.get_reverse_index(k), v - + def label(self, index, lower_case=True): if lower_case: return "%04x" % (index + self.start_addr) else: return "%04X" % (index + self.start_addr) - + @property def search_copy(self): if self._search_copy is None: self._search_copy = self.data.tostring() return self._search_copy - + def compare_segment(self, other_segment): self.clear_style_bits(diff=True) diff = self.rawdata.data != other_segment.rawdata.data @@ -953,20 +956,20 @@ class DefaultSegment(object): class EmptySegment(DefaultSegment): def __init__(self, rawdata, name="", error=None): DefaultSegment.__init__(self, rawdata, 0, name, error) - + def __str__(self): s = "%s (empty file)" % (self.name, ) if self.error: s += " " + self.error return s - + @property def verbose_info(self): s = "%s (empty file)" % (self.name, ) if self.error: s += " error='%s'" % self.error return s - + def __len__(self): return 0 @@ -977,14 +980,14 @@ class ObjSegment(DefaultSegment): self.metadata_start = metadata_start self.data_start = data_start self.use_origin = True - + def __str__(self): count = len(self) s = "%s $%04x-$%04x ($%04x @ $%04x)" % (self.name, self.start_addr, self.start_addr + count, count, self.data_start) if self.error: s += " " + self.error return s - + @property def verbose_info(self): count = len(self) @@ -1007,7 +1010,7 @@ class RawSectorsSegment(DefaultSegment): self.page_size = sector_size 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) @@ -1016,7 +1019,7 @@ class RawSectorsSegment(DefaultSegment): if self.error: s += " " + self.error return s - + @property def verbose_info(self): name = self.verbose_name or self.name @@ -1028,7 +1031,7 @@ class RawSectorsSegment(DefaultSegment): if self.error: s += " error='%s'" % self.error return s - + def label(self, index, lower_case=True): boot_size = self.num_boot_sectors * self.boot_sector_size if index >= boot_size: @@ -1055,6 +1058,7 @@ class RawTrackSectorSegment(RawSectorsSegment): return "t%02ds%02d:%02x" % (t, s, byte) return "t%02ds%02d:%02X" % (t, s, byte) + def interleave_indexes(segments, num_bytes): num_segments = len(segments) size = len(segments[0]) @@ -1074,6 +1078,7 @@ def interleave_indexes(segments, num_bytes): start += 1 return interleave + def interleave_segments(segments, num_bytes): new_index = interleave_indexes(segments, num_bytes) data_base, style_base = segments[0].rawdata.get_bases() diff --git a/atrcopy/spartados.py b/atrcopy/spartados.py index 1256074..04eb910 100644 --- a/atrcopy/spartados.py +++ b/atrcopy/spartados.py @@ -30,7 +30,7 @@ class SpartaDosDirent(AtariDosDirent): # rather the boot sector so it must be specified here. self.starting_sector = starting_sector self.is_sane = self.sanity_check(image) - + def __str__(self): output = "o" if self.opened_output else "." subdir = "D" if self.is_dir else "." @@ -39,7 +39,7 @@ class SpartaDosDirent(AtariDosDirent): locked = "*" if self.locked else " " flags = "%s%s%s%s%s %03d" % (output, subdir, in_use, deleted, locked, self.starting_sector) return "File #%-2d (%s) %-8s%-3s %8d %s" % (self.file_num, flags, self.filename, self.ext, self.length, self.str_timestamp) - + @property def verbose_info(self): flags = [] @@ -49,11 +49,11 @@ class SpartaDosDirent(AtariDosDirent): if self.deleted: flags.append("DEL") if self.locked: flags.append("LOCK") return "flags=[%s]" % ", ".join(flags) - + def parse_raw_dirent(self, image, bytes): if bytes is None: return - values = bytes.view(dtype=self.format)[0] + values = bytes.view(dtype=self.format)[0] flag = values['status'] self.flag = flag self.locked = (flag&0x1) > 0 @@ -73,14 +73,14 @@ class SpartaDosDirent(AtariDosDirent): self.date_array = tuple(bytes[17:20]) self.time_array = tuple(bytes[20:23]) self.is_sane = self.sanity_check(image) - + def sanity_check(self, image): if not self.in_use: return True if not image.header.sector_is_valid(self.starting_sector): return False return True - + @property def str_timestamp(self): str_date = "%d/%d/%d" % self.date_array @@ -94,7 +94,7 @@ class SpartaDosDirent(AtariDosDirent): self.sector_map = image.get_sector_map(self.starting_sector) self.sector_map_index = 0 self.length_remaining = self.length - + def read_sector(self, image): sector = self.sector_map[self.sector_map_index] if sector == 0: @@ -114,10 +114,10 @@ class SpartaDosDiskImage(AtariDosDiskImage): self.root_dir_dirent = None self.fs_version = 0 AtariDosDiskImage.__init__(self, *args, **kwargs) - + def __str__(self): return "%s Sparta DOS Format: %d usable sectors (%d free), %d files" % (self.header, self.total_sectors, self.unused_sectors, len(self.files)) - + boot_record_type = np.dtype([ ('unused', 'u1'), ('num_boot', 'u1'), @@ -137,12 +137,12 @@ class SpartaDosDiskImage(AtariDosDiskImage): ('sector_size','u1'), ('fs_version','u1'), ]) - + sector_size_map = {0: 256, 1: 512, 0x80: 128, } - + def get_boot_sector_info(self): data, style = self.get_sectors(1) values = data[0:33].view(dtype=self.boot_record_type)[0] @@ -163,7 +163,7 @@ class SpartaDosDiskImage(AtariDosDiskImage): def get_vtoc(self): pass - + def get_directory(self): self.files = [] dir_map = self.get_sector_map(self.root_dir) @@ -177,7 +177,7 @@ class SpartaDosDiskImage(AtariDosDiskImage): dirent = SpartaDosDirent(self, filenum + 1, s[i:i + 23]) self.files.append(dirent) self.root_dir_dirent = d - + def get_boot_segments(self): segments = [] num = min(self.num_boot, 1) @@ -191,7 +191,7 @@ class SpartaDosDiskImage(AtariDosDiskImage): code = ObjSegment(r[43:], 0, 0, addr + 43, addr + len(r), name="Boot Code") segments.extend([header, code]) return segments - + def get_vtoc_segments(self): r = self.rawdata segments = [] @@ -206,7 +206,7 @@ class SpartaDosDiskImage(AtariDosDiskImage): segment = RawSectorsSegment(r[start:start+count], self.first_bitmap, self.num_bitmap, count, 0, 0, self.sector_size, name="Bitmap") segments.append(segment) return segments - + def get_sector_map(self, sector): m = None while sector > 0: @@ -217,7 +217,7 @@ class SpartaDosDiskImage(AtariDosDiskImage): else: m = np.hstack((m, b[4:].view(dtype='