2014-06-18 16:01:35 -07:00
#!/usr/bin/env python
2015-09-26 15:52:54 -07:00
2016-02-09 10:45:23 -08:00
__version__ = " 2.1.0 "
2015-09-26 15:52:54 -07:00
2016-02-05 23:08:32 -08:00
import types
import numpy as np
2015-09-26 15:52:54 -07:00
2015-05-18 22:52:44 -07:00
class AtrError ( RuntimeError ) :
pass
class InvalidAtrHeader ( AtrError ) :
pass
2016-02-07 22:22:21 -08:00
class InvalidDiskImage ( AtrError ) :
pass
2015-05-18 22:52:44 -07:00
class LastDirent ( AtrError ) :
pass
class FileNumberMismatchError164 ( AtrError ) :
pass
2015-05-19 16:46:40 -07:00
class ByteNotInFile166 ( AtrError ) :
pass
2015-05-18 22:52:44 -07:00
class AtrHeader ( object ) :
2016-02-05 23:08:32 -08:00
# ATR Format described in http://www.atarimax.com/jindroush.atari.org/afmtatr.html
format = np . dtype ( [
( ' wMagic ' , ' <u2 ' ) ,
( ' wPars ' , ' <u2 ' ) ,
( ' wSecSize ' , ' <u2 ' ) ,
( ' btParsHigh ' , ' u1 ' ) ,
( ' dwCRC ' , ' <u4 ' ) ,
( ' unused ' , ' <u4 ' ) ,
( ' btFlags ' , ' u1 ' ) ,
] )
2015-05-19 15:16:19 -07:00
file_format = " ATR "
2015-05-18 22:52:44 -07:00
def __init__ ( self , bytes = None ) :
2016-02-07 18:21:13 -08:00
self . image_size = 0
2015-05-18 22:52:44 -07:00
self . sector_size = 0
self . crc = 0
self . unused = 0
self . flags = 0
self . atr_header_offset = 0
2015-05-19 16:46:40 -07:00
self . initial_sector_size = 0
self . num_initial_sectors = 0
self . max_sectors = 0
2015-05-18 22:52:44 -07:00
if bytes is None :
return
if len ( bytes ) == 16 :
2016-02-05 23:08:32 -08:00
values = bytes . view ( dtype = self . format ) [ 0 ]
2015-05-18 22:52:44 -07:00
if values [ 0 ] != 0x296 :
raise InvalidAtrHeader
2016-02-07 18:21:13 -08:00
self . image_size = ( int ( values [ 3 ] ) * 256 * 256 + int ( values [ 1 ] ) ) * 16
2016-02-05 23:08:32 -08:00
self . sector_size = int ( values [ 2 ] )
self . crc = int ( values [ 4 ] )
self . unused = int ( values [ 5 ] )
self . flags = int ( values [ 6 ] )
2015-05-18 22:52:44 -07:00
self . atr_header_offset = 16
else :
raise InvalidAtrHeader
def __str__ ( self ) :
2016-02-07 18:21:13 -08:00
return " %s Disk Image (size= %d ( %d x %d b), crc= %d flags= %d unused= %d ) " % ( self . file_format , self . image_size , self . max_sectors , self . sector_size , self . crc , self . flags , self . unused )
2015-05-19 15:16:19 -07:00
def check_size ( self , size ) :
2016-02-07 18:21:13 -08:00
if size == 92160 or size == 92176 :
self . image_size = 92160
self . sector_size = 128
self . initial_sector_size = 0
self . num_initial_sectors = 0
elif size == 184320 or size == 184336 :
self . image_size = 184320
self . sector_size = 256
self . initial_sector_size = 0
self . num_initial_sectors = 0
elif size == 183936 or size == 183952 :
self . image_size = 183936
self . sector_size = 256
self . initial_sector_size = 128
self . num_initial_sectors = 3
2016-02-07 22:22:21 -08:00
else :
self . image_size = size
self . sector_size = 128
2016-02-07 18:21:13 -08:00
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
2015-05-19 16:46:40 -07:00
def sector_is_valid ( self , sector ) :
2015-11-03 14:25:43 -08:00
return sector > 0 and sector < = self . max_sectors
2015-05-19 16:46:40 -07:00
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
2015-05-19 15:16:19 -07:00
class XfdHeader ( AtrHeader ) :
file_format = " XFD "
def __str__ ( self ) :
2016-02-07 18:21:13 -08:00
return " %s Disk Image (size= %d ( %d x %d b) " % ( self . file_format , self . image_size , self . max_sectors , self . sector_size )
2015-05-18 22:52:44 -07:00
class AtrDirent ( object ) :
2016-02-05 23:08:32 -08:00
# ATR Dirent structure described at http://atari.kensclassics.org/dos.htm
format = np . dtype ( [
( ' FLAG ' , ' u1 ' ) ,
( ' COUNT ' , ' <u2 ' ) ,
( ' START ' , ' <u2 ' ) ,
( ' NAME ' , ' S8 ' ) ,
( ' EXT ' , ' S3 ' ) ,
] )
2015-05-18 22:52:44 -07:00
2015-05-19 16:46:40 -07:00
def __init__ ( self , disk , file_num = 0 , bytes = None ) :
2015-05-18 22:52:44 -07:00
self . file_num = file_num
self . flag = 0
self . opened_output = False
self . dos_2 = False
self . mydos = False
self . is_dir = False
self . locked = False
self . in_use = False
self . deleted = False
self . num_sectors = 0
self . starting_sector = 0
self . filename = " "
self . ext = " "
2016-02-09 10:23:06 -08:00
self . is_sane = True
2015-05-18 22:52:44 -07:00
if bytes is None :
return
2016-02-05 23:08:32 -08:00
values = bytes . view ( dtype = self . format ) [ 0 ]
2015-05-18 22:52:44 -07:00
flag = values [ 0 ]
self . flag = flag
self . opened_output = ( flag & 0x01 ) > 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
2016-02-05 23:08:32 -08:00
self . num_sectors = int ( values [ 1 ] )
self . starting_sector = int ( values [ 2 ] )
self . filename = str ( values [ 3 ] ) . rstrip ( )
self . ext = str ( values [ 4 ] ) . rstrip ( )
2015-05-19 15:16:19 -07:00
self . current_sector = 0
2015-05-19 16:46:40 -07:00
self . is_sane = self . sanity_check ( disk )
2015-05-18 22:52:44 -07:00
def __str__ ( self ) :
2015-05-19 15:29:10 -07:00
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 )
2015-05-18 22:52:44 -07:00
if self . in_use :
2015-05-19 15:29:10 -07:00
return " File # %-2d ( %s ) %-8s %-3s %03d " % ( self . file_num , flags , self . filename , self . ext , self . num_sectors )
2015-05-18 22:52:44 -07:00
return
2015-05-19 16:46:40 -07:00
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
2015-05-19 15:16:19 -07:00
def start_read ( self ) :
self . current_sector = self . starting_sector
self . current_read = self . num_sectors
def read_sector ( self , disk ) :
2016-02-05 23:08:32 -08:00
raw , pos , size = disk . get_raw_bytes ( self . current_sector )
bytes , num_data_bytes = self . process_raw_sector ( disk , raw )
return bytes , self . current_sector == 0 , pos , num_data_bytes
2015-05-19 15:16:19 -07:00
def process_raw_sector ( self , disk , raw ) :
2016-02-05 23:08:32 -08:00
file_num = raw [ - 3 ] >> 2
if file_num != self . file_num :
raise FileNumberMismatchError164 ( )
self . current_sector = ( ( raw [ - 3 ] & 0x3 ) << 8 ) + raw [ - 2 ]
num_bytes = raw [ - 1 ]
return raw [ 0 : num_bytes ] , num_bytes
2015-05-18 22:52:44 -07:00
def get_filename ( self ) :
ext = ( " . " + self . ext ) if self . ext else " "
return self . filename + ext
2015-05-19 15:16:19 -07:00
class MydosDirent ( AtrDirent ) :
def process_raw_sector ( self , disk , raw ) :
2016-02-08 14:36:24 -08:00
# No file number stored in the sector data; two full bytes available
# for next sector
self . current_sector = ( raw [ - 3 ] << 8 ) + raw [ - 2 ]
num_bytes = raw [ - 1 ]
return raw [ 0 : num_bytes ] , num_bytes
2015-05-19 15:16:19 -07:00
2015-09-25 23:02:35 -07:00
class InvalidBinaryFile ( AtrError ) :
2015-05-18 22:52:44 -07:00
pass
2016-02-05 23:08:32 -08:00
2016-02-08 12:23:38 -08:00
class SegmentSaver ( object ) :
name = " Raw Data "
extensions = [ " .dat " ]
@classmethod
def encode_data ( cls , segment ) :
return segment . tostring ( )
@classmethod
def get_file_dialog_wildcard ( cls ) :
# Using only the first extension
wildcards = [ ]
if cls . extensions :
ext = cls . extensions [ 0 ]
wildcards . append ( " %s (* %s )|* %s " % ( cls . name , ext , ext ) )
return " | " . join ( wildcards )
class XEXSegmentSaver ( SegmentSaver ) :
name = " Atari 8-bit Executable "
extensions = [ " .xex " ]
2016-02-05 23:08:32 -08:00
class DefaultSegment ( object ) :
2016-02-08 12:23:38 -08:00
savers = [ SegmentSaver ]
2016-02-05 23:08:32 -08:00
2016-02-09 16:55:24 -08:00
def __init__ ( self , data , style , start_addr = 0 , name = " All " , error = None ) :
2016-02-05 23:08:32 -08:00
self . start_addr = int ( start_addr ) # force python int to decouple from possibly being a numpy datatype
2015-09-25 23:02:35 -07:00
self . data = data
2016-02-09 16:55:24 -08:00
self . style = style
2015-09-25 23:02:35 -07:00
self . error = error
2015-10-16 10:15:00 -07:00
self . name = name
2015-12-14 15:58:29 -08:00
self . page_size = - 1
2016-02-05 23:08:32 -08:00
self . map_width = 40
self . _search_copy = None
2015-09-25 23:02:35 -07:00
def __str__ ( self ) :
2016-02-06 11:36:57 -08:00
return " %s ( %d bytes) " % ( self . name , len ( self ) )
2015-10-14 15:02:02 -07:00
def __len__ ( self ) :
2016-02-05 23:08:32 -08:00
return np . alen ( self . data )
def __getitem__ ( self , index ) :
return self . data [ index ]
def __setitem__ ( self , index , value ) :
self . data [ index ] = value
self . _search_copy = None
2016-02-06 11:55:09 -08:00
def byte_bounds_offset ( self ) :
return np . byte_bounds ( self . data ) [ 0 ]
2016-02-05 23:08:32 -08:00
def tostring ( self ) :
return self . data . tostring ( )
2016-02-10 10:28:36 -08:00
def get_style_bits ( self , match = False , comment = False , selected = False ) :
2016-02-05 23:08:32 -08:00
style_bits = 0
if match :
style_bits | = 1
if comment :
2016-02-10 10:28:36 -08:00
style_bits | = 2
if selected :
2016-02-05 23:08:32 -08:00
style_bits | = 0x80
return style_bits
2016-02-10 10:28:36 -08:00
def get_style_mask ( self , * * kwargs ) :
return 0xff ^ self . get_style_bits ( * * kwargs )
2016-02-05 23:08:32 -08:00
def set_style_ranges ( self , ranges , * * kwargs ) :
style_bits = self . get_style_bits ( * * kwargs )
s = self . style
for start , end in ranges :
2016-02-10 10:28:36 -08:00
if end < start :
start , end = end , start
2016-02-05 23:08:32 -08:00
s [ start : end ] | = style_bits
def clear_style_bits ( self , * * kwargs ) :
style_mask = self . get_style_mask ( * * kwargs )
self . style & = style_mask
def label ( self , index , lower_case = True ) :
if lower_case :
return " %04x " % ( index + self . start_addr )
else :
return " %04X " % ( index + self . start_addr )
2015-10-14 15:02:02 -07:00
2016-02-05 23:08:32 -08:00
@property
def search_copy ( self ) :
if self . _search_copy is None :
self . _search_copy = self . data . tostring ( )
return self . _search_copy
class ObjSegment ( DefaultSegment ) :
2016-02-09 16:55:24 -08:00
def __init__ ( self , data , style , metadata_start , data_start , start_addr , end_addr , name = " " , error = None ) :
DefaultSegment . __init__ ( self , data , style , start_addr , name , error )
2016-02-05 23:08:32 -08:00
self . metadata_start = metadata_start
self . data_start = data_start
2015-12-14 15:58:29 -08:00
2016-02-05 23:08:32 -08:00
def __str__ ( self ) :
count = len ( self )
2016-02-07 11:12:50 -08:00
s = " %s $ %04x -$ %04x ($ %04x @ $ %04x ) " % ( self . name , self . start_addr , self . start_addr + count , count , self . data_start )
2016-02-05 23:08:32 -08:00
if self . error :
s + = " " + self . error
return s
2015-12-14 15:58:29 -08:00
2016-02-08 12:23:38 -08:00
class XexSegment ( ObjSegment ) :
savers = [ SegmentSaver , XEXSegmentSaver ]
2016-02-05 23:08:32 -08:00
class RawSectorsSegment ( DefaultSegment ) :
2016-02-09 16:55:24 -08:00
def __init__ ( self , data , style , first_sector , num_sectors , count , * * kwargs ) :
DefaultSegment . __init__ ( self , data , style , 0 , * * kwargs )
2015-12-14 15:58:29 -08:00
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
2016-02-05 23:08:32 -08:00
def label ( self , index , lower_case = True ) :
2015-12-14 15:58:29 -08:00
sector , byte = divmod ( index , self . page_size )
2016-02-05 23:08:32 -08:00
if lower_case :
return " s %03d : %02x " % ( sector + self . first_sector , byte )
return " s %03d : %02X " % ( sector + self . first_sector , byte )
class IndexedByteSegment ( DefaultSegment ) :
2016-02-09 16:55:24 -08:00
def __init__ ( self , data , style , byte_order , * * kwargs ) :
2016-02-06 11:02:09 -08:00
self . order = byte_order
2016-02-09 16:55:24 -08:00
DefaultSegment . __init__ ( self , data , style , 0 , * * kwargs )
2016-02-06 11:02:09 -08:00
2016-02-07 11:12:50 -08:00
def __str__ ( self ) :
return " %s ($ %x @ $ %x ) " % ( self . name , len ( self ) , self . order [ 0 ] )
2016-02-06 11:36:57 -08:00
def __len__ ( self ) :
return np . alen ( self . order )
2016-02-06 11:02:09 -08:00
def __getitem__ ( self , index ) :
return self . data [ self . order [ index ] ]
def __setitem__ ( self , index , value ) :
self . data [ self . order [ index ] ] = value
self . _search_copy = None
2016-02-06 11:55:09 -08:00
def byte_bounds_offset ( self ) :
return np . byte_bounds ( self . data ) [ 0 ] + self . order [ 0 ]
2016-02-06 11:02:09 -08:00
def tostring ( self ) :
return self . data [ self . order [ : ] ] . tostring ( )
2016-02-05 23:08:32 -08:00
2015-09-25 23:02:35 -07:00
class AtariDosFile ( object ) :
2015-09-26 15:52:54 -07:00
""" Parse a binary chunk into segments according to the Atari DOS object
file format .
Ref : http : / / www . atarimax . com / jindroush . atari . org / afmtexe . html
"""
2016-02-09 16:55:24 -08:00
def __init__ ( self , data , style = None ) :
self . bytes = to_numpy ( data )
self . size = np . alen ( self . bytes )
if style is None :
self . style = np . zeros ( self . size , dtype = np . uint8 )
else :
self . style = style
2015-09-25 23:02:35 -07:00
self . segments = [ ]
self . parse_segments ( )
def __str__ ( self ) :
return " \n " . join ( str ( s ) for s in self . segments ) + " \n "
def parse_segments ( self ) :
2016-02-09 16:55:24 -08:00
b = self . bytes
s = self . style
2015-09-25 23:02:35 -07:00
pos = 0
first = True
while pos < self . size :
2016-02-09 10:37:36 -08:00
if pos + 1 < self . size :
2016-02-09 16:55:24 -08:00
header , = b [ pos : pos + 2 ] . view ( dtype = ' <u2 ' )
2016-02-09 10:37:36 -08:00
else :
2016-02-09 16:55:24 -08:00
self . segments . append ( ObjSegment ( b [ pos : pos + 1 ] , s [ pos : pos + 1 ] , pos , pos + 1 , 0 , 1 , " Incomplete Data " ) )
2016-02-09 10:37:36 -08:00
break
2015-09-25 23:02:35 -07:00
if header == 0xffff :
# Apparently 0xffff header can appear in any segment, not just
# the first. Regardless, it is ignored everywhere.
pos + = 2
elif first :
raise InvalidBinaryFile
first = False
2016-02-09 16:55:24 -08:00
if len ( b [ pos : pos + 4 ] ) < 4 :
self . segments . append ( ObjSegment ( b [ pos : pos + 4 ] , s [ pos : pos + 4 ] , 0 , 0 , " Short Segment Header " ) )
2015-09-25 23:02:35 -07:00
break
2016-02-09 16:55:24 -08:00
start , end = b [ pos : pos + 4 ] . view ( dtype = ' <u2 ' )
2015-09-25 23:02:35 -07:00
count = end - start + 1
2016-02-09 16:55:24 -08:00
found = len ( b [ pos + 4 : pos + 4 + count ] )
2015-09-25 23:02:35 -07:00
if found < count :
2016-02-09 16:55:24 -08:00
self . segments . append ( ObjSegment ( b [ pos + 4 : pos + 4 + count ] , s [ pos + 4 : pos + 4 + count ] , pos , pos + 4 , start , end , " Incomplete Data " ) )
2015-09-25 23:02:35 -07:00
break
2016-02-09 16:55:24 -08:00
self . segments . append ( ObjSegment ( b [ pos + 4 : pos + 4 + count ] , s [ pos + 4 : pos + 4 + count ] , pos , pos + 4 , start , end ) )
2015-09-25 23:02:35 -07:00
pos + = 4 + count
2016-02-07 22:22:21 -08:00
class DiskImageBase ( object ) :
2016-02-09 16:55:24 -08:00
debug = False
def __init__ ( self , bytes , style = None , filename = " " ) :
2016-02-05 23:08:32 -08:00
self . bytes = to_numpy ( bytes )
2016-02-09 16:55:24 -08:00
self . size = np . alen ( self . bytes )
if style is None :
if self . debug :
self . style = np . arange ( self . size , dtype = np . uint8 )
else :
self . style = np . zeros ( self . size , dtype = np . uint8 )
else :
self . style = style
2016-02-09 10:23:06 -08:00
self . set_filename ( filename )
2015-09-27 17:52:37 -07:00
self . header = None
self . total_sectors = 0
self . unused_sectors = 0
self . files = [ ]
2015-10-16 10:15:00 -07:00
self . segments = [ ]
2015-09-27 17:52:37 -07:00
self . all_sane = True
self . setup ( )
2016-02-09 10:23:06 -08:00
def set_filename ( self , filename ) :
if " . " in filename :
self . filename , self . ext = filename . rsplit ( " . " , 1 )
else :
self . filename , self . ext = filename , " "
2015-09-27 17:52:37 -07:00
def setup ( self ) :
2016-02-07 22:22:21 -08:00
self . size = np . alen ( self . bytes )
2015-09-27 17:52:37 -07:00
self . read_atr_header ( )
self . check_size ( )
2016-02-09 10:23:06 -08:00
self . get_vtoc ( )
self . get_directory ( )
self . check_sane ( )
def check_sane ( self ) :
if not self . all_sane :
raise InvalidDiskImage ( " Invalid directory entries; may be boot disk " )
2015-09-27 17:52:37 -07:00
def read_atr_header ( self ) :
bytes = self . bytes [ 0 : 16 ]
try :
self . header = AtrHeader ( bytes )
except InvalidAtrHeader :
self . header = XfdHeader ( )
def check_size ( self ) :
self . header . check_size ( self . size )
2016-02-09 10:23:06 -08:00
def get_vtoc ( self ) :
pass
def get_directory ( self ) :
pass
2015-09-27 17:52:37 -07:00
def get_raw_bytes ( self , sector ) :
pos , size = self . header . get_pos ( sector )
2016-02-05 23:08:32 -08:00
return self . bytes [ pos : pos + size ] , pos , size
2015-09-27 17:52:37 -07:00
def get_sectors ( self , start , end = None ) :
""" Get contiguous sectors
: param start : first sector number to read ( note : numbering starts from 1 )
: param end : last sector number to read
: returns : bytes
"""
pos , size = self . header . get_pos ( start )
if end is None :
end = start
2015-10-16 10:12:44 -07:00
while start < end :
2015-09-27 17:52:37 -07:00
start + = 1
_ , more = self . header . get_pos ( start )
size + = more
2016-02-09 16:55:24 -08:00
return self . bytes [ pos : pos + size ] , self . style [ pos : pos + size ]
2015-09-27 17:52:37 -07:00
2016-02-07 22:22:21 -08:00
def get_contiguous_sectors ( self , sector , num ) :
start = 0
count = 0
for index in range ( sector , sector + num ) :
pos , size = self . header . get_pos ( index )
if start == 0 :
start = pos
count + = size
return start , count
2016-02-08 10:13:42 -08:00
def parse_segments ( self ) :
2016-02-09 16:55:24 -08:00
b = self . bytes
s = self . style
i = self . header . atr_header_offset
2016-02-08 10:13:42 -08:00
if self . header . image_size > 0 :
2016-02-09 16:55:24 -08:00
self . segments . append ( ObjSegment ( b [ 0 : i ] , s [ 0 : i ] , 0 , 0 , 0 , i , name = " %s Header " % self . header . file_format ) )
self . segments . append ( RawSectorsSegment ( b [ i : ] , s [ i : ] , 1 , self . header . max_sectors , self . header . image_size , name = " Raw disk sectors " ) )
2016-02-09 10:23:06 -08:00
self . segments . extend ( self . get_boot_segments ( ) )
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 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 :
segment = self . get_file_segment ( dirent )
segments . append ( segment )
return segments
2016-02-08 10:13:42 -08:00
class BootDiskImage ( DiskImageBase ) :
def __str__ ( self ) :
return " %s Boot Disk " % ( self . header )
def check_size ( self ) :
self . header . check_size ( self . size )
2016-02-09 16:55:24 -08:00
2016-02-08 10:13:42 -08:00
start , size = self . header . get_pos ( 1 )
2016-02-09 16:55:24 -08:00
b = self . bytes
2016-02-08 10:13:42 -08:00
i = self . header . atr_header_offset
2016-02-09 16:55:24 -08:00
flag = b [ i : i + 2 ] . view ( dtype = ' <u2 ' ) [ 0 ]
2016-02-08 10:13:42 -08:00
if flag == 0xffff :
raise InvalidDiskImage ( " Appears to be an executable " )
2016-02-09 16:55:24 -08:00
nsec = b [ i + 1 ]
bload = b [ i + 2 : i + 4 ] . view ( dtype = ' <u2 ' ) [ 0 ]
binit = b [ i + 4 : i + 6 ] . view ( dtype = ' <u2 ' ) [ 0 ]
2016-02-08 10:13:42 -08:00
blen , _ = self . header . get_pos ( nsec + 1 )
print nsec , bload , binit , blen
if not ( bload < binit < bload + blen ) :
raise InvalidDiskImage ( " Incorrect boot load/init parameters " )
2016-02-07 22:22:21 -08:00
class AtariDosDiskImage ( DiskImageBase ) :
2016-02-09 16:55:24 -08:00
def __init__ ( self , bytes , style = None ) :
2016-02-07 22:22:21 -08:00
self . first_vtoc = 360
self . num_vtoc = 1
self . vtoc2 = 0
self . first_data_after_vtoc = 369
2016-02-09 16:55:24 -08:00
DiskImageBase . __init__ ( self , bytes , style )
2016-02-07 22:22:21 -08:00
def __str__ ( self ) :
if self . all_sane :
return " %s Atari DOS Format: %d usable sectors ( %d free), %d files " % ( self . header , self . total_sectors , self . unused_sectors , len ( self . files ) )
else :
return " %s bad directory entries; possible boot disk? Use -f option to try to extract anyway " % self . header
def dir ( self ) :
lines = [ ]
lines . append ( str ( self ) )
for dirent in self . files :
if dirent . in_use :
lines . append ( str ( dirent ) )
return " \n " . join ( lines )
2016-02-05 23:08:32 -08:00
vtoc_type = np . dtype ( [
( ' code ' , ' u1 ' ) ,
( ' total ' , ' <u2 ' ) ,
( ' unused ' , ' <u2 ' ) ,
] )
2015-09-27 17:52:37 -07:00
def get_vtoc ( self ) :
2016-02-09 16:55:24 -08:00
data , style = self . get_sectors ( 360 )
values = data [ 0 : 5 ] . view ( dtype = self . vtoc_type ) [ 0 ]
2015-09-27 17:52:37 -07:00
code = values [ 0 ]
if code == 0 or code == 2 :
num = 1
else :
num = ( code * 2 ) - 3
self . first_vtoc = 360 - num + 1
2015-12-14 15:58:29 -08:00
self . num_vtoc = num
2015-09-27 17:52:37 -07:00
self . total_sectors = values [ 1 ]
self . unused_sectors = values [ 2 ]
2016-02-07 18:37:38 -08:00
if self . header . image_size == 133120 :
# enhanced density has 2nd VTOC
self . vtoc2 = 1024
extra_free = self . get_sectors ( self . vtoc2 ) [ 122 : 124 ] . view ( dtype = ' <u2 ' ) [ 0 ]
self . unused_sectors + = extra_free
2015-09-27 17:52:37 -07:00
def get_directory ( self ) :
2016-02-09 16:55:24 -08:00
dir_bytes , style = self . get_sectors ( 361 , 368 )
2015-09-27 17:52:37 -07:00
i = 0
num = 0
files = [ ]
while i < len ( dir_bytes ) :
dirent = AtrDirent ( self , num , dir_bytes [ i : i + 16 ] )
if dirent . mydos :
dirent = MydosDirent ( self , num , dir_bytes [ i : i + 16 ] )
if dirent . in_use :
files . append ( dirent )
if not dirent . is_sane :
self . all_sane = False
elif dirent . flag == 0 :
break
i + = 16
num + = 1
self . files = files
def find_file ( self , filename ) :
for dirent in self . files :
if filename == dirent . get_filename ( ) :
2016-02-05 23:08:32 -08:00
return self . get_file ( dirent )
2015-09-27 17:52:37 -07:00
return " "
2015-10-16 10:15:00 -07:00
2016-02-05 23:08:32 -08:00
boot_record_type = np . dtype ( [
( ' BFLAG ' , ' u1 ' ) ,
( ' BRCNT ' , ' u1 ' ) ,
( ' BLDADR ' , ' <u2 ' ) ,
( ' BWTARR ' , ' <u2 ' ) ,
( ' jmp ' , ' u1 ' ) ,
( ' XBCONT ' , ' <u2 ' ) ,
( ' SABYTE ' , ' u1 ' ) ,
( ' DRVBYT ' , ' u1 ' ) ,
( ' unused ' , ' u1 ' ) ,
( ' SASA ' , ' <u2 ' ) ,
( ' DFSFLG ' , ' u1 ' ) ,
( ' DFLINK ' , ' <u2 ' ) ,
( ' BLDISP ' , ' u1 ' ) ,
( ' DFLADR ' , ' <u2 ' ) ,
] )
2015-12-22 19:57:17 -08:00
2015-10-16 10:15:00 -07:00
def get_boot_segments ( self ) :
2016-02-09 16:55:24 -08:00
data , style = self . get_sectors ( 360 )
values = data [ 0 : 20 ] . view ( dtype = self . boot_record_type ) [ 0 ]
2016-02-05 23:08:32 -08:00
flag = int ( values [ 0 ] )
2015-10-16 10:15:00 -07:00
segments = [ ]
if flag == 0 :
2016-02-05 23:08:32 -08:00
num = int ( values [ 1 ] )
addr = int ( values [ 2 ] )
2016-02-09 16:55:24 -08:00
bytes , style = self . get_sectors ( 1 , num )
header = ObjSegment ( bytes [ 0 : 20 ] , style [ 0 : 20 ] , 0 , 0 , addr , addr + 20 , name = " Boot Header " )
sectors = ObjSegment ( bytes , style , 0 , 0 , addr , addr + len ( bytes ) , bytes , name = " Boot Sectors " )
code = ObjSegment ( bytes [ 20 : ] , style [ 20 : ] , 0 , 0 , addr + 20 , addr + len ( bytes ) , name = " Boot Code " )
2015-10-16 10:15:00 -07:00
segments = [ sectors , header , code ]
return segments
2015-12-14 15:58:29 -08:00
def get_vtoc_segments ( self ) :
2016-02-09 16:55:24 -08:00
b = self . bytes
s = self . style
2015-12-14 15:58:29 -08:00
segments = [ ]
addr = 0
start , count = self . get_contiguous_sectors ( self . first_vtoc , self . num_vtoc )
2016-02-09 16:55:24 -08:00
segment = RawSectorsSegment ( b [ start : start + count ] , s [ start : start + count ] , self . first_vtoc , self . num_vtoc , count , name = " VTOC " )
2015-12-14 15:58:29 -08:00
segments . append ( segment )
2016-02-07 18:37:38 -08:00
if self . vtoc2 > 0 :
start , count = self . get_contiguous_sectors ( self . vtoc2 , 1 )
2016-02-09 16:55:24 -08:00
segment = RawSectorsSegment ( b [ start : start + count ] , s [ start : start + count ] , self . vtoc2 , 1 , count , name = " VTOC2 " )
2016-02-07 18:37:38 -08:00
segments . append ( segment )
2015-12-14 15:58:29 -08:00
return segments
def get_directory_segments ( self ) :
2016-02-09 16:55:24 -08:00
b = self . bytes
s = self . style
2015-11-03 14:28:18 -08:00
segments = [ ]
addr = 0
2015-12-14 15:58:29 -08:00
start , count = self . get_contiguous_sectors ( 361 , 8 )
2016-02-09 16:55:24 -08:00
segment = RawSectorsSegment ( b [ start : start + count ] , s [ start : start + count ] , 361 , 8 , count , name = " Directory " )
2015-12-14 15:58:29 -08:00
segments . append ( segment )
2015-11-03 14:28:18 -08:00
return segments
2016-02-05 23:08:32 -08:00
def get_file_segment ( self , dirent ) :
byte_order = [ ]
dirent . start_read ( )
while True :
bytes , last , pos , size = dirent . read_sector ( self )
byte_order . extend ( range ( pos , pos + size ) )
if last :
break
2016-02-09 16:55:24 -08:00
segment = IndexedByteSegment ( self . bytes , self . style , byte_order , name = dirent . get_filename ( ) )
2016-02-05 23:08:32 -08:00
return segment
2016-02-07 22:22:21 -08:00
2016-02-09 10:23:06 -08:00
class KBootDirent ( AtrDirent ) :
def __init__ ( self , image ) :
AtrDirent . __init__ ( self , image )
self . in_use = True
self . starting_sector = 4
self . filename = image . filename
if not self . filename :
self . filename = " KBOOT "
if self . filename == self . filename . upper ( ) :
self . ext = " XEX "
else :
self . ext = " xex "
start , size = image . header . get_pos ( 4 )
i = image . header . atr_header_offset + 9
count = image . bytes [ i ] + 256 * image . bytes [ i + 1 ] + 256 * 256 * image . bytes [ i + 2 ]
if start + count > image . size or start + count < image . size - 128 :
self . is_sane = False
else :
self . exe_size = count
self . exe_start = start
self . num_sectors = count / 128 + 1
def process_raw_sector ( self , disk , raw ) :
num_bytes = np . alen ( raw )
return raw [ 0 : num_bytes ] , num_bytes
2016-02-07 22:22:21 -08:00
2016-02-09 10:23:06 -08:00
class KBootImage ( DiskImageBase ) :
2016-02-07 22:22:21 -08:00
def __str__ ( self ) :
2016-02-09 10:23:06 -08:00
return " %s KBoot Format: %d byte executable " % ( self . header , self . files [ 0 ] . exe_size )
2016-02-07 22:22:21 -08:00
def check_size ( self ) :
self . header . check_size ( self . size )
2016-02-09 10:23:06 -08:00
def check_sane ( self ) :
if not self . all_sane :
raise InvalidDiskImage ( " Doesn ' t seem to be KBoot header " )
2016-02-07 22:22:21 -08:00
2016-02-09 10:23:06 -08:00
def get_directory ( self ) :
dirent = KBootDirent ( self )
if not dirent . is_sane :
self . all_sane = False
self . files = [ dirent ]
def get_file_segment ( self , dirent ) :
2016-02-09 16:55:24 -08:00
start = dirent . exe_start
end = dirent . exe_start + dirent . exe_size
return XexSegment ( self . bytes [ start : end ] , self . style [ start : end ] , 0 , 0 , 0 , start , name = " KBoot Executable " )
2016-02-07 22:22:21 -08:00
2015-09-27 17:52:37 -07:00
2016-02-05 23:08:32 -08:00
def to_numpy ( value ) :
if type ( value ) is np . ndarray :
return value
elif type ( value ) is types . StringType :
return np . fromstring ( value , dtype = np . uint8 )
raise TypeError ( " Can ' t convert to numpy data " )
2016-02-08 22:11:49 -08:00
def process ( image , dirent , options ) :
2015-05-18 22:52:44 -07:00
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 options . extract :
print " %s : %s %s " % ( dirent , action , outfilename )
2016-02-07 11:13:49 -08:00
if not skip :
2016-02-08 22:11:49 -08:00
bytes = image . get_file ( dirent )
2016-02-07 11:13:49 -08:00
with open ( outfilename , " wb " ) as fh :
fh . write ( bytes )
2015-05-18 22:52:44 -07:00
else :
print dirent
2014-06-18 16:01:35 -07:00
2016-02-09 10:32:02 -08:00
def run ( ) :
2014-06-18 16:01:35 -07:00
import sys
2015-05-18 22:52:44 -07:00
import argparse
2014-06-18 16:01:35 -07:00
2015-05-18 22:52:44 -07:00
parser = argparse . ArgumentParser ( description = " Extract images off ATR format disks " )
parser . add_argument ( " -v " , " --verbose " , default = 0 , action = " count " )
2015-05-18 23:23:38 -07:00
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 " )
2015-05-19 16:46:40 -07:00
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 " )
2015-05-18 23:23:38 -07:00
parser . add_argument ( " files " , metavar = " ATR " , nargs = " + " , help = " an ATR image file [or a list of them] " )
2015-10-16 10:15:00 -07:00
parser . add_argument ( " -s " , " --segments " , action = " store_true " , default = False , help = " display segments " )
2015-05-18 22:52:44 -07:00
options , extra_args = parser . parse_known_args ( )
2015-05-19 15:16:19 -07:00
for filename in options . files :
with open ( filename , " rb " ) as fh :
2015-09-27 17:52:37 -07:00
data = fh . read ( )
2016-02-08 10:13:42 -08:00
image = None
2015-05-19 16:46:40 -07:00
try :
2016-02-07 22:22:21 -08:00
data = to_numpy ( data )
try :
header = AtrHeader ( data [ 0 : 16 ] )
2016-02-09 10:32:02 -08:00
for format in [ KBootImage , AtariDosDiskImage ] :
if options . verbose : print " trying " , format . __name__
2016-02-07 22:22:21 -08:00
try :
2016-02-09 10:23:06 -08:00
image = format ( data , filename )
2016-02-08 10:13:42 -08:00
print " %s : %s " % ( filename , image )
2016-02-07 22:22:21 -08:00
break
except InvalidDiskImage :
pass
2016-02-09 16:55:24 -08:00
except AtrError :
2016-02-07 22:22:21 -08:00
for format in [ AtariDosDiskImage ] :
try :
2016-02-08 10:13:42 -08:00
image = format ( data )
print " %s : %s " % ( filename , image )
2016-02-07 22:22:21 -08:00
break
except :
raise
#pass
2016-02-09 16:55:24 -08:00
except AtrError :
2016-02-09 10:32:02 -08:00
if options . verbose : print " %s : Doesn ' t look like a supported disk image " % filename
2015-09-25 23:02:35 -07:00
try :
2016-02-09 10:32:02 -08:00
image = AtariDosFile ( data )
2016-02-09 10:37:36 -08:00
print " %s : \n %s " % ( filename , image )
2015-09-25 23:02:35 -07:00
except InvalidBinaryFile :
2016-02-09 10:32:02 -08:00
if options . verbose : print " %s : Doesn ' t look like an XEX either " % filename
2015-05-19 16:47:23 -07:00
continue
2016-02-08 10:13:42 -08:00
if image is None :
2016-02-09 10:23:06 -08:00
image = BootDiskImage ( data , filename )
2015-10-16 10:15:00 -07:00
if options . segments :
2016-02-08 10:13:42 -08:00
image . parse_segments ( )
print " \n " . join ( [ str ( a ) for a in image . segments ] )
elif image . files or options . force :
for dirent in image . files :
2015-05-19 16:46:40 -07:00
try :
2016-02-08 22:11:49 -08:00
process ( image , dirent , options )
2015-05-19 16:46:40 -07:00
except FileNumberMismatchError164 :
print " Error 164: %s " % str ( dirent )
except ByteNotInFile166 :
print " Invalid sector for: %s " % str ( dirent )
2015-05-18 22:52:44 -07:00
2016-02-09 10:32:02 -08:00
if __name__ == " __main__ " :
run ( )