Coverage for capasm/captools.py : 90%

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