2018-05-22 00:33:43 +00:00
#!/usr/bin/env python3
# (c) 2018 by 4am
# MIT-licensed
# portions from MIT-licensed defedd.py (c) 2014 by Paul Hagstrom
import binascii
import bitarray # https://pypi.org/project/bitarray/
import collections
import itertools
import sys
# domain-specific constants defined in .woz specification
kWOZ1 = b ' WOZ1 '
kINFO = b ' INFO '
kTMAP = b ' TMAP '
kTRKS = b ' TRKS '
kMETA = b ' META '
kBitstreamLengthInBytes = 6646
kLanguages = ( ' English ' , ' Spanish ' , ' French ' , ' German ' , ' Chinese ' , ' Japanese ' , ' Italian ' , ' Dutch ' , ' Portugese ' , ' Danish ' , ' Finnish ' , ' Norwegian ' , ' Swedish ' , ' Russian ' , ' Polish ' , ' Turkish ' , ' Arabic ' , ' Thai ' , ' Czech ' , ' Hungarian ' , ' Catalan ' , ' Croatian ' , ' Greek ' , ' Hebrew ' , ' Romanian ' , ' Slovak ' , ' Ukranian ' , ' Indonesian ' , ' Malay ' , ' Vietnamese ' , ' Other ' )
kRequiresRAM = ( ' 16K ' , ' 24K ' , ' 32K ' , ' 48K ' , ' 64K ' , ' 128K ' , ' 256K ' , ' 512K ' , ' 768K ' , ' 1M ' , ' 1.25M ' , ' 1.5M+ ' , ' Unknown ' )
kRequiresMachine = ( ' 2 ' , ' 2+ ' , ' 2e ' , ' 2c ' , ' 2e+ ' , ' 2gs ' , ' 2c+ ' , ' 3 ' , ' 3+ ' )
# strings and things, for print routines and error messages
sEOF = " Unexpected EOF "
sBadChunkSize = " Bad chunk size "
dNoYes = { False : ' no ' , True : ' yes ' }
tQuarters = ( ' .00 ' , ' .25 ' , ' .50 ' , ' .75 ' )
# errors that may be raised
class WozError ( Exception ) : pass # base class
class WozCRCError ( WozError ) : pass
class WozFormatError ( WozError ) : pass
class WozEOFError ( WozFormatError ) : pass
class WozHeaderError ( WozFormatError ) : pass
class WozHeaderError_NoWOZ1 ( WozHeaderError ) : pass
class WozHeaderError_NoFF ( WozHeaderError ) : pass
class WozHeaderError_NoLF ( WozHeaderError ) : pass
class WozINFOFormatError ( WozFormatError ) : pass
class WozINFOFormatError_BadVersion ( WozINFOFormatError ) : pass
class WozINFOFormatError_BadDiskType ( WozINFOFormatError ) : pass
class WozINFOFormatError_BadWriteProtected ( WozINFOFormatError ) : pass
class WozINFOFormatError_BadSynchronized ( WozINFOFormatError ) : pass
class WozINFOFormatError_BadCleaned ( WozINFOFormatError ) : pass
class WozTMAPFormatError ( WozFormatError ) : pass
class WozTMAPFormatError_BadTRKS ( WozTMAPFormatError ) : pass
class WozTRKSFormatError ( WozFormatError ) : pass
class WozMETAFormatError ( WozFormatError ) : pass
class WozMETAFormatError_DuplicateKey ( WozFormatError ) : pass
class WozMETAFormatError_BadLanguage ( WozFormatError ) : pass
class WozMETAFormatError_BadRAM ( WozFormatError ) : pass
class WozMETAFormatError_BadMachine ( WozFormatError ) : pass
def from_uint32 ( b ) :
return int . from_bytes ( b , byteorder = " little " )
from_uint16 = from_uint32
def to_uint32 ( b ) :
return b . to_bytes ( 4 , byteorder = " little " )
def to_uint16 ( b ) :
return b . to_bytes ( 2 , byteorder = " little " )
def raise_if ( cond , e , s = " " ) :
if cond : raise e ( s )
class Track :
def __init__ ( self , bits , bit_count ) :
self . bits = bits
while len ( self . bits ) > bit_count :
self . bits . pop ( )
self . bit_count = bit_count
self . bit_index = 0
self . revolutions = 0
def bit ( self ) :
b = self . bits [ self . bit_index ] and 1 or 0
self . bit_index + = 1
if self . bit_index > = self . bit_count :
self . bit_index = 0
self . revolutions + = 1
yield b
def nibble ( self ) :
b = 0
while b == 0 :
b = next ( self . bit ( ) )
n = 0x80
for bit_index in range ( 6 , - 1 , - 1 ) :
b = next ( self . bit ( ) )
n + = b << bit_index
yield n
2018-05-24 13:39:14 +00:00
def rewind ( self , bit_count ) :
self . bit_index - = 1
if self . bit_index < 0 :
self . bit_index = self . bit_count - 1
self . revolutions - = 1
2018-05-22 00:33:43 +00:00
def find ( self , sequence ) :
starting_revolutions = self . revolutions
seen = [ 0 ] * len ( sequence )
while ( self . revolutions < starting_revolutions + 2 ) :
del seen [ 0 ]
seen . append ( next ( self . nibble ( ) ) )
if tuple ( seen ) == tuple ( sequence ) : return True
return False
class WozTrack ( Track ) :
def __init__ ( self , bits , bit_count , splice_point = 0xFFFF , splice_nibble = 0 , splice_bit_count = 0 ) :
Track . __init__ ( self , bits , bit_count )
self . splice_point = splice_point
self . splice_nibble = splice_nibble
self . splice_bit_count = splice_bit_count
class DiskImage : # base class
def __init__ ( self , filename = None , stream = None ) :
raise_if ( not filename and not stream , WozError , " no input " )
self . filename = filename
self . tracks = [ ]
def seek ( self , track_num ) :
""" returns Track object for the given track, or None if the track is not part of this disk image. track_num can be 0..40 in 0.25 increments (0, 0.25, 0.5, 0.75, 1, &c.) """
return None
class EDDReader ( DiskImage ) :
def __init__ ( self , filename = None , stream = None ) :
DiskImage . __init__ ( self , filename , stream )
with stream or open ( filename , ' rb ' ) as f :
for i in range ( 137 ) :
raw_bytes = f . read ( 16384 )
raise_if ( len ( raw_bytes ) != 16384 , WozError , " Bad EDD file (did you image by quarter tracks?) " )
bits = bitarray . bitarray ( endian = " big " )
bits . frombytes ( raw_bytes )
self . tracks . append ( Track ( bits , 131072 ) )
def seek ( self , track_num ) :
if type ( track_num ) != float :
track_num = float ( track_num )
if track_num < 0.0 or \
track_num > 35.0 or \
track_num . as_integer_ratio ( ) [ 1 ] not in ( 1 , 2 , 4 ) :
raise WozError ( " Invalid track %s " % track_num )
trk_id = int ( track_num * 4 )
return self . tracks [ trk_id ]
class WozWriter :
def __init__ ( self , creator ) :
self . tracks = [ ]
self . tmap = [ 0xFF ] * 160
self . creator = creator
#self.meta = collections.OrderedDict()
def add_track ( self , track_num , track ) :
tmap_id = int ( track_num * 4 )
trk_id = len ( self . tracks )
self . tracks . append ( track )
self . tmap [ tmap_id ] = trk_id
if tmap_id :
self . tmap [ tmap_id - 1 ] = trk_id
if tmap_id < 159 :
self . tmap [ tmap_id + 1 ] = trk_id
def build_info ( self ) :
chunk = bytearray ( )
chunk . extend ( kINFO ) # chunk ID
chunk . extend ( to_uint32 ( 60 ) ) # chunk size
chunk . extend ( b ' \x01 ' ) # version = 1
chunk . extend ( b ' \x01 ' ) # disk type = 1 (5.25-inch)
chunk . extend ( b ' \x00 ' ) # write-protected = 0
chunk . extend ( b ' \x00 ' ) # synchronized = 0
chunk . extend ( b ' \x00 ' ) # cleaned = 0
chunk . extend ( self . creator . encode ( " UTF-8 " ) . ljust ( 32 , b " " ) ) # creator
chunk . extend ( b ' \x00 ' * 23 ) # reserved
return chunk
def build_tmap ( self ) :
chunk = bytearray ( )
chunk . extend ( kTMAP ) # chunk ID
chunk . extend ( to_uint32 ( 160 ) ) # chunk size
chunk . extend ( bytes ( self . tmap ) )
return chunk
def build_trks ( self ) :
chunk = bytearray ( )
chunk . extend ( kTRKS ) # chunk ID
chunk_size = len ( self . tracks ) * 6656
chunk . extend ( to_uint32 ( chunk_size ) ) # chunk size
for track in self . tracks :
raw_bytes = track . bits . tobytes ( )
chunk . extend ( raw_bytes ) # bitstream as raw bytes
chunk . extend ( b ' \x00 ' * ( 6646 - len ( raw_bytes ) ) ) # padding to 6646 bytes
chunk . extend ( to_uint16 ( len ( raw_bytes ) ) ) # bytes used
chunk . extend ( to_uint16 ( track . bit_count ) ) # bit count
chunk . extend ( b ' \xFF \xFF ' ) # splice point (none)
chunk . extend ( b ' \xFF ' ) # splice nibble (none)
chunk . extend ( b ' \xFF ' ) # splice bit count (none)
chunk . extend ( b ' \x00 \x00 ' ) # reserved
return chunk
def build_meta ( self ) :
return b ' '
def build_head ( self , crc ) :
chunk = bytearray ( )
chunk . extend ( kWOZ1 ) # magic bytes
chunk . extend ( b ' \xFF \x0A \x0D \x0A ' ) # more magic bytes
chunk . extend ( to_uint32 ( crc ) ) # CRC32 of rest of file (calculated in caller)
return chunk
def write ( self , stream ) :
info = self . build_info ( )
tmap = self . build_tmap ( )
trks = self . build_trks ( )
meta = self . build_meta ( )
crc = binascii . crc32 ( info + tmap + trks + meta )
head = self . build_head ( crc )
stream . write ( head )
stream . write ( info )
stream . write ( tmap )
stream . write ( trks )
stream . write ( meta )
class WozReader ( DiskImage ) :
def __init__ ( self , filename = None , stream = None ) :
DiskImage . __init__ ( self , filename , stream )
self . tmap = None
self . info = None
self . meta = None
with stream or open ( filename , ' rb ' ) as f :
header_raw = f . read ( 8 )
raise_if ( len ( header_raw ) != 8 , WozEOFError , sEOF )
self . __process_header ( header_raw )
crc_raw = f . read ( 4 )
raise_if ( len ( crc_raw ) != 4 , WozEOFError , sEOF )
crc = from_uint32 ( crc_raw )
all_data = [ ]
while True :
chunk_id = f . read ( 4 )
if not chunk_id : break
raise_if ( len ( chunk_id ) != 4 , WozEOFError , sEOF )
all_data . append ( chunk_id )
chunk_size_raw = f . read ( 4 )
raise_if ( len ( chunk_size_raw ) != 4 , WozEOFError , sEOF )
all_data . append ( chunk_size_raw )
chunk_size = from_uint32 ( chunk_size_raw )
data = f . read ( chunk_size )
raise_if ( len ( data ) != chunk_size , WozEOFError , sEOF )
all_data . append ( data )
if chunk_id == kINFO :
raise_if ( chunk_size != 60 , WozINFOFormatError , sBadChunkSize )
self . __process_info ( data )
elif chunk_id == kTMAP :
raise_if ( chunk_size != 160 , WozTMAPFormatError , sBadChunkSize )
self . __process_tmap ( data )
elif chunk_id == kTRKS :
self . __process_trks ( data )
elif chunk_id == kMETA :
self . __process_meta ( data )
if crc :
raise_if ( crc != binascii . crc32 ( b ' ' . join ( all_data ) ) & 0xffffffff , WozCRCError , " Bad CRC " )
def __process_header ( self , data ) :
raise_if ( data [ : 4 ] != kWOZ1 , WozHeaderError_NoWOZ1 , " Magic string ' WOZ1 ' not present at offset 0 " )
raise_if ( data [ 4 ] != 0xFF , WozHeaderError_NoFF , " Magic byte 0xFF not present at offset 4 " )
raise_if ( data [ 5 : 8 ] != b ' \x0A \x0D \x0A ' , WozHeaderError_NoLF , " Magic bytes 0x0A0D0A not present at offset 5 " )
def __process_info ( self , data ) :
version = data [ 0 ]
raise_if ( version != 1 , WozINFOFormatError_BadVersion , " Unknown version (expected 1, found %d ) " % version )
disk_type = data [ 1 ]
raise_if ( disk_type not in ( 1 , 2 ) , WozINFOFormatError_BadDiskType , " Unknown disk type (expected 1 or 2, found %d ) " % disk_type )
write_protected = data [ 2 ]
raise_if ( write_protected not in ( 0 , 1 ) , WozINFOFormatError_BadWriteProtected , " Unknown write protected flag (expected 0 or 1, found %d ) " % write_protected )
synchronized = data [ 3 ]
raise_if ( synchronized not in ( 0 , 1 ) , WozINFOFormatError_BadSynchronized , " Unknown synchronized flag (expected 0, or 1, found %d ) " % synchronized )
cleaned = data [ 4 ]
raise_if ( cleaned not in ( 0 , 1 ) , WozINFOFormatError_BadCleaned , " Unknown cleaned flag (expected 0 or 1, found %d ) " % cleaned )
try :
creator = data [ 5 : 37 ] . decode ( ' UTF-8 ' )
except :
raise WOZINFOFormatError ( " Creator is not valid UTF-8 " )
self . info = { " version " : version ,
" disk_type " : disk_type ,
" write_protected " : ( write_protected == 1 ) ,
" synchronized " : ( synchronized == 1 ) ,
" cleaned " : ( cleaned == 1 ) ,
" creator " : creator }
def __process_tmap ( self , data ) :
self . tmap = list ( data )
def __process_trks ( self , data ) :
i = 0
while i < len ( data ) :
raw_bytes = data [ i : i + kBitstreamLengthInBytes ]
raise_if ( len ( raw_bytes ) != kBitstreamLengthInBytes , WozEOFError , sEOF )
i + = kBitstreamLengthInBytes
bytes_used_raw = data [ i : i + 2 ]
raise_if ( len ( bytes_used_raw ) != 2 , WozEOFError , sEOF )
bytes_used = from_uint16 ( bytes_used_raw )
raise_if ( bytes_used > kBitstreamLengthInBytes , WozTRKSFormatError , " TRKS chunk %d bytes_used is out of range " % len ( self . tracks ) )
i + = 2
bit_count_raw = data [ i : i + 2 ]
raise_if ( len ( bit_count_raw ) != 2 , WozEOFError , sEOF )
bit_count = from_uint16 ( bit_count_raw )
i + = 2
splice_point_raw = data [ i : i + 2 ]
raise_if ( len ( splice_point_raw ) != 2 , WozEOFError , sEOF )
splice_point = from_uint16 ( splice_point_raw )
if splice_point != 0xFFFF :
raise_if ( splice_point > bit_count , WozTRKSFormatError , " TRKS chunk %d splice_point is out of range " % len ( self . tracks ) )
i + = 2
splice_nibble = data [ i ]
i + = 1
splice_bit_count = data [ i ]
if splice_point != 0xFFFF :
raise_if ( splice_bit_count not in ( 8 , 9 , 10 ) , WozTRKSFormatError , " TRKS chunk %d splice_bit_count is out of range " % len ( self . tracks ) )
i + = 3
bits = bitarray . bitarray ( endian = " big " )
bits . frombytes ( raw_bytes )
self . tracks . append ( WozTrack ( bits , bit_count , splice_point , splice_nibble , splice_bit_count ) )
for trk , i in zip ( self . tmap , itertools . count ( ) ) :
raise_if ( trk != 0xFF and trk > = len ( self . tracks ) , WozTMAPFormatError_BadTRKS , " Invalid TMAP entry: track %d %s points to non-existent TRKS chunk %d " % ( i / 4 , tQuarters [ i % 4 ] , trk ) )
def __process_meta ( self , data ) :
try :
metadata = data . decode ( ' UTF-8 ' )
except :
raise WozMETAFormatError ( " Metadata is not valid UTF-8 " )
self . meta = collections . OrderedDict ( )
for line in metadata . split ( ' \n ' ) :
if not line : continue
columns_raw = line . split ( ' \t ' )
raise_if ( len ( columns_raw ) != 2 , WozMETAFormatError , " Malformed metadata " )
key , value_raw = columns_raw
raise_if ( key in self . meta , WozMETAFormatError_DuplicateKey , " Duplicate metadata key %s " % key )
values = value_raw . split ( " | " )
if key == " language " :
for value in values :
raise_if ( value and ( value not in kLanguages ) , WozMETAFormatError_BadLanguage , " Invalid metadata language " )
elif key == " requires_ram " :
for value in values :
raise_if ( value and ( value not in kRequiresRAM ) , WozMETAFormatError_BadRAM , " Invalid metadata requires_ram " )
elif key == " requires_machine " :
for value in values :
raise_if ( value and ( value not in kRequiresMachine ) , WozMETAFormatError_BadMachine , " Invalid metadata requires_machine " )
self . meta [ key ] = values
def seek ( self , track_num ) :
""" returns Track object for the given track, or None if the track is not part of this disk image. track_num can be 0..40 in 0.25 increments (0, 0.25, 0.5, 0.75, 1, &c.) """
if type ( track_num ) != float :
track_num = float ( track_num )
if track_num < 0.0 or \
track_num > 40.0 or \
track_num . as_integer_ratio ( ) [ 1 ] not in ( 1 , 2 , 4 ) :
raise WozError ( " Invalid track %s " % track_num )
trk_id = self . tmap [ int ( track_num * 4 ) ]
if trk_id == 0xFF : return None
return self . tracks [ trk_id ]
# ----- quick info dump routines -----
kWidth = 20 # width of first column for printing info and metadata
def print_info ( wozimage ) :
print ( )
print ( " INFO " )
print ( " File format version: " . ljust ( kWidth ) , " %d " % wozimage . info [ " version " ] )
print ( " Disk type: " . ljust ( kWidth ) , ( " 5.25-inch " , " 3.5-inch " ) [ wozimage . info [ " disk_type " ] - 1 ] )
print ( " Write protected: " . ljust ( kWidth ) , dNoYes [ wozimage . info [ " write_protected " ] ] )
print ( " Track synchronized: " . ljust ( kWidth ) , dNoYes [ wozimage . info [ " synchronized " ] ] )
print ( " Weakbits cleaned: " . ljust ( kWidth ) , dNoYes [ wozimage . info [ " cleaned " ] ] )
print ( " Creator: " . ljust ( kWidth ) , wozimage . info [ " creator " ] )
def print_tmap ( wozimage ) :
print ( )
print ( " TMAP " )
i = 0
for tindex in wozimage . tmap :
if tindex != 0xFF :
print ( " Track %d %s -> TRKS %d " % ( i / 4 , tQuarters [ i % 4 ] , tindex ) )
i + = 1
def print_meta ( wozimage ) :
if not wozimage . meta : return
print ( )
print ( " META " )
for key , values in wozimage . meta . items ( ) :
print ( ( key + " : " ) . ljust ( kWidth ) , values [ 0 ] )
for value in values [ 1 : ] :
print ( " " . ljust ( kWidth ) , value )
if __name__ == " __main__ " :
for wozfile in sys . argv [ 1 : ] :
w = WozReader ( wozfile )
print_tmap ( w )
print_meta ( w )
print_info ( w )