A lot of the posts leading up to this were exploratory of the Action! language and were occurring as I was writing this program. This program is something I’ve wanted to write for a while. In this post I present a utility to read information about dBase II/III and FoxBASE database files from the PC. Something never intended for the Atari 8 bits. I’m going to create several utilities to read and write to this format. This first one just displays information about the DBF file itself and is thusly named DBFINFO:
- If file is valid (cursory evaluation at this point)
- If file has an associated memo file
- Date Last Updated
- Number of Records
- Header Size
- Data Record Size
- Each field name, type, length, and applicable decimal precision
- Ability to dump the output to the printer
To do this I created a few databases with a set number of fields and various numbers of records. I then used documentation for the dBase III file format, a hex editor, and careful scrutiny of the byte streams to decipher the files. The documented format I found may have been correct, but it didn’t completely match the test files I created. I’ve lost the link to the page I got it from and subsequent searches have yielded what appear to be fairly accurate mappings. Maybe the original link was for a different dBase version. At any rate, it was only a few bytes off in a couple of places and only took careful examination to resolve. I’m not going to cover the structure definition in this post, but I will include the links I do have at the conclusion.
With an accurate file mapping, I started writing code to read a file. The program here is the result. This program includes a little bit of everything I’ve covered in the past Action! posts. While the code is fully documented, I am going to break it down in this post. This time the breakdown will follow the complete source – I think that makes more sense than having it before like I have done before. And since this uses several files via INCLUDE, those are listed as well (but not broken down because their contents have previously been broken down in posts).
In addition to code I wrote it uses two code libraries from the Action! Toolkit: PRINTF.ACT and CHARTEST.ACT. The source code INCLUDEs them from D5. Alter the drive for the drive you have the Action! Toolkit loaded into.
It also uses the Action! runtime library. I copied the runtime library SYS.ACT to the same drive (D2) as the program source. I did this because it requires a modification. For a successful compile the PRINTF routine has to be commented out of the runtime (SYS.ACT) because it conflicts with the PRINTF from the toolkit. Normally I have the runtime in disk 4 (D4), but here you see it included from disk 2 (D2).
The code I’ve written is released under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International license. Some may disagree with the license I’ve chosen. The choice is temporary until I complete the remaining components. When I feel I am finished I will alter the license to be more open.
Complete Source : DBFINFO.ACT
; Program: DBFINFO.ACT ; Author.: Wade Ripkowski ; Date...: 2015.08 ; Desc...: Displays info for dBase III ; DBF files. ; License: Creative Commons ; Attribution-NonCommercial- ; NoDerivatives ; 4.0 International ; Inc Action Runtime INCLUDE "D2:SYS.ACT" ; Inc Action Toolkit parts INCLUDE "D5:PRINTF.ACT" INCLUDE "D5:CHARTEST.ACT" ; Include library INCLUDE "D3:DEFINES.ACT" INCLUDE "D3:LIBIO.ACT" INCLUDE "D3:LIBMISC.ACT" INCLUDE "D3:LIBDOS.ACT" ; Start DBF Info MODULE ; Header record type TYPE DBHead=[BYTE bMe, bYr, bMo, bDy CARD cNDR, cHSZ, cDSZ] ; Global vars BYTE bgErr=[0] CARD cErrO ; Custom Quit routine PROC DIQuit() ; Restore error vector Error=cErrO RETURN ; Custom Error routine PROC DIErr(BYTE bE) ; Set global error number bgErr=bE ; Print custom error message PrintF("%EERROR: ") if bE=170 then PrintE("File not found!") elseif bE=130 then PrintE("Invalid device!") elseif bE=255 then PrintE("Not dBase II/III or FoxBASE file!") fi RETURN ; Reads a string from file of x bytes PROC GetStrD(BYTE bD CHAR POINTER pS BYTE bL) BYTE bLp,bI,bE ; Set done flag bE=0 ; Set string length pS(0)=bL ; Process length bytes for bLp=1 to bL DO ; Read a byte from device bI=GetD(bD) ; If done, pad with space if bE=1 then bI=32 fi ; If null byte, set done and space if bI=0 then bI=32 bE=1 fi ; Save read value to str pS(bLp)=bI OD RETURN ; Main routine PROC Main() DBHead dHd CHAR ARRAY cDBF(16),cF(12),cB(21) BYTE bI,bFn,bLp,bTp,bFL,bFD,bHT,bRT CARD cI,cM BYTE bPr,bSD=[0] ; Save and reassign error vector cErrO=Error Error=DIErr ; Setup screen Poke(82,0) Put(CCLS) PrintE("-[DBF Info v1.0]------------------------") ; Check for SpartaDOS bSD=IsSD() ; Get filename Print("DBF Filename?") InputS(cDBF) Print("Printer Output (Y/N)?") bPr=WaitYN(1) PutE() ; Open file for read Open(4,cDBF,4,0) ; If no error, proceed if bgErr=0 then ; Open printer if needed if bPr = TRUE then Open(5,"P:",8,0) fi ; Display filename PrintF("File : %S%E",cDBF) ; If print selected if bPr = TRUE then PrintFD(5,"File : %S%E",cDBF) fi ; Read 1st byte, check type bI=GetD(4) ; Print file version Print("File Type: ") if bI = $02 then PrintE("dBase II") elseif bI = $03 then PrintE("dBase III/FoxBASE+ (-Memo)") elseif bI = $04 then PrintE("dBase IV (-Memo)") elseif bI = $05 then PrintE("dBase V (-Memo)") elseif bI = $30 then PrintE("Visual FoxPro") elseif bI = $31 then PrintE("Visual FoxPro (+Auto Inc)") elseif bI = $32 then PrintE("Visual FoxPro (+Var Types)") elseif bI = $43 then PrintE("dBase IV SQL table (-Memo)") elseif bI = $63 then PrintE("dBase IV SQL system (-Memo)") elseif bI = $7B then PrintE("dBase IV (+Memo)") elseif bI = $83 then PrintE("dBase III/FoxBASE+ (+Memo)") elseif bI = $8B then PrintE("dBase IV (+Memo)") elseif bI = $CB then PrintE("dBase IV SQL table (+Memo)") elseif bI = $E5 then PrintE("HiPeR-Six (+SMT Memo)") elseif bI = $F5 then PrintE("FoxPro 1/2 (+Memo)") elseif bI = $FB then PrintE("FoxBASE") fi ; If print selected if bPr = TRUE then ; Print file version PrintD(5,"File Type: ") if bI = $02 then PrintDE(5,"dBase II") elseif bI = $03 then PrintDE(5,"dBase III/FoxBASE+ (+Memo)") elseif bI = $04 then PrintDE(5,"dBase IV (-Memo)") elseif bI = $05 then PrintDE(5,"dBase V (-Memo)") elseif bI = $30 then PrintDE(5,"Visual FoxPro") elseif bI = $31 then PrintDE(5,"Visual FoxPro (+Auto Inc)") elseif bI = $32 then PrintDE(5,"Visual FoxPro (+Var Types)") elseif bI = $43 then PrintDE(5,"dBase IV SQL table (-Memo)") elseif bI = $63 then PrintDE(5,"dBase IV SQL system (-Memo)") elseif bI = $7B then PrintDE(5,"dBase IV (+Memo)") elseif bI = $83 then PrintDE(5,"dBase III/FoxBASE+ (+Memo)") elseif bI = $8B then PrintDE(5,"dBase IV (+Memo)") elseif bI = $CB then PrintDE(5,"dBase IV SQL table (+Memo)") elseif bI = $E5 then PrintDE(5,"HiPeR-Six (+SMT Memo)") elseif bI = $F5 then PrintDE(5,"FoxPro 1/2 (+Memo)") elseif bI = $FB then PrintDE(5,"FoxBASE") fi fi ; Exit if not dBase II/III or FoxBASE if bI # $02 AND bI # $FB AND bI # $03 AND bI # $83 then DIErr(255) else ; Read next 3 bytes (year,month,day) dHd.bYr=GetD(4) dHd.bMo=GetD(4) dHd.bDy=GetD(4) PrintF("Updated : 20%D.%D.%D%E",dHd.bYr,dHd.bMo,dHd.bDy) ; If print selected if bPr = TRUE then PrintFD(5,"Updated : 20%D.%D.%D%E",dHd.bYr,dHd.bMo,dHd.bDy) fi ; Read 2 bytes of NDR (LSBMSB) dHd.cNDR=GetCD(4) PrintF("Records : %U%E",dHd.cNDR) ; If print selected if bPr = TRUE then PrintFD(5,"Records : %U%E",dHd.cNDR) fi ; Read 2 bytes (DB MSB of NDR - millions) EatByteD(4,2) ; Read 2 bytes of HSZ (LSBMSB) dHd.cHSZ=GetCD(4) PrintF("HeaderSz : %U%E",dHd.cHSZ) ; If print selected if bPr = TRUE then PrintFD(5,"HeaderSz : %U%E",dHd.cHSZ) fi ; Read 2 bytes of DSZ (LSBMSB) dHd.cDSZ=GetCD(4) PrintF("Data Sz : %U%E",dHd.cDSZ) ; If print selected if bPr = TRUE then PrintFD(5,"Data Sz : %U%E",dHd.cDSZ) fi ; Read 20 bytes filler EatByteD(4,20) ; Calc # of fields bFn=(dHd.cHSZ-32)/32 PrintE("#-- Name------ Type Len Dec") ; If print selected if bPr = TRUE then PrintDE(5,"#-- Name------ Type Len Dec") fi ; Loop through each field for bLp=1 to bFn DO ; Read 10 bytes of field name GetStrD(4,cF,10) ; Eat 1 byte EatByteD(4,1) ; Read 1 byte field type bTp=GetD(4) ; Eat 4 bytes of field mem addr EatByteD(4,4) ; Read 1 byte field len bFL=GetD(4) ; Read 1 byte dec len bFD=GetD(4) ; Read 14 bytes junk EatByteD(4,14) ; Print field info PrintF("%3D %S ",bLp,cF) ; If print selected if bPr = TRUE then PrintFD(5,"%3D %S ",bLp,cF) fi ; Print field type if bTp=$43 then Print("Char") elseif bTp=$44 then Print("Date") elseif bTp=$4C then Print("Log ") elseif bTp=$4D then Print("Memo") elseif bTp=$4E then Print("Num ") else Print("Unk ") fi ; If print selected if bPr = TRUE then if bTp=$43 then PrintD(5,"Char") elseif bTp=$44 then PrintD(5,"Date") elseif bTp=$4C then PrintD(5,"Log ") elseif bTp=$4D then PrintD(5,"Memo") elseif bTp=$4E then PrintD(5,"Num ") else Print("Unk ") fi fi PrintF(" %3D %3D%E",bFL,bFD) ; If print selected if bPr = TRUE then PrintFD(5," %3D %3D%E",bFL,bFD) fi OD ; Check header terminator, expect $0d bHT=GetD(4) ; Skip unknown byte EatByteD(4,1) ; Check record terminator ; 0 record file if dHd.cNDR = 0 then ; Get record terminator, expect $1a bRT=GetD(4) ; Multi-record file else ; Read all record bytes cM=(dHd.cNDR * dHd.cDSZ) EatByteD(4,cM) ; Get record terminator, expect $1a bRT=GetD(4) fi ; Header terminator status if bHT # $0D then PrintE("Header terminator invalid!") ; If print selected if bPr = TRUE then PrintDE(5,"Header terminator invalid!") fi fi ; Record terminator status if bRT # $1A then PrintE("Record terminator invalid!") ; If print selected if bPr = TRUE then PrintDE(5,"Record terminator invalid!") fi fi fi ; Close printer if opened if bPr = TRUE then Close(5) fi ; Close DBF file Close(4) fi ; Restore Error handler and clean up DIQuit() ; If SpartaDOS, exit through DOSVEC if bSD=1 then SDx() fi RETURN
Included Source : DEFINES.ACT
; Library: DEFINES.ACT ; Desc...: Global definitions ; Author.: Wade Ripkowski ; Date...: 2015.08 ; License: Creative Commons ; Attribution-NonCommercial- ; NoDerivatives ; 4.0 International ; Character Values DEFINE CCLS = "$7D" ; Booleans DEFINE TRUE = "1" DEFINE FALSE = "0" ; Keystroke Values DEFINE KNONE = "255" DEFINE KENTER= "12" DEFINE KESC = "28" DEFINE KLEFT = "134" DEFINE KRIGHT= "135" DEFINE KUP = "142" DEFINE KDOWN = "143" ; Console Key Values DEFINE KCNONE= "7" DEFINE KSTART= "262" DEFINE KSELEC= "261" DEFINE KOPTON= "259" MODULE
Included Source : LIBIO.ACT
; Library: LIBIO.ACT ; Author.: Wade Ripkowski ; Desc...: I/O Routines ; Date...: 2015.08 ; License: Creative Commons ; Attribution-NonCommercial- ; NoDerivatives ; 4.0 International ; Func..: CARD GetCD(BYTE bD) ; Param.: bD=device channel ; Return: CARD ; Desc..: Reads CARD value from device CARD FUNC GetCD(BYTE bD) BYTE bL=[0],bM=[0] CARD cV=[0] ; Read LSB byte bL=GetD(bD) ; Read MSB byte bM=GetD(bD) ; Compute value cV=(bM*256)+bL RETURN(cV) ; Func..: INT GetID(BYTE bD) ; Param.: bD=device channel ; Return: INT ; Desc..: Reads INT value from device INT FUNC GetID(BYTE bD) CARD cT=[0] INT iV=[0] ; Read CARD value from device cT=GetCD(bD) ; If CARD > 65536 its a negative int if cT > $8000 then ; Flip bits and add 1 to get INT value cT=(cT XOR $FFFF)+1 ; Multiply by -1 to make negative iV=cT*-1 else ; Not a negative int, just assign iV=cT fi RETURN(iV) ; Func..: PutCD(BYTE bD) ; Param.: bD=device channel ; Desc..: Puts CARD value to device PROC PutCD(BYTE bD CARD cV) BYTE bL=[0],bM=[0] ; Compute MSB and LSB bM=cV RSH 8 bL=cV ; Put LSB,MSB bytes to device PutD(bD,bL) PutD(bD,bM) RETURN ; Func..: PutID(BYTE bD) ; Param.: bD=device channel ; Desc..: Puts INT value to device PROC PutID(BYTE bD, INT iV) CARD cT ; If INT is negative if iV < 0 then ; Make it positive cT=iV * -1 ; Convert it to high card cT=(cT XOR $FFFF) + 1 else ; Is positive, just assign it cT=iV fi ; Put card value to device PutCD(bD,cT) RETURN ; Func..: EatByteD(BYTE bD CARD cL) ; Param.: bD=device channel ; cL=number of bytes ; Desc..: Read x bytes from device ; w/o save PROC EatByteD(BYTE bD BYTE cL) BYTE bI CARD cLp ; Process len bytes for cLp=1 to cL DO ; Read byte and disregard bI=GetD(bD) OD RETURN MODULE
Included Source : LIBMISC.ACT
; Library: LIBMISC.ACT ; Author.: Wade Ripkowski ; Desc...: Misc Routines ; Date...: 2015.08 ; License: Creative Commons ; Attribution-NonCommercial- ; NoDerivatives ; 4.0 International ; Requires DEFINES.ACT be loaded 1st! ; Func..: WaitYN(BYTE bE) ; Param.: bE=1 to print Y or N ; Return: BYTE (1=Y,0=N) ; Desc..: Waits for Y or N keypress BYTE FUNC WaitYN(BYTE bE) BYTE bRKEYCH=764,bR=[0],bK=[0] DO ; Wait for any keypress WHILE bRKEYCH=KNONE DO OD ; Key keypress and reset debounce bK=bRKEYCH bRKEYCH=KNONE ; Stay in loop until YyNn UNTIL bK=43 OR bK=107 OR bK=35 OR bK=99 OD ; If Yy then set return 1 if bK=43 OR bK=107 then bR=1 fi ; If echo on if bE=1 then ; If Y, print Y if bR=1 then Put('Y) ; Else print N else Put('N) fi fi RETURN(bR) MODULE
Included Source : LIBDOS.ACT
; Library: LIBDOS.ACT ; Desc...: DOS routines ; Author.: Wade Ripkowski ; Date...: 2015.08 ; License: Creative Commons ; Attribution-NonCommercial- ; NoDerivatives ; 4.0 International ; Proc..: IsSD() ; Return: bR: 1=yes, 0=no BYTE FUNC IsSD() BYTE bR=[0],bPk=[0] ; Get value from loc 3889 bPk=Peek(3889) ; Sparta if 0,15,68,89 if bPk=0 OR bPk=15 OR bPk=68 OR bPk=89 then bR=1 fi RETURN(bR) ; Proc..: SDx() ; Desc..: Exits to DOSVEC ($000A) via ; JMP using inline assembly PROC SDx() [ $6C $0A $00 ] MODULE
Source Breakdown : DBFINFO.ACT
Excerpts from the complete source above are further explained here. In this first section all the includes are pulled in. The first is the Action! Runtime package. This one is a copy and has PrintF commented out because I want the PrintF from the Action! Toolkit instead as it has padding built-in. The runtime package lets the program run without the Action! cartridge installed. Then I pull in two parts from the Action! Toolkit; the expanded PrintF routine and some Character Test routines. Following that I pull in the custom library routines I’ve written (broken down by category):
; Inc Action Runtime INCLUDE "D2:SYS.ACT" ; Inc Action Toolkit parts INCLUDE "D5:PRINTF.ACT" INCLUDE "D5:CHARTEST.ACT" ; Include library INCLUDE "D3:DEFINES.ACT" INCLUDE "D3:LIBIO.ACT" INCLUDE "D3:LIBMISC.ACT" INCLUDE "D3:LIBDOS.ACT"
In this section I declare the dBase Header record, or all of it minus the actual field definitions anyway. There is a memo flag which indicates if the file has an associated memo field file; the year, month, and day of the last update; the number of data records; the size of the file header; and the data record size including one byte for the deletion flag:
; Header record type TYPE DBHead=[BYTE bMe, bYr, bMo, bDy CARD cNDR, cHSZ, cDSZ]
These variables are used for storing the last encountered error (bgErr), and the address of the original Error vector. I change it to customize error handling:
; Global vars BYTE bgErr=[0] CARD cErrO
This is the custom quit routine meant to clean up environment things the program changes. I had grander plans for this but haven’t fleshed it out entirely yet. It just resets the Error vector back to the original. In its current state, it could just as well be a single line of code at the end of the program instead of a PROCedure:
; Custom Quit routine PROC DIQuit() ; Restore error vector Error=cErrO RETURN
This is the custom error routine. It accepts the error encounter as a byte parameter. It sets the global variable bgErr so it can be referenced later by other parts of the program. It then displays the appropriate error message. Non-DOS (i.e.; program custom) errors are high numbers like 255. Since this program is only designed to read dBase II/III, and FoxBASE files, that is currently the only custom error:
; Custom Error routine PROC DIErr(BYTE bE) ; Set global error number bgErr=bE ; Print custom error message PrintF("%EERROR: ") if bE=170 then PrintE("File not found!") elseif bE=130 then PrintE("Invalid device!") elseif bE=255 then PrintE("Not dBase II/III or FoxBASE file!") fi RETURN
This routine reads a string of x bytes (bL) from the specified device (bD) and stores the result in the given string (pS). When it encounters a NULL byte (0), it starts padding the string with space until all bytes required are filled:
; Reads a string from file of x bytes PROC GetStrD(BYTE bD CHAR POINTER pS BYTE bL) BYTE bLp,bI,bE ; Set done flag bE=0 ; Set string length pS(0)=bL ; Process length bytes for bLp=1 to bL DO ; Read a byte from device bI=GetD(bD) ; If done, pad with space if bE=1 then bI=32 fi ; If null byte, set done and space if bI=0 then bI=32 bE=1 fi ; Save read value to str pS(bLp)=bI OD RETURN
This is the start of the long (too long) main routine. The printing should be broken down into a separate function which would make it a lot shorter. In the next version perhaps. This declares a DBHead variable (dHd) to store the dBase header info (minus field definitions). It then declares 3 character strings to store the database file name (cDBF), string storage for a field name (cF), and small buffer (cB). It then declares a handful of bytes for various things; bI is an input byte; bFn tracks the field number; bLp is for looping; bTp is the field type; bFL is the field length; bFD is the field decimal precision; bHT is the header terminator; bRT is the record terminator. Then there are two cards; cI is an input card; cM is used to hold the number of bytes consumed by all records via calculation. Then there are two last bytes; bPr is the send to printer flag, and bSD holds the SpartaDOS detection flag:
; Main routine PROC Main() DBHead dHd CHAR ARRAY cDBF(16),cF(12),cB(21) BYTE bI,bFn,bLp,bTp,bFL,bFD,bHT,bRT CARD cI,cM BYTE bPr,bSD=[0]
This sections saves the original error vector and assigns it to the custom one. It then sets up the screen for output. I used plain ASCII for all these blog posts because ATASCII doesn’t display properly.
; Save and reassign error vector cErrO=Error Error=DIErr ; Setup screen Poke(82,0) Put(CCLS) PrintE("-[DBF Info v1.0]------------------------")
This calls the routine to check for SpartDOS and stores the flag. It needs to know so it can exit properly:
; Check for SpartaDOS bSD=IsSD()
This just gets the filename from the user and asks if the output should also be printed to the printer:
; Get filename Print("DBF Filename?") InputS(cDBF) Print("Printer Output (Y/N)?") bPr=WaitYN(1) PutE()
This tries to open device (4) with the file given by the user (cDBF) for read (4). If there was no error (bgErr is 0) then proceed. Next try to open device (5) to the printer (P:) for write (8):
; Open file for read Open(4,cDBF,4,0) ; If no error, proceed if bgErr=0 then ; Open printer if needed if bPr = TRUE then Open(5,"P:",8,0) fi
Just display the filename, and optionally print:
; Display filename PrintF("File : %S%E",cDBF) ; If print selected if bPr = TRUE then PrintFD(5,"File : %S%E",cDBF) fi
This section reads the first byte of the file. This is the signature byte. It tells what flavor of dBase created it. Then it displays the name of the dBase flavor based on the value. And then it optionally prints the same information:
; Read 1st byte, check type bI=GetD(4) ; Print file version Print("File Type: ") if bI = $02 then PrintE("dBase II") elseif bI = $03 then PrintE("dBase III/FoxBASE+ (-Memo)") elseif bI = $04 then PrintE("dBase IV (-Memo)") elseif bI = $05 then PrintE("dBase V (-Memo)") elseif bI = $30 then PrintE("Visual FoxPro") elseif bI = $31 then PrintE("Visual FoxPro (+Auto Inc)") elseif bI = $32 then PrintE("Visual FoxPro (+Var Types)") elseif bI = $43 then PrintE("dBase IV SQL table (-Memo)") elseif bI = $63 then PrintE("dBase IV SQL system (-Memo)") elseif bI = $7B then PrintE("dBase IV (+Memo)") elseif bI = $83 then PrintE("dBase III/FoxBASE+ (+Memo)") elseif bI = $8B then PrintE("dBase IV (+Memo)") elseif bI = $CB then PrintE("dBase IV SQL table (+Memo)") elseif bI = $E5 then PrintE("HiPeR-Six (+SMT Memo)") elseif bI = $F5 then PrintE("FoxPro 1/2 (+Memo)") elseif bI = $FB then PrintE("FoxBASE") fi ; If print selected if bPr = TRUE then ; Print file version PrintD(5,"File Type: ") if bI = $02 then PrintDE(5,"dBase II") elseif bI = $03 then PrintDE(5,"dBase III/FoxBASE+ (+Memo)") elseif bI = $04 then PrintDE(5,"dBase IV (-Memo)") elseif bI = $05 then PrintDE(5,"dBase V (-Memo)") elseif bI = $30 then PrintDE(5,"Visual FoxPro") elseif bI = $31 then PrintDE(5,"Visual FoxPro (+Auto Inc)") elseif bI = $32 then PrintDE(5,"Visual FoxPro (+Var Types)") elseif bI = $43 then PrintDE(5,"dBase IV SQL table (-Memo)") elseif bI = $63 then PrintDE(5,"dBase IV SQL system (-Memo)") elseif bI = $7B then PrintDE(5,"dBase IV (+Memo)") elseif bI = $83 then PrintDE(5,"dBase III/FoxBASE+ (+Memo)") elseif bI = $8B then PrintDE(5,"dBase IV (+Memo)") elseif bI = $CB then PrintDE(5,"dBase IV SQL table (+Memo)") elseif bI = $E5 then PrintDE(5,"HiPeR-Six (+SMT Memo)") elseif bI = $F5 then PrintDE(5,"FoxPro 1/2 (+Memo)") elseif bI = $FB then PrintDE(5,"FoxBASE") fi fi
This section decides if the program can continue. It is capable of reading dBase II, dBase III, and FoxBASE files. If the signature byte is not one of those, call the error routine and drop to the endif later in the code to exit cleanly, otherwise start the next section:
; Exit if not dBase II/III or FoxBASE if bI # $02 AND bI # $FB AND bI # $03 AND bI # $83 then DIErr(255) else
This gets the next part of the header; the year, the month, and the day of last update. it then displays the information and optionally prints it:
; Read next 3 bytes (year,month,day) dHd.bYr=GetD(4) dHd.bMo=GetD(4) dHd.bDy=GetD(4) PrintF("Updated : 20%D.%D.%D%E",dHd.bYr,dHd.bMo,dHd.bDy) ; If print selected if bPr = TRUE then PrintFD(5,"Updated : 20%D.%D.%D%E",dHd.bYr,dHd.bMo,dHd.bDy) fi
This reads the number of data records from the file header. It’s a card value. It then displays the information and optionally prints it:
; Read 2 bytes of NDR (LSBMSB) dHd.cNDR=GetCD(4) PrintF("Records : %U%E",dHd.cNDR) ; If print selected if bPr = TRUE then PrintFD(5,"Records : %U%E",dHd.cNDR) fi
This eats the last 2 bytes of the number of data records. Sizes it likely will never need to support anyway. The previous card value will support up to 65 thousand records:
; Read 2 bytes (DB MSB of NDR - millions) EatByteD(4,2)
This reads the header size from the file header. It’s a card value. It then displays the information and optionally prints it:
; Read 2 bytes of HSZ (LSBMSB) dHd.cHSZ=GetCD(4) PrintF("HeaderSz : %U%E",dHd.cHSZ) ; If print selected if bPr = TRUE then PrintFD(5,"HeaderSz : %U%E",dHd.cHSZ) fi
This reads the data record size from the file header. It’s a card value. It then displays the information and optionally prints it:
; Read 2 bytes of DSZ (LSBMSB) dHd.cDSZ=GetCD(4) PrintF("Data Sz : %U%E",dHd.cDSZ) ; If print selected if bPr = TRUE then PrintFD(5,"Data Sz : %U%E",dHd.cDSZ) fi
This reads the next 20 bytes from the file. The result is discarded. There are some additional flags in a couple of bytes but they are of no interest for this program:
; Read 20 bytes filler EatByteD(4,20)
This calculates the # of field definitions it needs to parse from the file header. It’s the header size minus 32, all divided by 32. It then displays a header line for the subsequent field information and optionally prints it:
; Calc # of fields bFn=(dHd.cHSZ-32)/32 PrintE("#-- Name------ Type Len Dec") ; If print selected if bPr = TRUE then PrintDE(5,"#-- Name------ Type Len Dec") fi
This is the start of the loop in which each field definition is read from the header:
; Loop through each field for bLp=1 to bFn DO
Read 10 bytes as a string for the field name. Then read one byte and discard. This is the 11th byte (0 terminator) of the field name:
; Read 10 bytes of field name GetStrD(4,cF,10) ; Eat 1 byte EatByteD(4,1)
Read 1 byte for the field type. It can be C for character, D for date, L for logical, M for memo, or N for numeric:
; Read 1 byte field type bTp=GetD(4)
This eats 4 bytes of the fields address in memory. Something the Atari couldn’t use if it wanted to. I’m not sure why it’s there anyway:
; Eat 4 bytes of field mem addr EatByteD(4,4)
This reads 1 byte for the field length and 1 byte for the fields decimal precision. Then it reads 14 bytes of junk to discard:
; Read 1 byte field len bFL=GetD(4) ; Read 1 byte dec len bFD=GetD(4) ; Read 14 bytes junk EatByteD(4,14)
This prints the field information that was just read in, and optionally prints it. It starts with the field number and name. The number is printed using Action! Toolkits PrintF to take advantage of padded output at 3 characters wide. This also displays, and optionally prints, the field type in a short descriptive form. And it displays and optionally prints the field length and decimal precision:
; Print field info PrintF("%3D %S ",bLp,cF) ; If print selected if bPr = TRUE then PrintFD(5,"%3D %S ",bLp,cF) fi ; Print field type if bTp=$43 then Print("Char") elseif bTp=$44 then Print("Date") elseif bTp=$4C then Print("Log ") elseif bTp=$4D then Print("Memo") elseif bTp=$4E then Print("Num ") else Print("Unk ") fi ; If print selected if bPr = TRUE then if bTp=$43 then PrintD(5,"Char") elseif bTp=$44 then PrintD(5,"Date") elseif bTp=$4C then PrintD(5,"Log ") elseif bTp=$4D then PrintD(5,"Memo") elseif bTp=$4E then PrintD(5,"Num ") else Print("Unk ") fi fi PrintF(" %3D %3D%E",bFL,bFD) ; If print selected if bPr = TRUE then PrintFD(5," %3D %3D%E",bFL,bFD) fi
This completes the loop for reading field definitions from the header:
OD
This reads the header terminator byte which should be hex 0D. It then reads one unknown byte (documentation I’ve found isn’t clear about it):
; Check header terminator, expect $0d bHT=GetD(4) ; Skip unknown byte EatByteD(4,1)
This checks if the number of data records is 0 or not. The record terminator is in different positions based on the number of records. This byte is read to validate if the file is structure properly:
; Check record terminator ; 0 record file if dHd.cNDR = 0 then
If the number of data records is 0, read the next byte which is the record terminator; hex 1A:
; Get record terminator, expect $1a bRT=GetD(4)
If the number of data records is greater than 0, read all the bytes for all data records inclusive of each records deletion flag (number of data records times the data record size). The output is not needed for this program so it is discarded. Then read the record terminator:
; Multi-record file else ; Read all record bytes cM=(dHd.cNDR * dHd.cDSZ) EatByteD(4,cM) ; Get record terminator, expect $1a bRT=GetD(4) fi
This checks if the header terminator was read correctly. If the value is not right the file structure is likely corrupt or otherwise invalid. If the value is not hex 0D, display and optionally print an error:
; Header terminator status if bHT # $0D then PrintE("Header terminator invalid!") ; If print selected if bPr = TRUE then PrintDE(5,"Header terminator invalid!") fi fi
This checks if the record terminator was read correctly. If the value is not right the file structure is likely corrupt, truncated, or otherwise invalid. If the value is not hex 1A, display and optionally print an error:
; Record terminator status if bRT # $1A then PrintE("Record terminator invalid!") ; If print selected if bPr = TRUE then PrintDE(5,"Record terminator invalid!") fi fi
This concludes the check of the files signature byte to ensure the file can be parsed; from far above:
fi
This cleans up by closing the printer on device 5, if opened; and the dBase file (cDBF) on device 4:
; Close printer if opened if bPr = TRUE then Close(5) fi ; Close DBF file Close(4)
This concludes the initial device open success check for the dBase file (cDBF) on device 4 from far above:
fi
In this final section the custom quit routine is called to restore the original Error vector. Then if SpartaDOS was detected a call to my library routine SDx is made. This causes the program to JMP to DOSVEC so it will return to the SpartaDOS command prompt:
; Restore Error handler and clean up DIQuit() ; If SpartaDOS, exit through DOSVEC if bSD=1 then SDx() fi RETURN
Results
I ran a PC DOS based version of a similar program as a validation against each of the files. I’ll present screenshots of both the PC DOS and my Action! version against each file.
The first is a zero (0) record DBF file (RECFLD.DBF) – essentially just a database structure:
The second is a single (1) record DBF file (RECFLD1.DBF):
The third is a triple (3) record DBF file (RECFLD3.DBF):
The last is a one hundred (100) field DBF file structure (no records – RECFLD99.DBF). Because there are more lines than will fit on the screen I redirected the PC output to this file: PC Output PDF
…
For the 99 record (100) one I also send the output to the printer: A8 Output PDF
All of the above showed databases with fields names of 8 characters. Here are screenshots from both the PC and Atari showing field names with 10 characters, just to prove it works:
I’ve included an ATR image as well. It has the program source, the libraries, the compiled program, and sample databases. Download the ATR. Remove the “.key” from it and add “.ATR”. It is not a Keynote file! ATR and zip are not allowed hosting types here so I added “.key” to it.
And finally there is this thread on AtariAge forums talking about dBase for Atari. Looks like dBase II was released for the Atari ST only.
Links
Here are some links discussing the dBase file format where I discovered what most of the structure is:
http://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_NOTE_8_TARGET
http://www.dbf2002.com/dbf-file-format.html