Coverage for capasm/tools.py : 91%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/python3
2# -*- coding: utf-8 -*-
3#
4# This module contains the following tools for the CAPASM assembler:
5# - static methods to support regression testing
6# - capglo utilty to create global symbol class files from text files
7# - caplif utility to put assembled lex files into an import lif image file
8#
9# (c) 2020 Joachim Siebold
10#
11# This program is free software; you can redistribute it and/or
12# modify it under the terms of the GNU General Public License
13# as published by the Free Software Foundation; either version 2
14# of the License, or (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program; if not, write to the Free Software
23# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24#
25#--------------------------------------------------------------------------
26#
27# Changelog
28# 18.05.2020 jsi:
29# - start change log
30# 19.05.2020 jsi
31# - added clsSymClassGenerator class
32# - added capglo entry point
33# - removed main entry point
34# 22.05.2020 jsi
35# - raise custom exception on fatal error
36# 29.05.2020 jsi
37# - much refactoring
38# - regression test utilities rewritten
39# - removed mklex75 entry point
40# - added caplex and caplif entry points and the corresponding classes
41# 29.06.2020 jsi
42# - added caprom tool
43# 04.07.2020 jsi
44# - fix rom number checking
45# 26.07.2020 jsi
46# - catch I/O errors in caplglo generate method
47# 27.07.2020 jsi
48# - added capconv tool
49#
50import sys, argparse,os, codecs,re,contextlib
51from pathlib import Path
52from itertools import groupby
53from datetime import datetime
54from .assembler import clsLineScanner, parseFunc, capasmError, CAPASM_VERSION
56#
57# silently remove files, continue if they do not exist
58#
59def silentRemove(*args):
60 for fileName in args:
61 with contextlib.suppress(FileNotFoundError):
62 os.remove(fileName)
63 return False
64#
65# AsmSourceFileConverter class -----------------------------------------
66# This class converts a tokenized Series 80 assembler source file
67# which was for example extracted from a lif image file with "hpdir -extract"
68# or "lifget -r" to an ascii file. The input file format (HP-85 or
69# HP-87 style is auto detected
70#
71class clsAsmSourceFileConverter(object):
73 def __init__(self):
74 super().__init__()
75 self.__outFile__=None
76#
77# output comment
78#
79 def printComment(self,d):
80 self.__outFile__.write("! ")
81 for c in d:
82 self.__outFile__.write(chr(c))
83#
84# output label or blanks
85#
86 def printLabel(self,d,labelLength):
87 for i in range(0,labelLength):
88 if d is None:
89 self.__outFile__.write(" ")
90 else:
91 self.__outFile__.write(chr(d[i]))
92 self.__outFile__.write(" ")
93#
94# process opcode, operands and trailing comment
95#
96 def printOpcodeAndOperand(self,d):
97 operand=""
98 for i in range(0,3):
99 operand+=chr(d[i])
100 d=d[3:]
101 if operand in ["SBB","SBM","CMB","CMM","ANM","ADB","ADM", \
102 "PUB","PUM","POB","POM","LDB","LDM","STB","STM"]:
103 if chr(d[0]) in ["D","I"]:
104 operand+=chr(d[0])
105 d=d[1:]
106 else:
107 operand+=" "
108 else:
109 operand+=" "
110 i=4
111 self.__outFile__.write(operand)
112 self.__outFile__.write(" ")
113 for c in d:
114 if c== 0xFE:
115 for j in range(i,20):
116 self.__outFile__.write(" ")
117 self.__outFile__.write(" ! ")
118 continue
119 if c==0xFF:
120 continue
121 self.__outFile__.write(chr(c))
122 i+=1
124#
125 def convert(self,inputFileName,outputFileName):
126 print("")
127 print("Converting file "+inputFileName+" to "+outputFileName)
128 try:
129 infile=open(inputFileName,"rb")
130 asmBytes=infile.read()
131 infile.close()
132 except OSError:
133 raise capasmError("cannot open/read input file")
135 try:
136 self.__outFile__=open(outputFileName,"w")
137 except OSError:
138 raise capasmError("cannot create output file")
139#
140# detect file type and skip header
141#
142 k=len(asmBytes)
143 count=0
144 if asmBytes[6]==0x20:
145 is85=True
146 i=24
147 elif asmBytes[6]==0x10 or asmBytes[6]==0x50:
148 is85=False
149 i=32
150 else:
151 raise capasmError("Illegal input file")
152#
153# process records
154#
155 try:
156 while i < k:
157#
158# line number, HP87 has one more digit
159#
160 if not is85:
161 labelLength=8
162 h=asmBytes[i]
163 if h==10:
164 break
165 lineNumber="{:1x}{:02x}{:02x} ".format(asmBytes[i],\
166 asmBytes[i+1],asmBytes[i+2])
167 i+=1
168 else:
169 labelLength=6
170 if asmBytes[i]==0x99 and asmBytes[i+1]==0xA9:
171 break
172 lineNumber="{:02x}{:02x} ".format(asmBytes[i+1],asmBytes[i])
173 i+=2
174 count+=1
175#
176# store tokenized assembler statement in d
177#
178 l=asmBytes[i]
179 i+=1
180 d=bytearray(0)
181 self.__outFile__.write(lineNumber)
182 for j in range (0,l):
183 d.append(asmBytes[i])
184 i+=1
185 if d[-1]!=0x0E:
186 raise capasmError("Illegal End byte detected")
187 d=d[0:-1]
188#
189# comment line
190#
191 if d[0]==0xFE:
192 self.printComment(d[1:-1])
193 self.__outFile__.write("\n")
194 continue
195#
196# label
197#
198 if d[0]==0xFF:
199 d=d[1:]
200 self.printLabel(d,labelLength)
201 d=d[labelLength:]
202 else:
203 self.printLabel(None,labelLength)
204#
205# opcode, operands and trailing comment
206#
207 self.printOpcodeAndOperand(d)
208 self.__outFile__.write("\n")
209 self.__outFile__.close()
210 except OSError:
211 raise capasmError("cannot write to output file")
212 print("{:d} assembler source lines written to file {:s}".format(count,\
213 outputFileName))
214 return False
216#
217# SymbolFileConverter class ---------------------------------------------
218# This class converts a Series 80 binary global symbols file
219# which was for example extracted from a lif image file with "hpdir -extract"
220# or "lifget -r" to an ascii file
221#
222class clsSymbolFileConverter(object):
224 def __init__(self):
225 super().__init__()
226#
227# conversion method
228# The binary input file has a fixed record length of 256 bytes. Each
229# global symbol has an entry of 11 bytes length. An entry may span
230# a record boundary. A zero length record marks end of file.
231#
232 def convert(self,inputFileName,outputFileName):
234 tdict= {0: "EQU", 1: "DAD", 2:"DAD"}
235 print("")
236 print("Converting file "+inputFileName+" to "+outputFileName)
237 try:
238 infile=open(inputFileName,"rb")
239 gloBytes=infile.read()
240 infile.close()
241 except OSError:
242 raise capasmError("cannot open/read input file")
244 try:
245 outfile=open(outputFileName,"w")
246 except OSError:
247 raise capasmError("cannot create output file")
249 i=0
250 count=0
251 try:
252 while True:
253 header=gloBytes[i]
254#
255# check header byte 0xDF: normal entry, 0xCF: entry spans two records
256#
257 if not header in [0xDF, 0xCF]:
258 raise capasmError("illegal input file")
259 i+=1
260#
261# get length, if zero length then exit loop
262#
263 length=gloBytes[i]
264 if (length==0):
265 break
266 i+=1
267#
268# extract bytes of entry
269#
270 d=bytearray(0)
271#
272# if the entry spans two records, skip the header at the
273# beginning of the second line
274#
275 for j in range (0,length):
276 d.append(gloBytes[i])
277 i+=1
278 if i % 256 == 0:
279 i+=3
280#
281# get type of symbol
282#
283 typ=gloBytes[i]
284#
285# extract symbol name
286#
287 symName=""
288 ci=length-3
289 while d[ci]!=0:
290 symName+=chr(d[ci])
291 ci-=1
292#
293# get symbol value
294#
295 symValue=d[-2]*256+d[-1]
296 d=None
297 outfile.write("{:8s} {:3s} {:o}\n".format(symName,tdict[typ],\
298 symValue))
299 i+=1
300 count+=1
301 outfile.close()
302 except OSError:
303 raise capasmError("cannot write to output file")
304 print("{:d} symbols generated in file {:s}".format(count,outputFileName))
305 return False
306#
307#
308# SymCassGenerator class -------------------------------------------------
309#
310# An object of this class reads a global symbol file which must match
311# the syntax rules of the CAPASM assembler. Only comments, DEF and EQU
312# statements are allowed in this file.
313#
314# The object generates a Python script which a static class "globalSymbols"
315# to access global symbols for a specific machine. At the moment CAPASM
316# supports the script file names globals75.py, globals85.py, globals87.py.
317# The -m option of the assembler controls which file is used for the assembly.
318#
319# The program checks for duplicate global symbol definitions which exist
320# in the file globals75.txt. At the moment duplicate definitions overwrite
321# an existing definition. The reason for duplicateentries is under
322# investigation.
323#
324class clsSymClassGenerator(object):
326 SYM_DAD=0
327 SYM_EQU=1
329 def __init__(self):
330 super().__init__()
332#
333# generate method:
334# convert file with name inputFileName to a python file with a global
335# symbol class definition which has the name outputFileName
336#
337# Returns:
338# False: everything o.k.
339# True: errors or duplicates
340# Raises capasmError on i/o error
341#
342 def generate(self,inputFileName,outputFileName,labelLen=8):
344 lineScanner=clsLineScanner("!","!",'"')
345 symDict= { }
346 duplicates=0
347 errors=0
348 hasErrors=False
349 print("")
350 print("Processing file "+inputFileName)
351 try:
352 infile=codecs.open(inputFileName,"r",encoding="ISO-8859-1",\
353 errors="ignore")
354 except OSError:
355 raise capasmError("cannot open input file")
357 try:
358 outfile=open(outputFileName,"w")
359 except OSError:
360 raise capasmError("cannot create output file")
361#
362# Write global symbol class definition
363#
364 try:
365 outfile.write("#!/usr/bin/python3\n# -*- coding: utf-8 -*-\n")
366 outfile.write("#\n# Global symbols from file "+inputFileName+"\n")
367 outfile.write("# Autogenerated file, do not modify!\n")
368 outfile.write("#\n")
369 outfile.write("class globalSymbols():\n")
370 outfile.write("\n")
371 outfile.write(" symbols= {\n")
372#
373# Process lines
374#
375 lineCount=0
376 while True:
377 line=infile.readline()
378 if not line:
379 break
380 line=line.strip("\r\n")
381 lineCount+=1
382#
383# Scan line, we get a list of token:
384# - lineNumber (from source code file, if any, ignored here)
385# - label
386# - opcode
387# - list of operands which should consist only of the symbol value
388#
389 scannedLine=lineScanner.scanLine(line)
390 lineNumber=str(lineCount)
391#
392# Empty line
393#
394 if scannedLine[1]==None:
395 continue
396#
397# Comment
398#
399 symbolName=scannedLine[1].string
400 if symbolName[0]=="*" or symbolName=="!":
401 continue
402#
403# Check symbol name
404#
405 if parseFunc.parseLabel(symbolName,labelLen) is None:
406 print("Line: "+lineNumber+": "+line)
407 print("illegal symbol")
408 errors+=1
409 continue
410#
411# Check opcode, only "EQU" and "DAD" are allowed
412#
413 if scannedLine[2] is None:
414 print("Line: "+lineNumber+": "+line)
415 print("missing opcode")
416 errors+=1
417 continue
418 opCode= scannedLine[2].string
419 if opCode== "EQU":
420 opTyp=clsSymClassGenerator.SYM_EQU
421 elif opCode == "DAD":
422 opTyp=clsSymClassGenerator.SYM_DAD
423 else:
424 print("Line: "+lineNumber+": "+line)
425 print("illegal opcode")
426 errors+=1
427 continue
428#
429# Check value which must be a valid number
430#
431 if len(scannedLine[3])!=1:
432 print("Line: "+lineNumber+": "+line)
433 print("illegal label value")
434 errors+=1
435 continue
436 value=scannedLine[3][0].string
437 intValue=parseFunc.parseNumber(value)
438 if intValue==None:
439 print("Line: "+lineNumber+": "+line)
440 print("illegal label value")
441 errors+=1
442 continue
443 if intValue > 0xFFFF:
444 print("Line: "+lineNumber+": "+line)
445 print("illegal label value")
446 errors+=1
447 continue
448#
449# Check and print duplicates
450#
451 if symbolName in symDict.keys():
452 print("Line: "+lineNumber+": "+line)
453 ret=symDict[symbolName]
454 print("symbol redefined, first definition was at line: "+ \
455 ret[0]+" opcode: "+ret[1]+" value: "+ret[2])
456 outfile.write(' "'+symbolName+'" : ['+str(opTyp)+ \
457 ","+str(intValue)+"],\n")
458 duplicates+=1
459 else:
460 symDict[symbolName]=[lineNumber,opCode,value]
461 outfile.write(' "'+symbolName+'" : ['+str(opTyp)+ \
462 ","+str(intValue)+"],\n")
463#
464# All input line processed, write access method
465#
466 infile.close()
467 outfile.write(" }\n")
468 outfile.write(" @staticmethod\n")
469 outfile.write(" def get(name):\n")
470 outfile.write(" if name[0]=='=':\n")
471 outfile.write(" name=name[1:]\n")
472 outfile.write(" if name in globalSymbols.symbols.keys():\n")
473 outfile.write(" return globalSymbols.symbols[name]\n")
474 outfile.write(" else:\n")
475 outfile.write(" return None\n")
476 outfile.write("\n")
477 outfile.close()
478 except OSError:
479 raise capasmError("I/O Error while converting global symbols file")
480 print("Errors {:d}, duplicate entries {:d} ".format(errors,duplicates))
481#
482# return error condition
483#
484 hasErrors=(errors!=0) or (duplicates!=0)
485 return hasErrors
486#
487# Static classes for the comparision of files
488# Returns: [l1, l2, diffs]
489# l1: length of file 1
490# l2: length of file 2
491# diffs: list of differences, each member is a list of [pos, num] where
492# pos: byte position of difference
493# num: number of consecutive bytes that are different
494#
495# return None on file i/o error
496#
497#
498class fileDiff(object):
500 @staticmethod
501 def compareBin(fileName1, fileName2):
502 try:
503 name=fileName1
504 f=open(name,"rb")
505 f1=f.read()
506 f.close()
507 name=fileName2
508 f=open(name,"rb")
509 f2=f.read()
510 f.close()
511 l1=len(f1)
512 l2=len(f2)
513 diffs = [(next(g), len(list(g))+1)
514 for k, g in groupby(range(min(len(f1), len(f2))), \
515 key=lambda i: f1[i] != f2[i]) if k]
516 except (OSError,FileNotFoundError):
517 print("Can not read binary file: "+name)
518 return None
519 return l1,l2,diffs
521 @staticmethod
522 def compareAsc(fileName1, fileName2):
523 try:
524 name=fileName1
525 f=open(name,"r")
526 f1=[]
527 while True:
528 l=f.readline()
529 if not l:
530 break
531 f1.append(l.strip("\r\n"))
532 f.close()
533 name=fileName2
534 f=open(name,"r")
535 f2=[]
536 while True:
537 l=f.readline()
538 if not l:
539 break
540 f2.append(l.strip("\r\n"))
541 f.close()
542 l1=len(f1)
543 l2=len(f2)
544 diffs = [(next(g), len(list(g))+1)
545 for k, g in groupby(range(min(len(f1), len(f2))), \
546 key=lambda i: f1[i] != f2[i]) if k]
547 except (OSError,FileNotFoundError):
548 print("Can not read ascii file: "+name)
549 return None
550 return l1,l2,diffs
552 @staticmethod
553 def testBinFile(fileName1,fileName2):
554 ret=fileDiff.compareBin(fileName1,fileName2)
555 if ret is None:
556 return True
557 ret=fileDiff.postProc(fileName1, fileName2,ret)
558 return ret
560 @staticmethod
561 def testAscFile(fileName1, fileName2):
562 ret=fileDiff.compareAsc(fileName1,fileName2)
563 if ret is None:
564 return True
565 ret=fileDiff.postProc(fileName1, fileName2,ret)
566 return ret
568 @staticmethod
569 def postProc(fileName1,fileName2,ret):
570 notIdentical=False
571 l1,l2,diffs=ret
572 if l1 !=l2:
573 notIdentical=True
574 print(fileName1+"!="+fileName2+", file length differs: "\
575 +str(l1)+"!="+str(l2))
577 if len(diffs)!=0:
578 notIdentical= True
579 print(fileName1+"!="+fileName2+": compare failed at pos: ",end="")
580 print (diffs)
581 return notIdentical
583#
584# Class to generates Date/Time as BCD ---------------------------------------
585#
586class clsDateTime(object):
588 def __init__(self):
589 super().__init__()
590 now=datetime.now()
591 self.bcdYear= self.intToBcd(now.date().year-2000)
592 self.bcdMonth= self.intToBcd(now.date().month)
593 self.bcdDay= self.intToBcd(now.date().day)
594 self.bcdHour= self.intToBcd(now.time().hour)
595 self.bcdMin= self.intToBcd(now.time().minute)
596 self.bcdSec= self.intToBcd(now.time().second)
598 def intToBcd(self,value):
599 return (((int(value/10)%10)<<4)+(value%10))
600#
601# LIF item classes ----------------------------------------------------------
602#
603# This classes generate various items we need to create the structure of
604# LIF files and volumes
605#
606# Base class for all LIF items
607#
608class clsBinaryItem(object):
609 def __init__(self):
610 super().__init__()
611 self.__objectCode__= None
612#
613# get bytes of item
614#
615 def getBytes(self):
616 return self.__objectCode__
618#
619# get byte length of item
620#
621 def getLen(self):
622 return len(self.__objectCode__)
623#
624# LIF item: file content ------------------------------------------------
625#
626class clsObjectFile(clsBinaryItem):
628 def __init__(self,filename):
629 super().__init__()
630 try:
631 f=open(filename,"rb")
632 self.__objectCode__=f.read()
633 f.close()
634 except OSError:
635 raise capasmError("Can not read binary object file")
636#
637# LIF item: volume header, always 512 bytes long ------------------------
638#
639class clsVolumeHeader(clsBinaryItem):
641 def __init__(self):
642 super().__init__()
643 self.__dt__=clsDateTime()
645 def create(self,length,machine,isRegressionTest):
646#
647# for the HP75 we create a standard Volume header with 2 sectors
648#
649 if machine=="75":
650 self.__objectCode__=bytearray(512)
651 self.__objectCode__[11]=2 # start of directory
652#
653# for Series80 we create a 256 byte sector, because
654# the HP-85 emulator requires that for upload files
655 else:
656 self.__objectCode__=bytearray(256)
657 self.__objectCode__[11]=2 # start of directory
658 self.__objectCode__[0]=0x80 # LIF volume identifier
659 labelName="HFSLIF" # Volumen label, always HFSLIF
660 i=2
661 for c in labelName:
662 self.__objectCode__[i]=ord(c)
663 i+=1
664 self.__objectCode__[12]=0x10 # HP-3000 LIF identifier
665 self.__objectCode__[19]=0x01 # one sector in directory
666 self.__objectCode__[21]=0x01 # LIF version number
667#
668# Volume size
669#
670 self.__objectCode__[27]=0x01 # tracks per surface
671 self.__objectCode__[31]=0x01 # number of surfaces
672 length+=+512+255 # calculate length of volume in blocks
673 self.__objectCode__[32]=0
674 self.__objectCode__[33]= (length >> 24) & 0xFF
675 self.__objectCode__[34]= (length >> 16) & 0xFF
676 self.__objectCode__[35]= (length >> 8) & 0xFF
677#
678# Date and Time
679#
680 if not isRegressionTest:
681 self.__objectCode__[36]=self.__dt__.bcdYear
682 self.__objectCode__[37]=self.__dt__.bcdMonth
683 self.__objectCode__[38]=self.__dt__.bcdDay
684 self.__objectCode__[39]=self.__dt__.bcdHour
685 self.__objectCode__[40]=self.__dt__.bcdMin
686 self.__objectCode__[41]=self.__dt__.bcdSec
688#
689# LIF item: directory entry, always 32 bytes long -------------------------
690#
691class clsDirEntry(clsBinaryItem):
693 def __init__(self):
694 super().__init__()
695 self.__dt__=clsDateTime()
697 def createFileEntry(self,length,machine,filename,lexOnly,isRegressionTest):
698 origLength=length
699 self.__objectCode__=bytearray(32)
700 i=0
701 for c in filename: # LIF file name
702 self.__objectCode__[i]=ord(c)
703 i+=1
704 self.__objectCode__[8]=0x20 # pad file name
705 self.__objectCode__[9]=0x20
706 if machine =="75": # file type for HP-75: LEX75
707 self.__objectCode__[10]=0xE0
708 self.__objectCode__[11]=0x89
709 else: # file type for Series 80: BPGM binary P.
710 self.__objectCode__[10]=0xE0
711 self.__objectCode__[11]=0x08
713 if not lexOnly:
714 self.__objectCode__[15]=3 # start at sector 3
716 length+=255 # calculate length in blocks
717 self.__objectCode__[16]=0
718 self.__objectCode__[17]= (length >> 24) & 0xFF
719 self.__objectCode__[18]= (length >> 16) & 0xFF
720 self.__objectCode__[19]= (length >> 8) & 0xFF
721 if not isRegressionTest:
722 self.__objectCode__[20]=self.__dt__.bcdYear
723 self.__objectCode__[21]=self.__dt__.bcdMonth
724 self.__objectCode__[22]=self.__dt__.bcdDay
725 self.__objectCode__[23]=self.__dt__.bcdHour
726 self.__objectCode__[24]=self.__dt__.bcdMin
727 self.__objectCode__[25]=self.__dt__.bcdSec
728 self.__objectCode__[26]=0x80 # Implementing bytes
729 self.__objectCode__[27]=0x01
730 if machine =="75": # HP-75 Password
731 self.__objectCode__[28]=0x20
732 self.__objectCode__[29]=0x20
733 self.__objectCode__[30]=0x20
734 self.__objectCode__[31]=0x20
735 else: # Series 80 file and block length
736 self.__objectCode__[28]=origLength & 0xFF
737 self.__objectCode__[29]=(origLength >> 8) & 0xFF
738 self.__objectCode__[30]=0x00
739 self.__objectCode__[31]=0x01
741#
742# this creates an empty directory entry, file type is 0xFFFD
743#
744 def createNullEntry(self):
745 self.__objectCode__=bytearray(32)
746 self.__objectCode__[10]=0xFF
747 self.__objectCode__[11]=0xFF
749#
750# LIF item: HP-75 RAM file system header --------------------------------
751#
752# This header must be prepended to the file content.
753# Series 80 computers do not need that because they start with a NAM header
754#
755class clsFileHeader(clsBinaryItem):
757 def __init__(self):
758 super().__init__()
760 def create(self,length,filename):
761 self.__objectCode__=bytearray(18)
762 length=length+18 # length of file including header
763 low= length & 0xFF
764 high= length >> 8
765 self.__objectCode__[2]=low
766 self.__objectCode__[3]=high
767 self.__objectCode__[4]=0x8D # access bits, fake
768 self.__objectCode__[5]=0x4C # "L"
769 self.__objectCode__[6]=0xC7 # date and time faked
770 self.__objectCode__[7]=0xBA
771 self.__objectCode__[8]=0x7F
772 self.__objectCode__[9]=0xA0
773 i=10
774 for c in filename:
775 self.__objectCode__[i]=ord(c)
776 i+=1
777#
778# Check the filename which is used in the LIF directory entry
779#
780def makeLifFileName(fName,machine):
781 fName=fName.upper()
782 if len(fName)> 8:
783 raise capasmError("LIF filename: "+fName+" exceeds 8 characters")
784 if machine=="75":
785 match=re.fullmatch("[A-Z][A-Z0-9]*",fName)
786 else:
787 match=re.fullmatch("[A-Z][A-Z0-9_]*",fName) # allow underscores here
788 if not match:
789 raise capasmError("illegal LIF file name: "+fName)
790 fName=fName.ljust(8)
791 return fName
792#
793# LIF LEX and Image creator class ----------------------------------------
794#
795class clsLifCreator(object):
797 def __init__(self):
798 super().__init__()
799#
800# create LIF LEX files (lexOnly= True) from binary input file
801# - or -
802# create LIF import image (lexOnly=False) from binary input file
803#
804# Returns:
805# True: everything o.k.
806# Raises capasmError on i/o error or if the LIF directory file name is
807# illegal
808#
809 def create(self,binFileName,machine,outputFileName="",lifFileName="",
810 lexOnly=False):
811#
812# check if we run in regression test mode
813#
814 isRegressionTest=False
815 if os.getenv("CAPASMREGRESSIONTEST"):
816 isRegressionTest=True
817#
818# build name of file image or lex file if not specified
819#
820 if outputFileName=="":
821 if lexOnly:
822 outputFileName=Path(binFileName).with_suffix(".lex").name
823 else:
824 outputFileName=Path(binFileName).with_suffix(".dat").name
825#
826# build the file name for the LIF directory entry, if not specified
827#
828 if lifFileName=="":
829 if machine == "75":
830 fname=Path(binFileName).stem
831 else:
832 fname="WS_FILE"
833 else:
834 fname=lifFileName
835 lifFileName=makeLifFileName(fname,machine)
836#
837# read object file into memory
838#
839 objectFile=clsObjectFile(binFileName)
840#
841# create the RAM file header for the HP-75
842#
843 fileLength=objectFile.getLen()
844 if machine == "75":
845 fileHeader=clsFileHeader()
846 fileHeader.create(fileLength,lifFileName)
847 fileLength+=18
848#
849# create directory Entry
850#
851 dirEntry=clsDirEntry()
852 dirEntry.createFileEntry(fileLength,machine,lifFileName, \
853 lexOnly,isRegressionTest)
854#
855# create LIF volume header
856#
857 volHeader=clsVolumeHeader()
858 volHeader.create(fileLength, machine, isRegressionTest)
859#
860# write LIF image or LEX file
861#
862 try:
863 imgFile=open(outputFileName,"wb")
864#
865# if LEX file: write only the directory header
866#
867 if lexOnly:
868 imgFile.write(dirEntry.getBytes())
869 else:
870#
871# if LIF image file, write volume header and directory
872#
873 imgFile.write(volHeader.getBytes())
874 imgFile.write(dirEntry.getBytes())
875 dirEntry.createNullEntry()
876 for i in range(0,7):
877 imgFile.write(dirEntry.getBytes())
878#
879# HP-75 only: write RAM file header
880#
881 if machine == "75":
882 imgFile.write(fileHeader.getBytes())
883#
884# write file content
885#
886 imgFile.write(objectFile.getBytes())
887#
888# fill up remaining bytes in sector
889#
890 rem=256-(fileLength % 256)
891 imgFile.write(bytearray(rem))
892 imgFile.close()
893 except OSError:
894 raise capasmError("cannot write lif image file")
896 if lexOnly:
897 print("LEX file "+outputFileName+" created")
898 else:
899 print("LIF image file "+outputFileName+" created which contains: " \
900 + lifFileName+" (LEX"+machine+")")
901#
902# we return always false here, because all error conditions raise
903# an exception
904#
905 return False
906#
907# ROM file creator class ----------------------------------------
908#
909class clsRomCreator(object):
911 def __init__(self):
912 super().__init__()
913#
914# create LIF import image (lexOnly=False) from binary input file
915#
916# Returns:
917# True: everything o.k.
918# Raises capasmError on i/o error or if the ROM size is too small
919#
920 def create(self,binFileName,romFileName="",romSize=2):
921#
922# check if we run in regression test mode
923#
924 isRegressionTest=False
925 if os.getenv("CAPASMREGRESSIONTEST"):
926 isRegressionTest=True
927#
928# build name of file image or lex file if not specified
929#
930 if romFileName=="":
931 romFileName=Path(binFileName).with_suffix(".rom").name
932#
933# read object file into memory, check rom number and rom size
934#
935 romSize=romSize*1024
936 rom75=False
937 objectFile=clsObjectFile(binFileName)
938 code=bytearray(objectFile.getBytes())
939#
940# HP-75 has always ROM number 0xE3 and the HP-85 style complementary no
941#
942 if code[0]==0xE3 and code[1]==0x1C:
943 rom75=True
944 if romSize < len(code)+2:
945 raise capasmError("ROM size too small")
946 print("creating HP-75 ROM file ",romFileName)
947#
948# determine if we have a HP-85 or HP-87 file
949#
950 else:
951 romNo=code[0]
952 checkRomNo= ~ romNo &0xFF
953 if checkRomNo== code[1]:
954 print("creating HP-85 ROM file ",romFileName)
955 elif checkRomNo+1 == code[1]:
956 print("creating HP-87 ROM file ",romFileName)
957 else:
958 raise capasmError("Invalid ROM number")
959 if romSize < len(code)+4:
960 raise capasmError("ROM size too small")
961#
962# fill code to length of rom
963#
964 fill=romSize - len(code)
965 for i in range(0,fill):
966 code.append(0)
967#
968# HP-75 check sum
969#
970 if rom75:
971 c=0
972 i=0
973 while(i<len(code)):
974 c+=code[i]
975 while c> 255:
976 c+=1
977 c-= 256
978 i+=1
979 cInv= ~c &0xFF
980 code[-1]=cInv
981 else:
982#
983# determine secondary checksum, thanks to Philippe (hp80series@groups.io)
984#
985 t=0
986 i=0
987 while(i< len(code)-4):
988 c1=code[i]
989 i+=1
990 c2=code[i]
991 i+=1
992 w = (c1 & 0xff) + ((c2 & 0xff)<<8)
993 for j in range (0,16):
994 r26 = t & 0xff
995 r27 = t>>8
996 r45 = r27
997 r27 = (r27<<4) & 0xff
998 r26 = ((r26<<1) & 0xff) | (w & 1)
999 w = w>>1
1000 r45 = r45 ^ r26
1001 r45 = r45 ^ r27
1002 if (r45 & 1):
1003 r45 = (r45 + 0x80) & 0xff
1004 t = ((t<<1) & 0xffff) | (r45 >>7)
1005 code[-4]=(t & 0xFF)
1006 code[-3]=((t>>8) & 0xFF)
1007#
1008# determine primary checksum, thanks to Philippe (hp80series@groups.io)
1009#
1010 t=0
1011 i=0
1012 while(i< len(code)-2):
1013 c1=code[i]
1014 i+=1
1015 c2=code[i]
1016 i+=1
1017 t = t + (c1 & 0xff) + ((c2 & 0xff)<<8)
1018 s = ((t>>16) + (t & 0xffff)) & 0xffff
1019 t = s>>8
1020 s = s & 0xff
1021 code[-2]=(255-s)
1022 code[-1]=(255-t)
1023#
1024# write rom file
1025#
1026 try:
1027 romFile=open(romFileName,"wb")
1028 romFile.write(code)
1029 romFile.close()
1030 except OSError:
1031 raise capasmError("cannot write rom file")
1032 return False
1033#
1034# entry point caplif ------------------------------------------------------
1035# put assembled binary file to an import LIF image file
1036#
1037def caplif(): # pragma: no cover
1038#
1039# command line arguments processing
1040#
1041 argparser=argparse.ArgumentParser(description=\
1042 "Utility to put an assembled binary file into an import LIF image file",\
1043 epilog="See https://github.com/bug400/capasm for details. "+CAPASM_VERSION)
1044 argparser.add_argument("binfile",help="binary object code file (required)")
1045 argparser.add_argument("-m","--machine",choices=['75','85','87'], \
1046 help="Machine type (default=85)",default='85')
1047 argparser.add_argument("-l","--lifimagefilename", help=\
1048 "name of the Upload LIF image file (default: objectfile name with suffix .dat)",\
1049 default="")
1050 argparser.add_argument("-f","--filename", \
1051 help="name of the LIF directory entry (default: WS_FILE for Series 80, deduced from object file name for HP-75)",\
1052 default="")
1053 args= argparser.parse_args()
1055 l=clsLifCreator()
1056 try:
1057 l.create(args.binfile,args.machine,args.lifimagefilename,args.filename,
1058 False)
1059 except capasmError as e:
1060 print(e.msg+" -- program terminated")
1061 sys.exit(1)
1062#
1063# entry point caplex ------------------------------------------------------
1064# convert assembled binary file to a LIF file of type LEX75 or BPGM80
1065#
1066def caplex(): # pragma: no cover
1067#
1068# command line arguments processing
1069#
1070 argparser=argparse.ArgumentParser(description=\
1071 "Utility to convert an assembled binary file into a LIF file of type LEX75 or BPGM80",\
1072 epilog="See https://github.com/bug400/capasm for details. "+CAPASM_VERSION)
1073 argparser.add_argument("binfile",help="binary object code file (required)")
1074 argparser.add_argument("-m","--machine",choices=['75','85','87'], \
1075 help="Machine type (default:85)",default='85')
1076 argparser.add_argument("-l","--lexfilename",help=\
1077 "name of the LIF output file (default: objectfile name with suffix .lex)",\
1078 default="")
1079 argparser.add_argument("-f","--filename", help=\
1080 "file name in the LIF header (default: deduced from objectfilename)",\
1081 default="")
1082 args= argparser.parse_args()
1084 l=clsLifCreator()
1085 try:
1086 l.create(args.binfile,args.machine,args.lexfilename,args.filename,
1087 True)
1088 except capasmError as e:
1089 print(e.msg+" -- program terminated")
1090 sys.exit(1)
1091#
1092# entry point capglo -------------------------------------------------------
1093# convert a list of input files to a python files with a global
1094# symbol class definition which have the name of the input file with
1095# the suffix ".py"
1096#
1097def capglo(): # pragma: no cover
1099 p=argparse.ArgumentParser(description=\
1100 "Utility to convert global HP-85/HP-87/HP-75 symbol files for the capasm assembler",\
1101 epilog="See https://github.com/bug400/capasm for details. "+CAPASM_VERSION)
1102 p.add_argument('inputfiles',nargs='+',help="list of gobal symbol assembler files (one argument required)")
1103 args=p.parse_args()
1105 gen=clsSymClassGenerator()
1106 hasErrors=False
1107 for inputFileName in args.inputfiles:
1108 outputFileName=Path(inputFileName).with_suffix(".py").name
1109 try:
1110 hasErrors!=gen.generate(inputFileName,outputFileName)
1111 except capasmError as e:
1112 print(e.msg+" -- program terminated")
1113 hasErrors=True
1114 break
1115 if hasErrors:
1116 sys.exit(1)
1117#
1118# entry point caprom -------------------------------------------------------
1119#
1120# convert an assembled binary file to a Series 80 ROM file with checksums
1121#
1123def caprom(): # pragma: no cover
1125 argparser=argparse.ArgumentParser(description=\
1126 "Utility to convert an assembled binary file to a Series 80 ROM file",\
1127 epilog="See https://github.com/bug400/capasm for details. "+CAPASM_VERSION)
1128 argparser.add_argument("binfile",help="binary object code file (required)")
1129 argparser.add_argument("-r","--romfilename",help=\
1130 "name of the LIF output file (default: objectfile name with suffix .lex)",\
1131 default="")
1132 argparser.add_argument("-s","--romsize",choices=[2,4,8,16,32],type=int, \
1133 help="ROM size in KB (default:2)",default=2)
1134 args= argparser.parse_args()
1136 l=clsRomCreator()
1137 try:
1138 l.create(args.binfile,args.romfilename,args.romsize)
1139 except capasmError as e:
1140 print(e.msg+" -- program terminated")
1141 sys.exit(1)
1142#
1143# entry point capconv -------------------------------------------------------
1144# convert a binary Series 80 global symbols file to an ascii file with
1145# DAD or EQU symbol definitions
1146#
1147def capconv(): # pragma: no cover
1149 p=argparse.ArgumentParser(description=\
1150 "Utility to convert binary HP-85/HP-87/HP-75 symbol files to ascii files",\
1151 epilog="See https://github.com/bug400/capasm for details. "+CAPASM_VERSION)
1152 p.add_argument('inputfiles',nargs='+',help="list of gobal symbol assembler files (one argument required)")
1153 p.add_argument("-t","--type",required=True,help="what to convert", \
1154 choices=["asm","glo"])
1155 args=p.parse_args()
1157 if args.type=="glo":
1158 conv=clsSymbolFileConverter()
1159 suffix=".glo"
1160 else:
1161 conv=clsAsmSourceFileConverter()
1162 suffix=".asm"
1163 hasErrors=False
1164 for inputFileName in args.inputfiles:
1165 outputFileName=Path(inputFileName).with_suffix(suffix).name
1166 try:
1167 hasErrors!=conv.convert(inputFileName,outputFileName)
1168 except capasmError as e:
1169 print(e.msg+" -- program terminated")
1170 hasErrors=True
1171 break
1172 if hasErrors:
1173 sys.exit(1)
1174#