2018-05-31 17:53:00 +00:00
#!/usr/bin/env python3
2019-02-13 00:13:04 +00:00
# (c) 2018-9 by 4am
2018-05-31 17:53:00 +00:00
# MIT-licensed
import argparse
import binascii
import bitarray # https://pypi.org/project/bitarray/
import collections
2018-09-06 19:20:34 +00:00
import json
2018-05-31 17:53:00 +00:00
import itertools
2018-06-07 14:26:47 +00:00
import os
2018-05-31 17:53:00 +00:00
2019-02-13 00:13:04 +00:00
__version__ = " 2.0-alpha " # https://semver.org
2019-02-17 20:30:09 +00:00
__date__ = " 2019-02-17 "
2018-05-31 17:53:00 +00:00
__progname__ = " wozardry "
__displayname__ = __progname__ + " " + __version__ + " by 4am ( " + __date__ + " ) "
# domain-specific constants defined in .woz specification
2018-06-02 14:23:11 +00:00
kWOZ1 = b " WOZ1 "
2019-02-13 00:13:04 +00:00
kWOZ2 = b " WOZ2 "
2018-06-02 14:23:11 +00:00
kINFO = b " INFO "
kTMAP = b " TMAP "
kTRKS = b " TRKS "
2019-02-15 19:49:03 +00:00
kWRIT = b " WRIT "
2018-06-02 14:23:11 +00:00
kMETA = b " META "
2018-05-31 17:53:00 +00:00
kBitstreamLengthInBytes = 6646
2018-09-08 02:30:30 +00:00
kLanguages = ( " English " , " Spanish " , " French " , " German " , " Chinese " , " Japanese " , " Italian " , " Dutch " , " Portuguese " , " Danish " , " Finnish " , " Norwegian " , " Swedish " , " Russian " , " Polish " , " Turkish " , " Arabic " , " Thai " , " Czech " , " Hungarian " , " Catalan " , " Croatian " , " Greek " , " Hebrew " , " Romanian " , " Slovak " , " Ukrainian " , " Indonesian " , " Malay " , " Vietnamese " , " Other " )
2018-06-02 14:23:11 +00:00
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+ " )
2018-05-31 17:53:00 +00:00
# strings and things, for print routines and error messages
sEOF = " Unexpected EOF "
sBadChunkSize = " Bad chunk size "
2018-06-02 14:23:11 +00:00
dNoYes = { False : " no " , True : " yes " }
tQuarters = ( " .00 " , " .25 " , " .50 " , " .75 " )
2019-02-15 19:49:03 +00:00
tDiskType = { ( 1 , 1 , False ) : " 5.25-inch (140K) " ,
( 2 , 1 , False ) : " 3.5-inch (400K) " ,
( 2 , 2 , False ) : " 3.5-inch (800K) " ,
( 2 , 2 , True ) : " 3.5-inch (1.44MB) " }
2019-02-18 01:51:42 +00:00
tBootSectorFormat = ( " unknown " , " 16-sector " , " 13-sector " , " hybrid 13- and 16-sector " )
2018-05-31 17:53:00 +00:00
# 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
2019-02-13 00:13:04 +00:00
class WozHeaderError_NoWOZMarker ( WozHeaderError ) : pass
2018-05-31 17:53:00 +00:00
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
2018-06-02 14:23:11 +00:00
class WozINFOFormatError_BadCreator ( WozINFOFormatError ) : pass
2019-02-13 00:13:04 +00:00
class WozINFOFormatError_BadDiskSides ( WozINFOFormatError ) : pass
class WozINFOFormatError_BadBootSectorFormat ( WozINFOFormatError ) : pass
class WozINFOFormatError_BadOptimalBitTiming ( WozINFOFormatError ) : pass
2018-05-31 17:53:00 +00:00
class WozTMAPFormatError ( WozFormatError ) : pass
class WozTMAPFormatError_BadTRKS ( WozTMAPFormatError ) : pass
class WozTRKSFormatError ( WozFormatError ) : pass
class WozMETAFormatError ( WozFormatError ) : pass
class WozMETAFormatError_DuplicateKey ( WozFormatError ) : pass
2018-06-02 14:23:11 +00:00
class WozMETAFormatError_BadValue ( WozFormatError ) : pass
2018-05-31 17:53:00 +00:00
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
2019-02-15 19:49:03 +00:00
from_uint8 = from_uint32
2018-05-31 17:53:00 +00:00
def to_uint32 ( b ) :
return b . to_bytes ( 4 , byteorder = " little " )
def to_uint16 ( b ) :
return b . to_bytes ( 2 , byteorder = " little " )
def to_uint8 ( b ) :
return b . to_bytes ( 1 , byteorder = " little " )
2019-02-15 19:49:03 +00:00
def is_booleanish ( v ) :
if type ( v ) is str :
try :
return is_booleanish ( int ( v ) )
except :
2019-02-16 01:05:26 +00:00
return v . lower ( ) in ( " true " , " false " , " yes " , " no " , " 1 " , " 0 " )
2019-02-15 19:49:03 +00:00
elif type ( v ) is bytes :
try :
return is_booleanish ( int . from_bytes ( v , byteorder = " little " ) )
except :
return False
return v in ( 0 , 1 )
def from_booleanish ( v , errorClass , errorString ) :
raise_if ( not is_booleanish ( v ) , errorClass , errorString % v )
if type ( v ) is str :
2019-02-16 01:05:26 +00:00
return v . lower ( ) in ( " true " , " yes " , " 1 " )
2019-02-15 19:49:03 +00:00
elif type ( v ) is bytes :
return v == b " \x01 "
return v == 1
def is_intish ( v ) :
if type ( v ) is str :
try :
int ( v )
return True
except :
return False
if type ( v ) is bytes :
try :
int . from_bytes ( v , byteorder = " little " )
return True
except :
return False
return type ( v ) is int
def from_intish ( v , errorClass , errorString ) :
raise_if ( not is_intish ( v ) , errorClass , errorString % v )
if type ( v ) is str :
return int ( v )
elif type ( v ) is bytes :
return int . from_bytes ( v , byteorder = " little " )
return v
2018-05-31 17:53:00 +00:00
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
2018-07-23 18:44:19 +00:00
2018-05-31 17:53:00 +00:00
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
def rewind ( self , bit_count ) :
self . bit_index - = 1
if self . bit_index < 0 :
self . bit_index = self . bit_count - 1
self . revolutions - = 1
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
2018-10-26 01:52:21 +00:00
class WozDiskImage :
def __init__ ( self ) :
2019-02-13 00:13:04 +00:00
self . woz_version = None
2018-10-26 01:52:21 +00:00
self . tmap = [ 0xFF ] * 160
2018-05-31 17:53:00 +00:00
self . tracks = [ ]
2019-02-15 19:49:03 +00:00
self . writ = None
2018-10-26 01:52:21 +00:00
self . info = collections . OrderedDict ( )
self . meta = collections . OrderedDict ( )
def track_num_to_half_phase ( self , track_num ) :
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 )
return int ( track_num * 4 )
2018-05-31 17:53:00 +00:00
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.) """
2018-10-26 01:52:21 +00:00
half_phase = self . track_num_to_half_phase ( track_num )
trk_id = self . tmap [ half_phase ]
if trk_id == 0xFF : return None
return self . tracks [ trk_id ]
def clean ( self ) :
""" removes tracks from self.tracks that are not referenced from self.tmap, and adjusts remaining self.tmap indices """
i = 0
while i < len ( self . tracks ) :
if i not in self . tmap :
del self . tracks [ i ]
for adjust in range ( len ( self . tmap ) ) :
if ( self . tmap [ adjust ] > = i ) and ( self . tmap [ adjust ] != 0xFF ) :
self . tmap [ adjust ] - = 1
else :
i + = 1
def add ( self , half_phase , track ) :
trk_id = len ( self . tracks )
self . tracks . append ( track )
self . tmap [ half_phase ] = trk_id
if half_phase :
self . tmap [ half_phase - 1 ] = trk_id
if half_phase < 159 :
self . tmap [ half_phase + 1 ] = trk_id
def add_track ( self , track_num , track ) :
self . add ( self . track_num_to_half_phase ( track_num ) , track )
def remove ( self , half_phase ) :
if self . tmap [ half_phase ] == 0xFF : return False
self . tmap [ half_phase ] = 0xFF
self . clean ( )
return True
def remove_track ( self , track_num ) :
""" removes given track, returns True if anything was actually removed, or False if track wasn ' t found. track_num can be 0..40 in 0.25 increments (0, 0.25, 0.5, 0.75, 1, &c.) """
return self . remove ( self . track_num_to_half_phase ( track_num ) )
def from_json ( self , json_string ) :
j = json . loads ( json_string )
root = [ x for x in j . keys ( ) ] . pop ( )
self . meta . update ( j [ root ] [ " meta " ] )
def to_json ( self ) :
j = { " woz " : { " info " : self . info , " meta " : self . meta } }
return json . dumps ( j , indent = 2 )
2018-05-31 17:53:00 +00:00
2018-06-02 14:23:11 +00:00
class WozValidator :
def validate_info_version ( self , version ) :
2019-02-15 19:49:03 +00:00
""" |version| can be str, bytes, or int. returns same value as int """
version = from_intish ( version , WozINFOFormatError_BadVersion , " Unknown version (expected numeric value, found %s ) " )
2019-02-13 00:13:04 +00:00
if self . woz_version == 1 :
2019-02-15 19:49:03 +00:00
raise_if ( version != 1 , WozINFOFormatError_BadVersion , " Unknown version (expected 1, found %s ) " % version )
2019-02-13 00:13:04 +00:00
else :
2019-02-15 19:49:03 +00:00
raise_if ( version < 2 , WozINFOFormatError_BadVersion , " Unknown version (expected 2 or more, found %s ) " % version )
return version
2018-06-02 14:23:11 +00:00
def validate_info_disk_type ( self , disk_type ) :
2019-02-15 19:49:03 +00:00
""" |disk_type| can be str, bytes, or int. returns same value as int """
disk_type = from_intish ( disk_type , WozINFOFormatError_BadDiskType , " Unknown disk type (expected numeric value, found %s ) " )
raise_if ( disk_type not in ( 1 , 2 ) , WozINFOFormatError_BadDiskType , " Unknown disk type (expected 1 or 2, found %s ) " % disk_type )
return disk_type
2018-06-02 14:23:11 +00:00
def validate_info_write_protected ( self , write_protected ) :
2019-02-15 19:49:03 +00:00
""" |write_protected| can be str, bytes, or int. returns same value as bool """
return from_booleanish ( write_protected , WozINFOFormatError_BadWriteProtected , " Unknown write protected flag (expected Boolean value, found %s ) " )
2018-06-02 14:23:11 +00:00
def validate_info_synchronized ( self , synchronized ) :
2019-02-15 19:49:03 +00:00
""" |synchronized| can be str, bytes, or int. returns same value as bool """
return from_booleanish ( synchronized , WozINFOFormatError_BadSynchronized , " Unknown synchronized flag (expected Boolean value, found %s ) " )
2018-06-02 14:23:11 +00:00
def validate_info_cleaned ( self , cleaned ) :
2019-02-15 19:49:03 +00:00
""" |cleaned| can be str, bytes, or int. returns same value as bool """
return from_booleanish ( cleaned , WozINFOFormatError_BadCleaned , " Unknown cleaned flag (expected Boolean value, found %s ) " )
2018-06-02 14:23:11 +00:00
def validate_info_creator ( self , creator_as_bytes ) :
raise_if ( len ( creator_as_bytes ) > 32 , WozINFOFormatError_BadCreator , " Creator is longer than 32 bytes " )
try :
2019-02-17 20:30:09 +00:00
return creator_as_bytes . decode ( " UTF-8 " ) . strip ( )
2018-06-02 14:23:11 +00:00
except :
raise_if ( True , WozINFOFormatError_BadCreator , " Creator is not valid UTF-8 " )
def encode_info_creator ( self , creator_as_string ) :
creator_as_bytes = creator_as_string . encode ( " UTF-8 " ) . ljust ( 32 , b " " )
self . validate_info_creator ( creator_as_bytes )
return creator_as_bytes
2019-02-13 00:13:04 +00:00
def validate_info_disk_sides ( self , disk_sides ) :
2019-02-15 19:49:03 +00:00
""" |disk_sides| can be str, bytes, or int. returns same value as int """
2019-02-13 00:13:04 +00:00
# assumes WOZ version 2 or later
2019-02-15 19:49:03 +00:00
disk_sides = from_intish ( disk_sides , WozINFOFormatError_BadDiskSides , " Bad disk sides (expected numeric value, found %s ) " )
2019-02-13 00:13:04 +00:00
if self . info [ " disk_type " ] == 1 : # 5.25-inch disk
2019-02-15 19:49:03 +00:00
raise_if ( disk_sides != 1 , WozINFOFormatError_BadDiskSides , " Bad disk sides (expected 1 for a 5.25-inch disk, found %s ) " )
2019-02-13 00:13:04 +00:00
elif self . info [ " disk_type " ] == 2 : # 3.5-inch disk
2019-02-15 19:49:03 +00:00
raise_if ( disk_sides not in ( 1 , 2 ) , WozINFOFormatError_BadDiskSides , " Bad disk sides (expected 1 or 2 for a 3.5-inch disk, found %s ) " % disk_sides )
return disk_sides
2019-02-13 00:13:04 +00:00
def validate_info_boot_sector_format ( self , boot_sector_format ) :
2019-02-15 19:49:03 +00:00
""" |boot_sector_format| can be str, bytes, or int. returns same value as int """
2019-02-13 00:13:04 +00:00
# assumes WOZ version 2 or later
2019-02-15 19:49:03 +00:00
boot_sector_format = from_intish ( boot_sector_format , WozINFOFormatError_BadBootSectorFormat , " Bad boot sector format (expected numeric value, found %s ) " )
2019-02-13 00:13:04 +00:00
if self . info [ " disk_type " ] == 1 : # 5.25-inch disk
2019-02-15 19:49:03 +00:00
raise_if ( boot_sector_format not in ( 0 , 1 , 2 , 3 ) , WozINFOFormatError_BadBootSectorFormat , " Bad boot sector format (expected 0,1,2,3 for a 5.25-inch disk, found %s ) " % boot_sector_format )
2019-02-13 00:13:04 +00:00
elif self . info [ " disk_type " ] == 2 : # 3.5-inch disk
2019-02-15 19:49:03 +00:00
raise_if ( boot_sector_format != 0 , WozINFOFormatError_BadBootSectorFormat , " Bad boot sector format (expected 0 for a 3.5-inch disk, found %s ) " % boot_sector_format )
return boot_sector_format
2019-02-13 00:13:04 +00:00
def validate_info_optimal_bit_timing ( self , optimal_bit_timing ) :
2019-02-15 19:49:03 +00:00
""" |optimal_bit_timing| can be str, bytes, or int. returns same value as int """
2019-02-13 00:13:04 +00:00
# assumes WOZ version 2 or later
2019-02-15 19:49:03 +00:00
optimal_bit_timing = from_intish ( optimal_bit_timing , WozINFOFormatError_BadOptimalBitTiming , " Bad optimal bit timing (expected numeric value, found %s ) " )
2019-02-13 00:13:04 +00:00
if self . info [ " disk_type " ] == 1 : # 5.25-inch disk
raise_if ( optimal_bit_timing not in range ( 24 , 41 ) , WozINFOFormatError_BadOptimalBitTiming , " Bad optimal bit timing (expected 24-40 for a 5.25-inch disk, found %s ) " % optimal_bit_timing )
elif self . info [ " disk_type " ] == 2 : # 3.5-inch disk
raise_if ( optimal_bit_timing not in range ( 8 , 25 ) , WozINFOFormatError_BadOptimalBitTiming , " Bad optimal bit timing (expected 8-24 for a 3.5-inch disk, found %s ) " % optimal_bit_timing )
2019-02-15 19:49:03 +00:00
return optimal_bit_timing
def validate_info_required_ram ( self , required_ram ) :
""" |required_ram| can be str, bytes, or int. returns same value as int """
# assumes WOZ version 2 or later
required_ram = from_intish ( required_ram , WozINFOFormatError_BadOptimalBitTiming , " Bad required RAM (expected numeric value, found %s ) " )
return required_ram
2019-02-13 00:13:04 +00:00
2018-06-02 14:23:11 +00:00
def validate_metadata ( self , metadata_as_bytes ) :
try :
metadata = metadata_as_bytes . decode ( " UTF-8 " )
except :
raise WozMETAFormatError ( " Metadata is not valid UTF-8 " )
2018-07-23 18:44:19 +00:00
2018-06-02 14:23:11 +00:00
def decode_metadata ( self , metadata_as_bytes ) :
self . validate_metadata ( metadata_as_bytes )
return metadata_as_bytes . decode ( " UTF-8 " )
def validate_metadata_value ( self , value ) :
raise_if ( " \t " in value , WozMETAFormatError_BadValue , " Invalid metadata value (contains tab character) " )
raise_if ( " \n " in value , WozMETAFormatError_BadValue , " Invalid metadata value (contains linefeed character) " )
raise_if ( " | " in value , WozMETAFormatError_BadValue , " Invalid metadata value (contains pipe character) " )
2018-07-23 18:44:19 +00:00
2018-06-02 14:23:11 +00:00
def validate_metadata_language ( self , language ) :
raise_if ( language and ( language not in kLanguages ) , WozMETAFormatError_BadLanguage , " Invalid metadata language " )
def validate_metadata_requires_ram ( self , requires_ram ) :
raise_if ( requires_ram and ( requires_ram not in kRequiresRAM ) , WozMETAFormatError_BadRAM , " Invalid metadata requires_ram " )
def validate_metadata_requires_machine ( self , requires_machine ) :
raise_if ( requires_machine and ( requires_machine not in kRequiresMachine ) , WozMETAFormatError_BadMachine , " Invalid metadata requires_machine " )
2018-10-26 01:52:21 +00:00
class WozReader ( WozDiskImage , WozValidator ) :
2018-05-31 17:53:00 +00:00
def __init__ ( self , filename = None , stream = None ) :
2018-10-26 01:52:21 +00:00
WozDiskImage . __init__ ( self )
self . filename = filename
2018-06-02 14:23:11 +00:00
with stream or open ( filename , " rb " ) as f :
2018-05-31 17:53:00 +00:00
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 )
2019-02-15 19:49:03 +00:00
elif chunk_id == kWRIT :
self . __process_writ ( data )
2018-05-31 17:53:00 +00:00
elif chunk_id == kMETA :
self . __process_meta ( data )
if crc :
2018-06-02 14:23:11 +00:00
raise_if ( crc != binascii . crc32 ( b " " . join ( all_data ) ) & 0xffffffff , WozCRCError , " Bad CRC " )
2018-05-31 17:53:00 +00:00
def __process_header ( self , data ) :
2019-02-13 00:13:04 +00:00
raise_if ( data [ : 4 ] not in ( kWOZ1 , kWOZ2 ) , WozHeaderError_NoWOZMarker , " Magic string ' WOZ1 ' or ' WOZ2 ' not present at offset 0 " )
self . woz_version = int ( data [ 3 ] ) - 0x30
2018-05-31 17:53:00 +00:00
raise_if ( data [ 4 ] != 0xFF , WozHeaderError_NoFF , " Magic byte 0xFF not present at offset 4 " )
2018-06-02 14:23:11 +00:00
raise_if ( data [ 5 : 8 ] != b " \x0A \x0D \x0A " , WozHeaderError_NoLF , " Magic bytes 0x0A0D0A not present at offset 5 " )
2018-05-31 17:53:00 +00:00
def __process_info ( self , data ) :
2019-02-15 19:49:03 +00:00
self . info [ " version " ] = self . validate_info_version ( data [ 0 ] ) # int
self . info [ " disk_type " ] = self . validate_info_disk_type ( data [ 1 ] ) # int
self . info [ " write_protected " ] = self . validate_info_write_protected ( data [ 2 ] ) # boolean
self . info [ " synchronized " ] = self . validate_info_synchronized ( data [ 3 ] ) # boolean
self . info [ " cleaned " ] = self . validate_info_cleaned ( data [ 4 ] ) # boolean
2019-02-17 20:30:09 +00:00
self . info [ " creator " ] = self . validate_info_creator ( data [ 5 : 37 ] ) # string
2019-02-15 19:49:03 +00:00
if self . info [ " version " ] > = 2 :
self . info [ " disk_sides " ] = self . validate_info_disk_sides ( data [ 37 ] ) # int
self . info [ " boot_sector_format " ] = self . validate_info_boot_sector_format ( data [ 38 ] ) # int
self . info [ " optimal_bit_timing " ] = self . validate_info_optimal_bit_timing ( data [ 39 ] ) # int
compatible_hardware_bitfield = from_uint16 ( data [ 40 : 42 ] )
compatible_hardware_list = [ ]
for offset in range ( 9 ) :
if compatible_hardware_bitfield & ( 1 << offset ) :
compatible_hardware_list . append ( kRequiresMachine [ offset ] )
self . info [ " compatible_hardware " ] = compatible_hardware_list
self . info [ " required_ram " ] = self . validate_info_required_ram ( data [ 42 : 44 ] )
2019-02-13 00:13:04 +00:00
self . info [ " largest_track " ] = from_uint16 ( data [ 44 : 46 ] )
2018-05-31 17:53:00 +00:00
def __process_tmap ( self , data ) :
self . tmap = list ( data )
def __process_trks ( self , data ) :
2019-02-13 00:13:04 +00:00
if self . info [ " version " ] == 1 :
self . __process_trks_v1 ( data )
else :
self . __process_trks_v2 ( data )
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_trks_v1 ( self , data ) :
2018-05-31 17:53:00 +00:00
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 ) )
2019-02-15 19:49:03 +00:00
def __process_trks_v2 ( self , data ) :
for trk in range ( 160 ) :
i = trk * 8
starting_block = from_uint16 ( data [ i : i + 2 ] )
raise_if ( starting_block in ( 1 , 2 ) , WozTRKSFormatError , " TRKS TRK %d starting_block out of range (expected 3+ or 0, found %s ) " % ( trk , starting_block ) )
block_count = from_uint16 ( data [ i + 2 : i + 4 ] )
bit_count = from_uint32 ( data [ i + 4 : i + 8 ] )
if starting_block == 0 :
raise_if ( block_count != 0 , WozTRKSFormatError , " TRKS unused TRK %d block_count must be 0 (found %s ) " % ( trk , block_count ) )
raise_if ( bit_count != 0 , WozTRKSFormatError , " TRKS unused TRK %d bit_count must be 0 (found %s ) " % ( trk , bit_count ) )
break
bits_index_into_data = 1280 + ( starting_block - 3 ) * 512
raw_bytes = data [ bits_index_into_data : bits_index_into_data + block_count * 512 ]
bits = bitarray . bitarray ( endian = " big " )
bits . frombytes ( raw_bytes )
self . tracks . append ( WozTrack ( bits , bit_count ) )
def __process_writ ( self , data ) :
self . writ = data
2018-06-02 14:23:11 +00:00
def __process_meta ( self , metadata_as_bytes ) :
metadata = self . decode_metadata ( metadata_as_bytes )
for line in metadata . split ( " \n " ) :
2018-05-31 17:53:00 +00:00
if not line : continue
2018-06-02 14:23:11 +00:00
columns_raw = line . split ( " \t " )
2018-05-31 17:53:00 +00:00
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 " :
2018-06-02 14:23:11 +00:00
list ( map ( self . validate_metadata_language , values ) )
2018-05-31 17:53:00 +00:00
elif key == " requires_ram " :
2018-06-02 14:23:11 +00:00
list ( map ( self . validate_metadata_requires_ram , values ) )
2018-05-31 17:53:00 +00:00
elif key == " requires_machine " :
2018-06-02 14:23:11 +00:00
list ( map ( self . validate_metadata_requires_machine , values ) )
2018-05-31 17:53:00 +00:00
self . meta [ key ] = len ( values ) == 1 and values [ 0 ] or tuple ( values )
2018-10-26 01:52:21 +00:00
class WozWriter ( WozDiskImage , WozValidator ) :
2019-02-15 19:49:03 +00:00
def __init__ ( self , woz_version , creator ) :
2018-10-26 01:52:21 +00:00
WozDiskImage . __init__ ( self )
2019-02-15 19:49:03 +00:00
self . woz_version = woz_version
self . info [ " version " ] = woz_version
2019-02-15 22:30:14 +00:00
self . validate_info_version ( woz_version )
2018-05-31 17:53:00 +00:00
self . info [ " disk_type " ] = 1
self . info [ " write_protected " ] = False
self . info [ " synchronized " ] = False
self . info [ " cleaned " ] = False
self . info [ " creator " ] = creator
2019-02-15 22:30:14 +00:00
self . encode_info_creator ( creator )
self . info [ " disk_sides " ] = 1
self . info [ " boot_sector_format " ] = 0
self . info [ " optimal_bit_timing " ] = 32
self . info [ " compatible_hardware " ] = [ ]
self . info [ " required_ram " ] = 0
2018-09-06 19:20:34 +00:00
2018-05-31 17:53:00 +00:00
def build_info ( self ) :
chunk = bytearray ( )
chunk . extend ( kINFO ) # chunk ID
chunk . extend ( to_uint32 ( 60 ) ) # chunk size (constant)
2018-06-02 14:23:11 +00:00
version_raw = to_uint8 ( self . info [ " version " ] )
self . validate_info_version ( version_raw )
disk_type_raw = to_uint8 ( self . info [ " disk_type " ] )
self . validate_info_disk_type ( disk_type_raw )
write_protected_raw = to_uint8 ( self . info [ " write_protected " ] )
self . validate_info_write_protected ( write_protected_raw )
synchronized_raw = to_uint8 ( self . info [ " synchronized " ] )
self . validate_info_synchronized ( synchronized_raw )
cleaned_raw = to_uint8 ( self . info [ " cleaned " ] )
self . validate_info_cleaned ( cleaned_raw )
creator_raw = self . encode_info_creator ( self . info [ " creator " ] )
2019-02-15 19:49:03 +00:00
chunk . extend ( version_raw ) # 1 byte, 1 or 2
chunk . extend ( disk_type_raw ) # 1 byte, 1=5.25 inch, 2=3.5 inch
chunk . extend ( write_protected_raw ) # 1 byte, 0=no, 1=yes
chunk . extend ( synchronized_raw ) # 1 byte, 0=no, 1=yes
chunk . extend ( cleaned_raw ) # 1 byte, 0=no, 1=yes
chunk . extend ( creator_raw ) # 32 bytes, UTF-8 encoded string
if self . woz_version == 1 :
chunk . extend ( b " \x00 " * 23 ) # 23 bytes of unused space
else :
disk_sides_raw = to_uint8 ( self . info [ " disk_sides " ] )
self . validate_info_disk_sides ( disk_sides_raw )
boot_sector_format_raw = to_uint8 ( self . info [ " boot_sector_format " ] )
self . validate_info_boot_sector_format ( boot_sector_format_raw )
optimal_bit_timing_raw = to_uint8 ( self . info [ " optimal_bit_timing " ] )
self . validate_info_optimal_bit_timing ( optimal_bit_timing_raw )
compatible_hardware_bitfield = 0
for offset in range ( 9 ) :
if kRequiresMachine [ offset ] in self . info [ " compatible_hardware " ] :
compatible_hardware_bitfield | = ( 1 << offset )
compatible_hardware_raw = to_uint16 ( compatible_hardware_bitfield )
required_ram_raw = to_uint16 ( self . info [ " required_ram " ] )
2019-02-19 19:31:46 +00:00
if self . tracks :
largest_bit_count = max ( [ track . bit_count for track in self . tracks ] )
largest_block_count = ( ( ( largest_bit_count + 7 ) / / 8 ) + 511 ) / / 512
else :
largest_block_count = 0
2019-02-15 19:49:03 +00:00
largest_track_raw = to_uint16 ( largest_block_count )
chunk . extend ( disk_sides_raw ) # 1 byte, 1 or 2
chunk . extend ( boot_sector_format_raw ) # 1 byte, 0,1,2,3
chunk . extend ( optimal_bit_timing_raw ) # 1 byte
chunk . extend ( compatible_hardware_raw ) # 2 bytes, bitfield
chunk . extend ( required_ram_raw ) # 2 bytes
chunk . extend ( largest_track_raw ) # 2 bytes
chunk . extend ( b " \x00 " * 14 ) # 14 bytes of unused space
2018-05-31 17:53:00 +00:00
return chunk
2018-07-23 18:44:19 +00:00
2018-05-31 17:53:00 +00:00
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
2018-07-23 18:44:19 +00:00
2018-05-31 17:53:00 +00:00
def build_trks ( self ) :
2019-02-15 19:49:03 +00:00
if self . woz_version == 1 :
return self . build_trks_v1 ( )
else :
return self . build_trks_v2 ( )
def build_trks_v1 ( self ) :
2018-05-31 17:53:00 +00:00
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
2018-06-02 14:23:11 +00:00
chunk . extend ( b " \x00 " * ( 6646 - len ( raw_bytes ) ) ) # padding to 6646 bytes
2018-05-31 17:53:00 +00:00
chunk . extend ( to_uint16 ( len ( raw_bytes ) ) ) # bytes used
chunk . extend ( to_uint16 ( track . bit_count ) ) # bit count
2018-06-02 14:23:11 +00:00
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
2018-05-31 17:53:00 +00:00
return chunk
2019-02-15 19:49:03 +00:00
def build_trks_v2 ( self ) :
starting_block = 3
trk_chunk = bytearray ( )
bits_chunk = bytearray ( )
for track in self . tracks :
# get bitstream as bytes and pad to multiple of 512
padded_bytes = track . bits . tobytes ( )
padded_bytes + = ( 512 - ( len ( padded_bytes ) % 512 ) ) * b " \x00 "
trk_chunk . extend ( to_uint16 ( starting_block ) )
block_size = len ( padded_bytes ) / / 512
starting_block + = block_size
trk_chunk . extend ( to_uint16 ( block_size ) )
trk_chunk . extend ( to_uint32 ( track . bits . length ( ) ) )
bits_chunk . extend ( padded_bytes )
for i in range ( len ( self . tracks ) , 160 ) :
trk_chunk . extend ( to_uint16 ( 0 ) )
trk_chunk . extend ( to_uint16 ( 0 ) )
trk_chunk . extend ( to_uint32 ( 0 ) )
chunk = bytearray ( )
chunk . extend ( kTRKS ) # chunk ID
chunk . extend ( to_uint32 ( len ( trk_chunk ) + len ( bits_chunk ) ) )
chunk . extend ( trk_chunk )
chunk . extend ( bits_chunk )
return chunk
def build_writ ( self ) :
chunk = bytearray ( )
if self . writ :
chunk . extend ( kWRIT ) # chunk ID
chunk . extend ( to_uint32 ( len ( self . writ ) ) ) # chunk size
chunk . extend ( self . writ )
return chunk
2018-05-31 17:53:00 +00:00
def build_meta ( self ) :
2018-06-02 14:23:11 +00:00
if not self . meta : return b " "
2018-07-25 18:21:46 +00:00
meta_tmp = { }
2018-06-02 14:23:11 +00:00
for key , value_raw in self . meta . items ( ) :
if type ( value_raw ) == str :
values = [ value_raw ]
2018-07-23 18:44:19 +00:00
else :
values = value_raw
2018-07-25 18:21:46 +00:00
meta_tmp [ key ] = values
2018-06-02 14:23:11 +00:00
list ( map ( self . validate_metadata_value , values ) )
if key == " language " :
list ( map ( self . validate_metadata_language , values ) )
elif key == " requires_ram " :
list ( map ( self . validate_metadata_requires_ram , values ) )
elif key == " requires_machine " :
list ( map ( self . validate_metadata_requires_machine , values ) )
data = b " \x0A " . join (
2018-05-31 17:53:00 +00:00
[ k . encode ( " UTF-8 " ) + \
2018-06-02 14:23:11 +00:00
b " \x09 " + \
2018-07-25 18:21:46 +00:00
" | " . join ( v ) . encode ( " UTF-8 " ) \
2019-02-15 19:49:03 +00:00
for k , v in meta_tmp . items ( ) ] ) + b ' \x0A '
2018-05-31 17:53:00 +00:00
chunk = bytearray ( )
chunk . extend ( kMETA ) # chunk ID
chunk . extend ( to_uint32 ( len ( data ) ) ) # chunk size
chunk . extend ( data )
return chunk
2018-07-23 18:44:19 +00:00
2018-05-31 17:53:00 +00:00
def build_head ( self , crc ) :
chunk = bytearray ( )
2019-02-15 19:49:03 +00:00
if self . woz_version == 1 :
chunk . extend ( kWOZ1 ) # magic bytes
else :
chunk . extend ( kWOZ2 ) # magic bytes
2018-06-02 14:23:11 +00:00
chunk . extend ( b " \xFF \x0A \x0D \x0A " ) # more magic bytes
2018-05-31 17:53:00 +00:00
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 ( )
2019-02-15 19:49:03 +00:00
writ = self . build_writ ( ) # will be zero-length if no WRIT chunk
meta = self . build_meta ( ) # will be zero-length if no META chunk
crc = binascii . crc32 ( info + tmap + trks + writ + meta )
2018-05-31 17:53:00 +00:00
head = self . build_head ( crc )
stream . write ( head )
stream . write ( info )
stream . write ( tmap )
stream . write ( trks )
2019-02-15 19:49:03 +00:00
stream . write ( writ )
2018-05-31 17:53:00 +00:00
stream . write ( meta )
#---------- command line interface ----------
class BaseCommand :
def __init__ ( self , name ) :
self . name = name
def setup ( self , subparser , description = None , epilog = None , help = " .woz disk image " , formatter_class = argparse . HelpFormatter ) :
self . parser = subparser . add_parser ( self . name , description = description , epilog = epilog , formatter_class = formatter_class )
self . parser . add_argument ( " file " , help = help )
self . parser . set_defaults ( action = self )
def __call__ ( self , args ) :
self . woz_image = WozReader ( args . file )
class CommandVerify ( BaseCommand ) :
def __init__ ( self ) :
BaseCommand . __init__ ( self , " verify " )
def setup ( self , subparser ) :
BaseCommand . setup ( self , subparser ,
description = " Verify file structure and metadata of a .woz disk image (produces no output unless a problem is found) " )
class CommandDump ( BaseCommand ) :
kWidth = 30
def __init__ ( self ) :
BaseCommand . __init__ ( self , " dump " )
def setup ( self , subparser ) :
BaseCommand . setup ( self , subparser ,
description = " Print all available information and metadata in a .woz disk image " )
def __call__ ( self , args ) :
BaseCommand . __call__ ( self , args )
self . print_tmap ( )
self . print_meta ( )
self . print_info ( )
def print_info ( self ) :
2019-02-15 19:49:03 +00:00
info = self . woz_image . info
info_version = info [ " version " ]
print ( " INFO: File format version: " . ljust ( self . kWidth ) , " %d " % info_version )
disk_type = info [ " disk_type " ]
disk_sides = info_version > = 2 and info [ " disk_sides " ] or 1
large_disk = disk_sides == 2 and info [ " largest_track " ] > = 0x20
print ( " INFO: Disk type: " . ljust ( self . kWidth ) , tDiskType [ ( disk_type , disk_sides , large_disk ) ] )
print ( " INFO: Write protected: " . ljust ( self . kWidth ) , dNoYes [ info [ " write_protected " ] ] )
print ( " INFO: Tracks synchronized: " . ljust ( self . kWidth ) , dNoYes [ info [ " synchronized " ] ] )
print ( " INFO: Weakbits cleaned: " . ljust ( self . kWidth ) , dNoYes [ info [ " cleaned " ] ] )
print ( " INFO: Creator: " . ljust ( self . kWidth ) , info [ " creator " ] )
if info_version == 1 : return
if disk_type == 1 : # 5.25-inch disk
boot_sector_format = info [ " boot_sector_format " ]
print ( " INFO: Boot sector format: " . ljust ( self . kWidth ) , " %s ( %s ) " % ( boot_sector_format , tBootSectorFormat [ boot_sector_format ] ) )
default_bit_timing = 32
else : # 3.5-inch disk
print ( " INFO: Disk sides: " . ljust ( self . kWidth ) , disk_sides )
default_bit_timing = 16
optimal_bit_timing = info [ " optimal_bit_timing " ]
print ( " INFO: Optimal bit timing: " . ljust ( self . kWidth ) , optimal_bit_timing ,
optimal_bit_timing == default_bit_timing and " (standard) " or
optimal_bit_timing < default_bit_timing and " (fast) " or " (slow) " )
compatible_hardware_list = info [ " compatible_hardware " ]
if not compatible_hardware_list :
print ( " INFO: Compatible hardware: " . ljust ( self . kWidth ) , " unknown " )
else :
print ( " INFO: Compatible hardware: " . ljust ( self . kWidth ) , compatible_hardware_list [ 0 ] )
for value in compatible_hardware_list [ 1 : ] :
print ( " INFO: " . ljust ( self . kWidth ) , value )
ram = info [ " required_ram " ]
print ( " INFO: Required RAM: " . ljust ( self . kWidth ) , ram and " %s K " % ram or " unknown " )
print ( " INFO: Largest track: " . ljust ( self . kWidth ) , info [ " largest_track " ] , " blocks " )
2018-05-31 17:53:00 +00:00
def print_tmap ( self ) :
2019-02-15 22:00:14 +00:00
if self . woz_image . info [ " disk_type " ] == 1 :
self . print_tmap_525 ( )
else :
self . print_tmap_35 ( )
def print_tmap_525 ( self ) :
2018-05-31 17:53:00 +00:00
i = 0
for trk , i in zip ( self . woz_image . tmap , itertools . count ( ) ) :
if trk != 0xFF :
print ( ( " TMAP: Track %d %s " % ( i / 4 , tQuarters [ i % 4 ] ) ) . ljust ( self . kWidth ) , " TRKS %d " % ( trk ) )
2019-02-15 22:00:14 +00:00
def print_tmap_35 ( self ) :
track_num = 0
side_num = 0
for trk in self . woz_image . tmap :
if trk != 0xFF :
print ( ( " TMAP: Track %d , Side %d " % ( track_num , side_num ) ) . ljust ( self . kWidth ) , " TRKS %d " % ( trk ) )
side_num = 1 - side_num
if not side_num :
track_num + = 1
2018-05-31 17:53:00 +00:00
def print_meta ( self ) :
if not self . woz_image . meta : return
for key , values in self . woz_image . meta . items ( ) :
if type ( values ) == str :
values = [ values ]
print ( ( " META: " + key + " : " ) . ljust ( self . kWidth ) , values [ 0 ] )
for value in values [ 1 : ] :
print ( " META: " . ljust ( self . kWidth ) , value )
2018-09-06 19:20:34 +00:00
class CommandExport ( BaseCommand ) :
def __init__ ( self ) :
BaseCommand . __init__ ( self , " export " )
def setup ( self , subparser ) :
BaseCommand . setup ( self , subparser ,
description = " Export (as JSON) all information and metadata from a .woz disk image " )
def __call__ ( self , args ) :
BaseCommand . __call__ ( self , args )
print ( self . woz_image . to_json ( ) )
class WriterBaseCommand ( BaseCommand ) :
def __call__ ( self , args ) :
BaseCommand . __call__ ( self , args )
self . args = args
2019-02-15 19:49:03 +00:00
self . output = WozWriter ( self . woz_image . info . get ( " version " , 2 ) ,
self . woz_image . info . get ( " creator " , __displayname__ ) )
2018-09-06 19:20:34 +00:00
self . output . tmap = self . woz_image . tmap
self . output . tracks = self . woz_image . tracks
2019-02-15 22:30:14 +00:00
self . output . info . update ( self . woz_image . info )
2019-02-15 19:49:03 +00:00
self . output . writ = self . woz_image . writ
2018-09-06 19:20:34 +00:00
self . output . meta = self . woz_image . meta . copy ( )
self . update ( )
tmpfile = args . file + " .ardry "
with open ( tmpfile , " wb " ) as f :
self . output . write ( f )
2019-02-15 22:00:14 +00:00
# as a final sanity check, load and parse the temporary file we just created
# to help ensure we never create invalid .woz files
2019-02-15 19:49:03 +00:00
try :
2019-02-15 22:00:14 +00:00
global raise_if
raise_if = old_raise_if
2019-02-15 19:49:03 +00:00
WozReader ( tmpfile )
except Exception as e :
sys . stderr . write ( " WozInternalError: refusing to write an invalid .woz file (this is the developer ' s fault) \n " )
os . remove ( tmpfile )
raise Exception from e
2018-09-06 19:20:34 +00:00
os . rename ( tmpfile , args . file )
class CommandEdit ( WriterBaseCommand ) :
2018-05-31 17:53:00 +00:00
def __init__ ( self ) :
2018-09-06 19:20:34 +00:00
WriterBaseCommand . __init__ ( self , " edit " )
2018-05-31 17:53:00 +00:00
def setup ( self , subparser ) :
2018-09-06 19:20:34 +00:00
WriterBaseCommand . setup ( self ,
subparser ,
description = " Edit information and metadata in a .woz disk image " ,
epilog = """ Tips:
2018-05-31 17:53:00 +00:00
- Use repeated flags to edit multiple fields at once .
- Use " key: " with no value to delete a metadata field .
- Keys are case - sensitive .
- Some values have format restrictions ; read the . woz specification . """ ,
2018-09-06 19:20:34 +00:00
help = " .woz disk image (modified in place) " ,
formatter_class = argparse . RawDescriptionHelpFormatter )
2018-05-31 17:53:00 +00:00
self . parser . add_argument ( " -i " , " --info " , type = str , action = " append " ,
help = """ change information field.
INFO format is " key:value " .
Acceptable keys are disk_type , write_protected , synchronized , cleaned , creator , version .
2019-02-15 19:49:03 +00:00
Additional keys for WOZ2 files are disk_sides , required_ram , boot_sector_format , compatible_hardware , optimal_bit_timing .
2018-06-07 14:26:47 +00:00
Other keys are ignored .
For boolean fields , use " 1 " or " true " or " yes " for true , " 0 " or " false " or " no " for false . """ )
2018-05-31 17:53:00 +00:00
self . parser . add_argument ( " -m " , " --meta " , type = str , action = " append " ,
help = """ change metadata field.
META format is " key:value " .
Standard keys are title , subtitle , publisher , developer , copyright , version , language , requires_ram ,
requires_machine , notes , side , side_name , contributor , image_date . Other keys are allowed . """ )
2018-09-06 19:20:34 +00:00
def update ( self ) :
2019-02-15 19:49:03 +00:00
# 1st update version info field
for i in self . args . info or ( ) :
k , v = i . split ( " : " , 1 )
if k == " version " :
2019-02-15 22:30:14 +00:00
v = from_intish ( v , WozINFOFormatError_BadVersion , " Unknown version (expected numeric value, found %s ) " )
raise_if ( v not in ( 1 , 2 ) , WozINFOFormatError_BadVersion , " Unknown version (expected 1 or 2, found %s ) % v " )
self . output . woz_version = v
self . output . info [ " version " ] = v
2019-02-15 19:49:03 +00:00
# 2nd update disk_type info field
2018-09-06 19:20:34 +00:00
for i in self . args . info or ( ) :
2018-05-31 17:53:00 +00:00
k , v = i . split ( " : " , 1 )
2019-02-15 19:49:03 +00:00
if k == " disk_type " :
self . output . info [ " disk_type " ] = self . output . validate_info_disk_type ( v )
# then update all other info fields
for i in self . args . info or ( ) :
k , v = i . split ( " : " , 1 )
if k == " version " : continue
if k == " disk_type " : continue
if k == " write_protected " :
self . output . info [ k ] = self . output . validate_info_write_protected ( v )
elif k == " synchronized " :
self . output . info [ k ] = self . output . validate_info_synchronized ( v )
elif k == " cleaned " :
2019-02-16 01:05:26 +00:00
self . output . info [ k ] = self . output . validate_info_cleaned ( v )
2019-02-17 20:30:09 +00:00
elif k == " creator " :
self . output . info [ k ] = self . output . validate_info_creator ( self . output . encode_info_creator ( v ) )
2019-02-15 19:49:03 +00:00
if self . output . info [ " version " ] == 1 : continue
# remaining fields are only recognized in WOZ2 files (v2+ INFO chunk)
if k == " disk_sides " :
self . output . info [ k ] = self . output . validate_info_disk_sides ( v )
elif k == " boot_sector_format " :
self . output . info [ k ] = self . output . validate_info_boot_sector_format ( v )
elif k == " optimal_bit_timing " :
self . output . info [ k ] = self . output . validate_info_optimal_bit_timing ( v )
elif k == " required_ram " :
if v . lower ( ) . endswith ( " k " ) :
# forgive user for typing "128K" instead of "128"
v = v [ : - 1 ]
self . output . info [ k ] = self . output . validate_info_required_ram ( v )
elif k == " compatible_hardware " :
machines = v . split ( " | " )
for machine in machines :
self . output . validate_metadata_requires_machine ( machine )
self . output . info [ k ] = machines
2018-09-06 19:20:34 +00:00
# add all new metadata fields, and delete empty ones
for m in self . args . meta or ( ) :
2018-05-31 17:53:00 +00:00
k , v = m . split ( " : " , 1 )
v = v . split ( " | " )
if len ( v ) == 1 :
v = v [ 0 ]
if v :
2018-09-06 19:20:34 +00:00
self . output . meta [ k ] = v
elif k in self . output . meta . keys ( ) :
del self . output . meta [ k ]
2018-10-26 01:52:21 +00:00
class CommandRemove ( WriterBaseCommand ) :
def __init__ ( self ) :
WriterBaseCommand . __init__ ( self , " remove " )
def setup ( self , subparser ) :
WriterBaseCommand . setup ( self ,
subparser ,
2019-02-15 22:00:14 +00:00
description = " Remove tracks from a 5.25-inch .woz disk image " ,
2018-10-26 01:52:21 +00:00
epilog = """ Tips:
- Tracks can be 0. .40 in 0.25 increments ( 0 , 0.25 , 0.5 , 0.75 , 1 , & c . )
- Use repeated flags to remove multiple tracks at once .
- It is harmless to try to remove a track that doesn ' t exist. " " " ,
help = " .woz disk image (modified in place) " ,
formatter_class = argparse . RawDescriptionHelpFormatter )
self . parser . add_argument ( " -t " , " --track " , type = str , action = " append " ,
help = """ track to remove """ )
def update ( self ) :
2019-02-15 22:00:14 +00:00
raise_if ( self . output . info [ " disk_type " ] != 1 , WozINFOFormatError_BadDiskType , " Can not remove tracks from 3.5-inch disks " )
2018-10-26 01:52:21 +00:00
for i in self . args . track or ( ) :
self . output . remove_track ( float ( i ) )
2018-09-06 19:20:34 +00:00
class CommandImport ( WriterBaseCommand ) :
def __init__ ( self ) :
WriterBaseCommand . __init__ ( self , " import " )
def setup ( self , subparser ) :
WriterBaseCommand . setup ( self , subparser ,
2018-09-07 17:56:14 +00:00
description = " Import JSON file to update metadata in a .woz disk image " )
2018-09-06 19:20:34 +00:00
def update ( self ) :
self . output . from_json ( sys . stdin . read ( ) )
2018-07-23 18:44:19 +00:00
2018-05-31 17:53:00 +00:00
if __name__ == " __main__ " :
2018-06-02 14:23:11 +00:00
import sys
2019-02-15 22:00:14 +00:00
old_raise_if = raise_if
2019-02-15 22:30:14 +00:00
# raise_if = lambda cond, e, s="": cond and sys.exit("%s: %s" % (e.__name__, s))
2018-10-26 01:52:21 +00:00
cmds = [ CommandDump ( ) , CommandVerify ( ) , CommandEdit ( ) , CommandRemove ( ) , CommandExport ( ) , CommandImport ( ) ]
2018-05-31 17:53:00 +00:00
parser = argparse . ArgumentParser ( prog = __progname__ ,
description = """ A multi-purpose tool for manipulating .woz disk images.
See ' " " " + __progname__ + " " " <command> -h ' for help on individual commands . """ ,
formatter_class = argparse . RawDescriptionHelpFormatter )
parser . add_argument ( " -v " , " --version " , action = " version " , version = __displayname__ )
sp = parser . add_subparsers ( dest = " command " , help = " command " )
for command in cmds :
command . setup ( sp )
args = parser . parse_args ( )
args . action ( args )