Driv3rs/Driv3rs.py

469 lines
18 KiB
Python

# use unpack from struct and argv from sys
from struct import unpack; import argparse
import hashlib
import os.path
parser = argparse.ArgumentParser(
prog='Driv3rs.py',
formatter_class=argparse.RawDescriptionHelpFormatter,
description='''\
****************************************************************
* Driv3rs.py - A tiny Python script to help catalog drivers on *
* imaged Apple /// disks. By Mike Whalen, Michael Sternberg *
* and Paul Hagstrom. Please submit pull requests to Github. *
* https://github.com/thecompu/Driv3rs *
* Special Thanks to Rob Justice for Bug Fixes and Suggestions *
****************************************************************
''')
group = parser.add_mutually_exclusive_group()
group.add_argument("-rh", "--rawhex", action="store_true", help="Store hex values in CSV")
group.add_argument("-rd", "--rawdec", action="store_true", help="Store decimal values in CSV")
parser.add_argument("sosfile", help="sos.driver file to parse")
parser.add_argument("csvfile", help="csv file to create")
args = parser.parse_args()
# stick passed arugments into variables for later
disk_img = args.sosfile
output_csv = args.csvfile
# this function unpacks several read operations --
# text, binary, and single-byte. Each uses unpack from
# struct and attempts converts the resulting tuple into
# into either a string or integer, depending upon need.
def readUnpack(bytes, **options):
if options.get("type") == 't':
SOS = SOSfile.read(bytes)
text_unpacked = unpack('%ss' % bytes, SOS)
return ''.join(text_unpacked)
if options.get("type") == 'b':
SOS = SOSfile.read(bytes)
offset_unpacked = unpack ('< H', SOS)
return int('.'.join(str(x) for x in offset_unpacked))
if options.get("type") == '1':
SOS = SOSfile.read(bytes)
offset_unpacked = unpack ('< B', SOS)
return int(ord(SOS))
# this function takes a byte and performs bit operations
# to determine integer value. Outputs as a string. Used in
# the versioning DIB pullout. Values are stored in HEX.
def nibblize(byte, **options):
if options.get("direction") == 'high':
return str(int(hex(byte >> 4), 0))
if options.get("direction") == 'low':
return str(int(hex(byte & 0x0F), 0))
def device_type_string(byte):
retstr = ""
if byte & 128 == 128:
retstr = "Block Device"
if byte & 64 == 64:
retstr = retstr + ", Read-Write"
else:
retstr = retstr + ", Read-Only"
if byte & 32 == 32:
retstr = retstr + ", Removable"
else:
retstr = retstr + ", Non-Removable"
if byte & 16 == 16:
retstr = retstr + ", Formatter Present"
else:
retstr = retstr + ", No Formatter"
else:
retstr = "Character Device"
if byte & 64 == 64:
retstr = retstr + ", Write Allowed"
else:
retstr = retstr + ", Write Disabled"
if byte & 32 == 32:
retstr = retstr + ", Read Allowed"
else:
retstr = retstr + ", Read Disabled"
return retstr
# dictionary for device types and sub types.
dev_types ={273: 'Character Device, Write-Only, Formatter', # $11/$01
321: 'Character Device, Write-Only, RS232 Printer', # $41/$01
577: 'Character Device, Write-Only, Silentype', # $41/$02
833: 'Character Device, Write-Only, Parallel Printer', # $41/$03
323: 'Character Device, Write-Only, Sound Port', # $43/$01
353: 'Character Device, Read-Write, System Console', # $61/$01
354: 'Character Device, Read-Write, Graphics Screen', # $62/$01
355: 'Character Device, Read-Write, Onboard RS232', # $63/$01
356: 'Character Device, Read-Write, Parallel Card', # $64/$01
273: 'Disk /// Formatter', # $11/$01
481: 'Block Device, Disk ///', # $E1/$01
721: 'Block Device, ProFile', # $D1/$02
4337: 'Block Device, CFFA3000'} # $F1/$10
# Dictionary for known manufacturers.
# Apple Computer is a defined as a range from 1-31.
mfgs = {35: 'Quark Incorporated',
13107: 'Bob Consorti: ON THREE Inc.',
17491: 'David Schmidt',
21066: 'Rob Justice'}
# open SOS.DRIVER file to interrogate, then read first
# eight bytes and determine if file is actual SOS.DRIVER file.
# will be replaced with logic to read full disk images (PRODOS)
SOSfile = open(disk_img, 'rb')
filetype = readUnpack(8, type = 't')
if filetype == 'SOS DRVR':
print "Valid SOS.DRIVER file: {}".format(disk_img)
else:
print "INVALID SOS.DRIVER file: {}".format(disk_img)
exit()
# read two bytes immediately after "SOS DRVR" to determine jump
# to first major driver. Print out what's found. Start a count of
# found major drivers for upcoming loop and initalize a list to
# hold driver dictionaries.
rel_offset = readUnpack(2, type = 'b')
#print "The first relative offset value is", rel_offset, hex(rel_offset)
drivers = 0
drivers_list=[]
# loop that determines beginning of all major drivers in this particular
# SOS.DRIVER file, logs the offsets into a dictionary for later use.
# for each driver found, we:
# 1. Jump past the bytes indicating the comment length.
# 2. Look at the next two bytes and determine if they are xFFFF.
# 3. If not, place offset in dictionary.
# 4. When FFFF is encountered, we've reached the end of all drivers.
loop = True
while loop :
driver = {}
SOSfile.seek(rel_offset,1)
driver['comment_start'] = SOSfile.tell()
rel_offset = readUnpack(2, type = 'b')
if rel_offset == 0xFFFF:
loop = False
else :
drivers_list.append(driver)
SOSfile.seek(rel_offset,1)
rel_offset = readUnpack(2, type = 'b')
SOSfile.seek(rel_offset,1)
rel_offset = readUnpack(2, type = 'b')
# utilizing the offsets found, we now push through each Device information
# Block to log information about a driver. Comments therein.
# For nearly all entries, we write decimal values to our initial dictionaries
# then convert to desired output at csv time. Excluded would be ascii strings
# and the version numbers.
# first deal with comment length and comment text.
# comment length is two bytes (either the length in hex or 0x0000)
# log both to list/dictionary
for i in range(0,len(drivers_list)):
SOSfile.seek(drivers_list[i]['comment_start'],0)
comment_len = readUnpack(2, type = 'b')
drivers_list[i]['comment_len'] = comment_len
if comment_len != 0x0000:
comment_txt = readUnpack(comment_len, type = 't')
drivers_list[i]['comment_txt'] = comment_txt.replace('"', "''") # quotation marks will mess up csv
else:
drivers_list[i]['comment_txt'] = 'None'
# these two bytes are the intermediate offset value used to jump
# to the next major driver. This is useful for computing the md5 hash.
drivers_list[i]['next_driver'] = readUnpack(2, type = 'b')
# Officially, the link pointer is the beginning of the DIB.
# comments are optional. We log dib_start to indicate where the
# actual DIB for a driver begins.
drivers_list[i]['dib_start'] = SOSfile.tell()
# the link pointer is two bytes and points to the next DIB
# when there are multiples -- .D1, .D2, etc.
link_ptr = readUnpack(2, type = 'b')
drivers_list[i]['link_ptr'] = link_ptr
# entry field is two bytes pointing to the area in memory
# where the driver is placed during SOS bootup.
entry = readUnpack(2, type = 'b')
drivers_list[i]['entry'] = entry
# the name length and name are next. Name length is one byte.
# name field is _always_ 15 bytes long but name can be anything
# up to 15 bytes
name_len = readUnpack(1, type = '1')
drivers_list[i]['name_len'] = name_len
name = readUnpack(name_len, type = 't')
drivers_list[i]['name'] = name
SOSfile.seek(15 - name_len,1)
# flag byte determine whether a driver is active, inactive
# or shall be loaded on a page boundary. This is set up in
# the System Configuration Program
flag = readUnpack(1, type = '1')
drivers_list[i]['flag'] = flag
# if the driver is for a card that is loaded into a slot
# and that slot has been defined in the SCP and placed into
# the current SOS.DRIVER file, we log it here.
slot_num = readUnpack(1, type = '1')
drivers_list[i]['slot_num'] = slot_num
# the unit byte is concerned with the device number encountered.
# for the major drivers, it's always 0 and if there are other
# DIBs, such as for a driver that supports multiple disk drives,
# the unit byte is incremented each time by 1.
unit = readUnpack(1, type = '1')
drivers_list[i]['unit'] = unit
# dev_type is the type of device. We currently use a dict
# and populate the field accordingly. This dictionary was
# built from Apple's published Driver Writer's Manual.
# The type is determined via two bytes. The LSB is the sub-type
# and the MSB is the type.
drivers_list[i]['dev_type'] = readUnpack(1, type ='1')
drivers_list[i]['dev_subtype'] = readUnpack(1, type ='1')
# we skip the Filler byte ($19) as Apple reserved it.
SOSfile.seek(1,1)
# block_num refers to the number of logical blocks in a device
# it is not guaranteed to be populated with anything
# we log the block number defined or that the the device is
# a character device. otherwise Undefined
block_num = readUnpack(2, type = 'b')
drivers_list[i]['block_num'] = block_num
# the manufacturer byte was ill-defined at the time the driver
# writer's manual was published. 1 through 31 were reserved for
# Apple Computer. Others were supposed to get their codes from
# Apple. At the time we wrote this script, we used Apple devices
# and the CFFA3000 and populated a dictionary. This dictionary
# will get more k/v pairs as time goes on.
mfg = readUnpack(2, type = 'b')
drivers_list[i]['mfg']=mfg
# version bytes are integer values stored across two bytes.
# a nibble corresponds to a major version number, one of two minor
# version numbers, or a "further qualification" as Apple
# called it. The format is V-v0-v1-Q. Q was not well-defined.
# Basically if the value is 0xA, 0xB, or 0xE, then the Q
# corresponds to: Alpha, Beta, and Experimental. Otherwise,
# Q is merely a number. Q was worked out via the SCP.
ver_byte0 = readUnpack(1, type = '1')
ver_byte1 = readUnpack(1, type = '1')
V = nibblize(ver_byte1, direction = 'high')
v0 = nibblize(ver_byte1, direction = 'low')
v1 = nibblize(ver_byte0, direction = 'high')
Q = nibblize(ver_byte0, direction = 'low')
if Q == '10':
drivers_list[i]['version'] = V + '.' + v0 + v1 + ' Alpha'
elif Q == '11':
drivers_list[i]['version'] = V + '.' + v0 + v1 + ' Beta'
elif Q == '14':
drivers_list[i]['version'] = V + '.' + v0 + v1 + ' Experimental'
else:
drivers_list[i]['version'] = V + '.' + v0 + v1
# device configuration block length
dcb_length = readUnpack(2, type = 'b')
drivers_list[i]['dcb_length'] = dcb_length
# calculate an md5 hash of the entire driver and of just the code
# portion
SOSfile.seek(drivers_list[i]['comment_start'], 0)
# configuration bytes include the comment and parameters that can be changed in SCP
config_bytes = SOSfile.read(4 + drivers_list[i]['comment_len'] + drivers_list[i]['entry'])
# code bytes contain the region between the entry point and the next driver
code_bytes = SOSfile.read(drivers_list[i]['next_driver'] - drivers_list[i]['entry'])
# Hash just the code portion
code_md5 = hashlib.md5(code_bytes)
# Hash the whole driver, which will include both code and parameters
driver_md5 = hashlib.md5(config_bytes)
driver_md5.update(code_bytes)
# Store the resulting hash digest hex strings
drivers_list[i]['driver_md5'] = driver_md5.hexdigest()
drivers_list[i]['code_md5'] = code_md5.hexdigest()
# here we run a new loop to determine how many other DIBs exist
# under a major driver. This is primarily for drivers that are designed
# to support more than one device. for instance, the CFFA3000 is
# written to support eight drives (.D1 - .D8). otherwise, nothing
# else changes save the unit number, which is incremented by 1 for
# each device supported.
# generally we enter each major drive DIB and look at the link field.
# if the link field is not 0000, we know there are other DIBs.
# we can open up a new loop to run through the interior DIBs until
# we encounter a 0000 in a link field, then we stop.
for i in range(0,len(drivers_list)):
total_devs = 0
SOSfile.seek(drivers_list[i]['dib_start'],0)
sub_sub = ""
drivers_list[i]['subdrivers'] = sub_sub #initialize subdrivers dict k/v
sub_loop = True
while sub_loop:
total_devs = total_devs + 1
sub_link = readUnpack(2, type = 'b') #link to next DIB
if sub_link != 0x0000:
SOSfile.seek(drivers_list[i]['dib_start'] + sub_link,0) #link is from DIB start
SOSfile.seek(4,1) #skip over dib Entry pointer
subname_len = readUnpack(1, type = '1')
subname = readUnpack(subname_len, type = 't')
sub_temp = drivers_list[i]['subdrivers']
if sub_temp != "" and sub_link != 0x0000:
sub_temp = sub_temp + ' ' + '|' + ' ' + subname
drivers_list[i]['subdrivers'] = sub_temp
else:
drivers_list[i]['subdrivers'] = subname
SOSfile.seek(drivers_list[i]['dib_start'] + sub_link,0)
else:
sub_loop = False
drivers_list[i]['num_devices'] = total_devs
# closing the SOS.DRIVER file
SOSfile.close()
# here begins writing out the CSV file. the order is mainly
# structured like the structure in the Driver Writer's Manual.
# first, check if file exists and, if so, omit header
exists = os.path.exists(output_csv)
if exists == False:
csvout = open(output_csv, 'w')
csvout.write('SOS_DRIVER_FILE,comment_start,comment_len,comment_txt,' + \
'dib_start,link_ptr,entry,name_len,majorname,flag,slot_num,num_devices,subnames,unit,' +\
'dev_type_sub,block_num,mfg,version,dcb_length,driver_md5,code_md5\n')
else:
csvout = open(output_csv, 'a')
for i in range(0,len(drivers_list)):
csvout.write(disk_img + ',')
#comment start hex or decimal
if args.rawhex:
csvout.write(hex(drivers_list[i]['comment_start']) + ',')
else:
csvout.write(str(drivers_list[i]['comment_start']) + ',')
#comment length hex or decimal
if args.rawhex:
csvout.write(hex(drivers_list[i]['comment_len']) + ',')
else:
csvout.write(str(drivers_list[i]['comment_len']) + ',')
#comment
csvout.write('"' + drivers_list[i]['comment_txt'] + '"' + ',')
#dib_start hex or decimal
if args.rawhex:
csvout.write(hex(drivers_list[i]['dib_start']) + ',')
else:
csvout.write(str(drivers_list[i]['dib_start']) + ',')
#link_ptr hex or decimal
if args.rawhex:
csvout.write(hex(drivers_list[i]['link_ptr']) + ',')
else:
csvout.write(str(drivers_list[i]['link_ptr']) + ',')
#entry hex or decimal
if args.rawhex:
csvout.write(hex(drivers_list[i]['entry']) + ',')
else:
csvout.write(str(drivers_list[i]['entry']) + ',')
#name_len hex or decimal
if args.rawhex:
csvout.write(hex(drivers_list[i]['name_len']) + ',')
else:
csvout.write(str(drivers_list[i]['name_len']) + ',')
#name ascii only
csvout.write(drivers_list[i]['name'] + ',' )
#flag hex or decimal or ascii
if args.rawhex:
csvout.write(hex(drivers_list[i]['flag']) + ',')
elif args.rawdec:
csvout.write(str(drivers_list[i]['flag']) + ',')
else:
if drivers_list[i]['flag'] == 192:
csvout.write('"' + 'ACTIVE, Load on Boundary' + '"' ',')
elif drivers_list[i]['flag'] == 128:
csvout.write('ACTIVE' + ',')
else:
csvout.write('INACTIVE' + ',')
#slot_num hex or decimal
if args.rawhex:
csvout.write(hex(drivers_list[i]['slot_num']) + ',')
else:
csvout.write(str(drivers_list[i]['slot_num']) + ',')
#num_devices hex or decimal
if args.rawhex:
csvout.write(hex(drivers_list[i]['num_devices']) + ',')
else:
csvout.write(str(drivers_list[i]['num_devices']) + ',')
#sub_driver names ascii only
csvout.write(drivers_list[i]['subdrivers'] + ',' )
#unit hex or decimal
if args.rawhex:
csvout.write(hex(drivers_list[i]['unit']) + ',')
else:
csvout.write(str(drivers_list[i]['unit']) + ',')
#dev_type hex or decimal or ascii
if args.rawhex:
csvout.write(hex(drivers_list[i]['dev_type']) + ',')
elif args.rawdec:
csvout.write(str(drivers_list[i]['dev_type']) + ',')
else:
try:
csvout.write('"' + dev_types[drivers_list[i]['dev_subtype']*256 + drivers_list[i]['dev_type']] + '"' + ',')
except:
csvout.write('"' + device_type_string(drivers_list[i]['dev_type']) + '; Subtype '+ hex(drivers_list[i]['dev_subtype']) + '"' + ',')
#block_num hex or decimal
if args.rawhex:
csvout.write(hex(drivers_list[i]['block_num']) + ',')
elif args.rawdec:
csvout.write(str(drivers_list[i]['block_num']) + ',')
else:
csvout.write(str(drivers_list[i]['block_num']) + ',')
#mfg hex or decimal or ascii
# csvout.write(drivers_list[i]['mfg'] + ',')
if args.rawhex:
csvout.write(hex(drivers_list[i]['mfg']) + ',')
elif args.rawdec:
csvout.write(str(drivers_list[i]['mfg']) + ',')
else:
try:
csvout.write(mfgs[(drivers_list[i]['mfg'])] + ',')
except:
if 1 <= drivers_list[i]['mfg'] <= 31:
csvout.write('Apple Computer' + ',')
else:
csvout.write(hex(drivers_list[i]['mfg']) + ' (Unknown)' + ',')
#version -- leaving as standard output
csvout.write(str(drivers_list[i]['version']) + ',')
#dcb_length hex or decimal
if args.rawhex:
csvout.write(hex(drivers_list[i]['dcb_length']) + ',')
else:
csvout.write(str(drivers_list[i]['dcb_length']) + ',')
#md5 outputs left alone
csvout.write(
drivers_list[i]['driver_md5'] + ',' + \
drivers_list[i]['code_md5']
)
csvout.write('\n')
csvout.close()