The PARADOX File Structure Compiled by Randy Beck ========================== rb@randybeck.com http://www.randybeck.com I haven't made changes to this description for a long while, but not much has changed. The latest revision concerns the tableName field, which I've discovered has lengthened in Paradox 7, and then another revision to that same field yet again. I'm not aware of any changes after Paradox 7. But if anybody knows of anything different in Paradox 8 and 9 then please let me know. I should say here that many of the fields are listed here as pointers while the file is in memory. I first made this connection back when Paradox 4 was still the state of the art. I have not verified that this remains true, but a quick glance at some of the fields makes me think it's probably still true. This document details the binary file format for Paradox data files. There are still a few unknowns, but the important items are covered. I refer to the older table format as 3.0 tables and the newer ones as 4.0 through 7.0, but I believe that most of the 3.0 information may also apply to the earlier versions. Pascal terms are used to describe data types: byte is 1 byte unsigned; integer is a 2-byte signed integer; word is a 2-byte unsigned integer; longint is a 4-byte signed integer; char is a 1-byte character; pchar is a pointer to a character; ^ modifies any type definition to a pointer to that type; ^pchar is a pointer to a pointer to a character. All pointers are 4-byte pointers. Please send additions and corrections to: Randy Beck P.O. Box 530433 DeBary, FL 32753-0433 USA email: rb@randybeck.com Distribute this freely, but please leave my name and address intact so that others may add information. GENERAL FILE STRUCTURE ====================== ============================ | Header | | Data Block 0 | | Data Block 1 | | Data Block 2 | | ... | | Data Block n | ============================ The size of the Paradox file header is usually 2048 bytes (see headerSize at file offset 0002). The first portion (offsets 0000 to $0057) has fixed field locations. The next section ($0058 to $0077) was new to Paradox 4.0 data files. The rest varies in size -- depending upon the upon the number of fields -- and is listed sequentially. Some of the information in the file header seems to be needed only while it is being used by Paradox in RAM. A few of these fields are pointers to other fields in the header, and are valid only during run-time. They are listed here anyway, although their meaning is subject to change and they serve little purpose for third-party software. The data area which follows is divided into record blocks. These use 1024, 2048, 3072 or 4096 bytes each, depending upon the maximum table size set when the table was created. The field structure of the data area is itself unusual in that each data field is arranged in hi-byte to low-byte order. Note: This hi-byte to low-byte arrangement only applies to the user data in the table. Everything else uses the normal low-byte to hi-byte format. The structure of primary and secondary index files generally follows that of Paradox version 3 data files. Paradox Common File Header -- offsets 0000 to 0057 ========================== With some noted exceptions, this part of the description is common to data and index files. Offsets in this list are given in hexadecimal. Other numbers should be assumed as listed in decimal format unless preceded by a dollar sign ('$'). offset type usage ============================================================================== | 0000 | integer recordSize | | | | | | This is the size of a user record in this table. | | | | | | For primary index files, each "record" is actually the | | | field or fields in the index, plus three integers which | | | are not referenced in the header. | | | | | | Secondary index files also have additional fields, but | | | these are listed in the header. | | | | +------+---------------------------------------------------------------------+ | 0002 | integer headerSize (always $0800) | | | | | | You can change headerSize, and move the data blocks | | | accordingly, to create larger or smaller table headers. | | | Borland's TUTILITY program would flag an error, but | | | Paradox, the Borland Database Engine and the Paradox | | | Engine will all still work with these tables. | | | | +------+---------------------------------------------------------------------+ | 0004 | byte fileType | | | | | | 0 = this is an indexed .DB data file | | | 1 = this is a primary index .PX file | | | 2 = this is a non-indexed .DB data file | | | 3 = this is a non-incrementing secondary index .Xnn file | | | 4 = this is a secondary index .Ynn file (inc or non-inc) | | | 5 = this is an incrementing secondary index .Xnn file | | | 6 = this is a non-incrementing secondary index .XGn file | | | 7 = this is a secondary index .YGn file (inc or non inc) | | | 8 = this is an incrementing secondary index .XGn file | | | | +------+---------------------------------------------------------------------+ | 0005 | byte maxTableSize | | | | | | This is the "maximum table size" determined when this | | | table was created. It really indicates the size of each | | | block of records in the data section of the table. | | | | | | 1 = 64M (block size = $0400 bytes) | | | 2 = 128M (block size = $0800 bytes) | | | 3 = 192M (block size = $0C00 bytes) | | | 4 = 256M (block size = $1000 bytes) | | | | +------+---------------------------------------------------------------------+ | 0006 | longint numRecords | | | | | | This is the number of records in this file. | | | | +------+---------------------------------------------------------------------+ | 000A | word nextBlock | | | | | | I'm not certain what this really is, but it seems to be | | | the same as fileBlocks unless there is an empty block in | | | the table. | | | | +------+---------------------------------------------------------------------+ | 000C | word fileBlocks | | | | | | This is the number of data blocks in the file. | | | (Each "block" is a cluster of records.) | | | | +------+---------------------------------------------------------------------+ | 000E | word firstBlock | | | | | | Always 1 unless the table is empty. | | | | +------+---------------------------------------------------------------------+ | 0010 | word lastBlock | | | | | | This works out to the number of blocks that the table | | | would contain if every block was packed. | | | | +------+---------------------------------------------------------------------+ | 0012 | word unknown | | | | | | The value of this field seems to the change when records | | | or blocks have been added to the table, but I still haven't | | | figured it out. | | | | +------+---------------------------------------------------------------------+ | 0014 | byte modifiedFlags1 | | | | | | A rebuild is required if this is not zero. | | | | +------+---------------------------------------------------------------------+ | 0015 | byte indexFieldNumber | | | | | | In the .Xnn file of a secondary index, this is the number | | | of the field it is referencing. | | | | | | This will be zero in the other files. | | | | +------+---------------------------------------------------------------------+ | 0016 | pointer primaryIndexWorkspace | | | | | | Pointer to the primary index file header (in RAM). | | | This will be a NIL if there is no primary index. | | | | +------+---------------------------------------------------------------------+ | 001A | pointer unknown (suspected pointer) | | | | | | This field is usually a NIL pointer. I've only seen it | | | used in 5.0 tables with BCD field types. It is probably | | | just a workspace pointer. | | | | +------+---------------------------------------------------------------------+ | 001E...0020 unknown | | | | | | I have only seen these three bytes used in .PX files. | | | | +------+---------------------------------------------------------------------+ | 0021 | integer numFields | | | | | | This is the number of fields in the table. If this is an | | | index file, then it would only be the number of fields in | | | this index. | | | | +------+---------------------------------------------------------------------+ | 0023 | integer primaryKeyFields | | | | | | This is the number of fields in the file's primary key. | | | It will be a zero for .PX and .Ynn files; and 2 for .Xnn | | | secondary index files. | | | | +------+---------------------------------------------------------------------+ | 0025 | longint encryption1 | | | | | | This was where the encryption information was stored for | | | versions 3.0 and 3.5. (It was a zero if not encrypted.) | | | | | | Subsequent versions store the value $FF00FF00 here, and | | | move the encryption code to offset $005C. Even so, these | | | newer versions still maintain this information at both | | | locations while working in RAM. | | | | | | Primary and .Ynn secondary index files always use this | | | field to store the encryption code, but it is often a | | | zero because the Paradox Engine and the Borland Database | | | Engine do not always encrypt index files. You can encrypt | | | unencrypted index files by following these steps: | | | | | | Begin with an empty encrypted table; | | | Truncate the index files to headerSize; | | | Zeroize nextBlock, fileBlocks, firstBlock and lastBlock; | | | Copy four bytes from the .DB data file's encryption1 | | | field (for version 3), or the encryption2 field | | | (for versions 4 and above) into the encryption1 | | | or encryption2 field of the index file; | | | Test thoroughly. | | | | | | | +------+---------------------------------------------------------------------+ | 0029 | byte sortOrder | | | | | | $00: ASCII | | | $B7: International | | | $82: Norwegian/Danish | | | $E6: Norwegian/Danish (4.0) | | | $F0: Swedish/Finnish | | | | +------+---------------------------------------------------------------------+ | 002A | byte modifiedFlags2 | | | | | | A rebuild is required if this is not zero. | | | | +------+---------------------------------------------------------------------+ | 002B...002C unknown (always 0) | +------+---------------------------------------------------------------------+ | 002D | byte changeCount1 | | | | | | This is incremented whenever the file header is updated. | | | | +------+---------------------------------------------------------------------+ | 002E | byte changeCount2 | | | | | | I'm not certain when this is incremented. | | | | +------+---------------------------------------------------------------------+ | 002F | byte unknown | +------+---------------------------------------------------------------------+ | 0030 | ^pchar tableNamePtrPtr | | | | | | This is a pointer to tableNamePtr, which is a pointer to | | | tableName. Paradox uses this field to gain faster access | | | to tableName because that part of the header is accessed | | | sequentially. | | | | +------+---------------------------------------------------------------------+ | 0034 | pointer fldInfoPtr | | | | | | Pointer to the list of field identifiers. This is listed | | | in the accompanying Pascal record definition as a | | | PFldInfoRec type. | | | | | | You can use this pointer value to locate the table header | | | in memory during run time. Just subtract $0078 from this | | | value (for 4.0+ tables), or $0058 (for .PX and .Ynn index | | | files and version 3.0 tables). | | | | +------+---------------------------------------------------------------------+ | 0038 | byte writeProtected | | | | | | 0 write protection OFF | | | 1 write protection ON | | | | +------+---------------------------------------------------------------------+ | 0039 | byte fileVersionID | | | | | | $03 version 3.0 | | | $04 version 3.5 | | | $05..09 version 4.x (usually = $09) | | | $0A,$0B version 5.x | | | $0C version 7.x | | | | +------+---------------------------------------------------------------------+ | 003A | word maxBlocks | | | | | | I don't know what this is for. It is usually the same | | | as fileBlocks (at offset 000C). | | | | +------+---------------------------------------------------------------------+ | 003C | byte unknown | +------+---------------------------------------------------------------------+ | 003D | byte auxPasswords | | | | | | Number of auxiliary passwords assigned to the table. | | | | +------+---------------------------------------------------------------------+ | 003E...003F unknown | +------+---------------------------------------------------------------------+ | 0040 | pointer cryptInfoStartPtr | | | | | | Points to cryptInfo field. It is always NIL when not | | | encrypted. It is sometimes NIL even when encrypted. | | | | +------+---------------------------------------------------------------------+ | 0044 | pointer cryptInfoEndPtr | | | | | | Points to end of cryptInfo. This is NIL if not encrypted. | | | | +------+---------------------------------------------------------------------+ | 0048 | byte unknown | +------+---------------------------------------------------------------------+ | 0049 | longint autoInc | | | | | | This long integer stores the value used for the next auto | | | incrementing field in tables with a "+" autoincrementing | | | field type. | | | | | | Formerly used as a modification count. | | | | | | My thanks to Orlando Ruiz for informing me of this change. | | | | +------+---------------------------------------------------------------------+ | 004D...004E unknown | +------+---------------------------------------------------------------------+ | 004F | byte indexUpdateRequired | +------+---------------------------------------------------------------------+ | 0050...0054 unknown | +------+---------------------------------------------------------------------+ | 0055 | byte refIntegrity | | | | | | A value here (=2?) denotes that this table uses | | | referential integrity checks. | | | | | | | | | inxDirection (sec'y index file only) | | | | | | Secondary .Xnn index files of v7.0 tables use this | | | field to indicate sort order direction: | | | | | | $01 ascending sort | | | $11 descending sort | | | | +------+---------------------------------------------------------------------+ | 0056...0057 unknown | ============================================================================== Paradox 4+ Data File Header -- offsets 0058 to 0077 =========================== This part of the description applies only to .DB data files and .Xnn index files for Paradox versions 4.0 and later. offset type usage ============================================================================== | 0058 | integer unknown (file version ID?) | | | | | | $0105..$0109 version 4.x (usually = $0109) | | | $010A, $010B version 5.x (usually = $010B) | | | $010C version 7.0 | | | | +------+---------------------------------------------------------------------+ | 005A | integer unknown (file version ID?) | | | | | | same values as at 0058 | | | | +------+---------------------------------------------------------------------+ | 005C | longint encryption2 | | | | | | This will be zero if not encrypted. | | | See encryption1 at offset 0025. | | | | +------+---------------------------------------------------------------------+ | 0060 | longint fileUpdateTime (4.x only) | | | | | | Format similar to a packed date and time. | | | I don't know what this does in 5.0 tables. | | | | +------+---------------------------------------------------------------------+ | 0064 | integer hiFieldID | | | | | | This number is always numFields + 1. | | | | +------+---------------------------------------------------------------------+ | 0066 | integer hiFieldIDinfo? | | | | | | This is related to hiFieldID (above), but I don't really | | | know what it's for. | | | | +------+---------------------------------------------------------------------+ | 0068 | integer sometimesNumFields? | | | | | | This is sometimes the number of fields in the table, but | | | is often just a zero. I don't know why. | | | | +------+---------------------------------------------------------------------+ | 006A | integer dosGlobalCodePage | | | | | | This was the Global Code Page when this table was created. | | | | | | $01B5 United States | | | $02E1 Greek 1 | | | $0352 Multilingual (Latin I) | | | $0354 Eastern European (Latin II) | | | $0359 Turkish | | | $035C Portuguese | | | $035D Icelandic | | | $035F Canadian French | | | $0361 Nordic | | | $0365 Greek 2 | | | | | | Refer to an MS-DOS technical reference about interrupt $21, | | | function $66, for more information about this. | | | | +------+---------------------------------------------------------------------+ | 006C...006F unknown | +------+---------------------------------------------------------------------+ | 0070 | integer changeCount4 | +------+---------------------------------------------------------------------+ | 0072...0077 unknown | ============================================================================== Paradox Common File Header -- continued ========================== The file header continues sequentially. Since the number of fields varies, there are no subsequent fixed offsets. This section begins where the previous section left off: offset ============================================================================== | 0058 Paradox tables version 3.0 and 3.5 | | 0058 Paradox .PX and .Ynn index files (any listed version) | | 0078 Paradox tables version 4.0 and above | | 0078 Paradox .Xnn index files version 4.0 and above | ============================================================================== type usage ============================================================================== | ---- | array[1..(numFields)] of TFldInfoRec fieldInfo | | | | | | type TFldInfoRec = RECORD | | | fType : byte; | | | fSize : byte; | | | end; | | | | | | These are the field identifiers for each field in the | | | table: | | | | | | fType fSize(decimal) | | | ------------------------- | | | $01 v "A" Alpha | | | $02 4 "D" Date | | | $03 2 "S" Short integer | | | $04 4 "I" Long integer | | | $05 8 "$" currency | | | $06 8 "N" Number | | | $09 1 "L" Logical | | | $0C v "M" Memo BLOb | | | $0D v "B" Binary Large Object | | | $0E v "F" Formatted Memo BLOb | | | $0F v "O" OLE | | | $10 v "G" Graphic BLOb | | | $14 4 "T" Time | | | $15 8 "@" Timestamp | | | $16 4 "+" Autoincrement | | | $17 17* "#" BCD | | | $18 v "Y" Bytes | | | | | | The fSize given for BCD fields is not used for field size. | | | Instead, fSize denotes the number of digits following the | | | decimal point. BCD fields are always 17 bytes long. | | | | +------+---------------------------------------------------------------------+ | ---- | pchar tableNamePtr | | | | | | Pointer to tableName when header is in RAM. | | | | +======+=====================================================================+ | ---- | array[1..(numFields)] of pchar fieldNamePtrArray | | | | | | * These pointers are not present in the index files. * | | | | | | This is an array of pointers that reference the field names | | | when Paradox (or one of the engines) is running. The size | | | of this array depends upon the number of fields. | | | | +======+=====================================================================+ | ---- | array[1..(length varies)] of char tableName | | | | | | This was the name this file was assigned when created. | | | | | | Most tables will use 79 bytes, padded with zeroes. | | | This was extended with Paradox 7 to 261 bytes. | | | | | | NOTE: It was previously listed here as 271 bytes but | | | I received a heads-up from George Thackray that | | | this was in error. It may also be that there's | | | there are more variations. | | | | ============================================================================== | | | The rest of this information does not apply to .PX and .Ynn files. | | | ============================================================================== | ---- | char[] fieldNames | | | | | | These are the ASCIIZ field name(s), arranged sequentially. | | | | +------+---------------------------------------------------------------------+ | ---- | record cryptInfo | | | | | | Encrypted tables would have additional data here. | | | | | | Tables with auxiliary passwords would have 256 bytes here | | | (about which I have no information). | | | Encrypted tables without auxiliary passwords would have | | | one byte for each field here. | | | | +------+---------------------------------------------------------------------+ | ---- | array[1..(numFields)] of integer fieldNumbers | | | | | | These seem to be field numbers. Changing these numbers | | | in my limited experiments seemed to cause no change in | | | behavior. TUTILITY didn't seem to mind either. | | | | | | I define this as an array, but the size is determined by | | | the number of fields in the table. | | | | +------+---------------------------------------------------------------------+ | ---- | char[] sortOrderID | | | | | | An ASCIIZ string representing the sort order for this | | | table ("ascii", "intl", etc.). | | | | ============================================================================== Paradox Data Blocks =================== The data area begins at offset headerSize (usually $0800). It is divided into blocks of 1024, 2048, 3072 or 4096 bytes -- depending upon the maximum table size set when the table was created. The entire data block will be encrypted if the table is encrypted. byte type usage ============================================================================== | 0000 | word nextBlock (block number + ???) | | | | | | I don't know what this does. | | | | +------+---------------------------------------------------------------------+ | 0002 | word blockNumber | | | | | | The first block is numbered zero. | | | | +------+---------------------------------------------------------------------+ | 0004 | integer addDataSize | | | | | | This represents the amount of data in this block -- in | | | addition to one record length. | | | | | | This will be a zero if there is one record in this block, | | | and a negative number if there are no records. | | | | | | numRecsInBlock = (addDataSize / recordSize) + 1 | | | | ============================================================================== | 0006....... fileData | | | | Block size varies according to maxTableSize (at 0005): | | maxTableSize = 1 (64M): block size = $0400 bytes | | maxTableSize = 2 (128M): block size = $0800 bytes | | maxTableSize = 3 (192M): block size = $0C00 bytes | | maxTableSize = 4 (256M): block size = $1000 bytes | | | | | | The records in the data area are arranged in field order. Alpha fields | | are arrays of characters, padded with zeroes. Other fields seem to be | | arranged in hi-byte to low-byte order, with the first byte's high bit | | set for positive numbers. | | | | For example, the number 1 stored as a 2-byte Short integer field is | | arranged this way: $80, 01. And the integer 256 would be stored as: | | $81, 00. | | | | Floating point Number and currency fields are 8-byte DOUBLE types, | | hi-byte to low-byte, with the first byte's high bit set. | | | | Date fields are stored as the number of days since JAN-0-0000, in high- | | byte to low-byte order with the first byte's high bit set. | | | | | | If you are wondering why the first byte's high bit is set for positive | | numbers, recall from the Paradox Engine's documentation that a "blank" | | SHORT (2-byte) integer is assigned the value of $8000. By using the | | hi-byte to low-byte format, and by reversing the first byte's high bit, | | the value $8000 is converted to all zeroes. So a record stored as all | | zeroes is actually all blanks. | | | | | | The records in primary index files contain the field or fields in the | | index -- plus three integers that are not described in the header's | | fieldInfo area. | | | | | ============================================================================== Sample Program with Paradox File Header Defined as a Pascal Record ================================================================== The rest of this text may be copied to a separate file as source code. Program PXFMT; (* This program will list the field values in the given table. Usage: PXFMT The file name's extension is mandatory. It also works with index files, and displays the index record's three additional fields. It will stop if there are any errors. *) uses Dos, Crt, Objects; const { Paradox codes for field types } pxfAlpha = $01; pxfDate = $02; pxfShort = $03; pxfLong = $04; pxfCurrency = $05; pxfNumber = $06; pxfLogical = $09; pxfMemoBLOb = $0C; pxfBLOb = $0D; pxfFmtMemoBLOb = $0E; pxfOLE = $0F; pxfGraphic = $10; pxfTime = $14; pxfTimestamp = $15; pxfAutoInc = $16; pxfBCD = $17; pxfBytes = $18; type { field information record used in TPxHeader below } PFldInfoRec = ^TFldInfoRec; TFldInfoRec = RECORD fType : byte; fSize : byte; end; PPxHeader = ^TPxHeader; TPxHeader = RECORD recordSize : word; headerSize : word; fileType : byte; maxTableSize : byte; numRecords : longint; nextBlock : word; fileBlocks : word; firstBlock : word; lastBlock : word; unknown12x13 : word; modifiedFlags1 : byte; indexFieldNumber : byte; primaryIndexWorkspace : pointer; unknownPtr1A : pointer; unknown1Ex20 : array[$001E..$0020] of byte; numFields : integer; primaryKeyFields : integer; encryption1 : longint; sortOrder : byte; modifiedFlags2 : byte; unknown2Bx2C : array[$002B..$002C] of byte; changeCount1 : byte; changeCount2 : byte; unknown2F : byte; tableNamePtrPtr : ^pchar; fldInfoPtr : PFldInfoRec; writeProtected : byte; fileVersionID : byte; maxBlocks : word; unknown3C : byte; auxPasswords : byte; unknown3Ex3F : array[$003E..$003F] of byte; cryptInfoStartPtr : pointer; cryptInfoEndPtr : pointer; unknown48 : byte; autoInc : longint; unknown4Dx4E : array[$004D..$004E] of byte; indexUpdateRequired : byte; unknown50x54 : array[$0050..$0054] of byte; refIntegrity : byte; unknown56x57 : array[$0056..$0057] of byte; case INTEGER of 3: (fieldInfo35 : array[1..255] of TFldInfoRec); 4: (fileVerID2 : integer; fileVerID3 : integer; encryption2 : longint; fileUpdateTime : longint; { 4.0 only } hiFieldID : word; hiFieldIDinfo : word; sometimesNumFields:integer; dosCodePage : word; unknown6Cx6F : array[$006C..$006F] of byte; changeCount4 : integer; unknown72x77 : array[$0072..$0077] of byte; fieldInfo : array[1..255] of TFldInfoRec); { This is only the first part of the file header. The last field is described as an array of 255 elements, but its size is really determined by the number of fields in the table. The actual table header has more information that follows. } end; PDataBlock = ^TDataBlock; TDataBlock = RECORD nextBlock : word; blockNumber : word; addDataSize : integer; fileData : array[0..$0FF9] of byte; { fileData size varies according to maxTableSize } end; procedure ConvertPxField(var N; F: PFldInfoRec); { This will convert both ways, but blanks will be turned to zeroes. } { Warning: Not all field types are converted. } type TNRec= array[0..16] of byte; var i : integer; size : integer; NRec : TNRec; function ItsBlank : boolean; var i : integer; begin ItsBlank := TRUE; For i := 0 to pred(size) do If TNRec(N)[i] <> 0 then ItsBlank := FALSE; end; begin If F^.fType = pxfBCD then { BCD field size value not used for field size } size := 17 else size := F^.fSize; If (F^.fType in [pxfDate..pxfNumber, pxfTime..pxfAutoInc]) and not ItsBlank { leave blank fields as all zeroes } then begin TNRec(N)[0] := TNRec(N)[0] xor $80; For i := 0 to pred(size) do NRec[pred(size-i)] := TNRec(N)[i]; Move(NRec, N, size); end; end; procedure ConvertPxRecord(Hdr: PPxHeader; P: pointer); const IndexF : TFldInfoRec = (fType: pxfShort; fSize: sizeof(INTEGER)); var i : integer; F : PFldInfoRec; begin F := Hdr^.fldInfoPtr; { begin with the first field identifier } For i := 1 to Hdr^.numFields do begin ConvertPxField(P^, F); If F^.fType = pxfBCD then { BCD field size value not used for field size } Inc(ptrrec(P).ofs, 17) else Inc(ptrrec(P).ofs, F^.fSize); Inc(ptrrec(F).ofs, sizeof(F^)); end; If Hdr^.fileType = 1 then { convert primary index information } begin For i := 1 to 3 do begin ConvertPxField(P^, @IndexF); Inc(ptrrec(P).ofs, 2); end; end; end; procedure WritePxField(var N; F: PFldInfoRec); { not all field types are supported here } var i : integer; A : string; begin Case F^.fType of pxfAlpha, pxfMemoBLOb: begin Move(N, A[1], F^.fSize); A[0] := char(F^.fSize); For i := length(A) downto 1 do If (A[i] = #0) then A[0] := char(pred(i)); write('"', A, '"'); end; pxfShort: write(integer(N)); pxfLong, pxfAutoInc: write(longint(N)); pxfCurrency: write('$', double(N):1:2); pxfNumber: write(double(N):1:3); { the rest of the field types are not translated } pxfDate: write(''); pxfLogical: write(''); pxfBLOb: write(''); pxfFmtMemoBLOb: write(''); pxfOLE: write(''); pxfGraphic: write(''); pxfTime: write(''); pxfTimestamp: write(''); pxfBCD: write(''); pxfBytes: write(''); else write(''); end; end; procedure WritePxRecord(Hdr: PPxHeader; P: pointer); const IndexF : TFldInfoRec = (fType: pxfShort; fSize: sizeof(INTEGER)); var i : integer; F : PFldInfoRec; begin F := Hdr^.fldInfoPtr; { begin with the first field identifier } For i := 1 to Hdr^.numFields do begin If i > 1 then write(', '); WritePxField(P^, F); If F^.fType = pxfBCD then { BCD field size value not used for field size } Inc(ptrrec(P).ofs, 17) else Inc(ptrrec(P).ofs, F^.fSize); Inc(ptrrec(F).ofs, sizeof(F^)); end; If Hdr^.fileType = 1 then { display primary index information } begin For i := 1 to 3 do begin If i = 1 then write('; index fields: ') else write(', '); WritePxField(P^, @IndexF); Inc(ptrrec(P).ofs, 2); end; end; writeln; end; procedure ReadBlock(var S: TStream; Hdr: PPxHeader; var AData ); begin S.Read(AData, Hdr^.maxTableSize * $0400) end; procedure SeekBlock(var S: TStream; Hdr: PPxHeader; ABlock: word); var L : longint; begin L := ABlock; L := (L * Hdr^.maxTableSize * $0400) + Hdr^.headerSize; S.Seek(L); end; procedure ReadAllRecords(var S: TStream); var i : integer; num,z : word; Block : PDataBlock; F : TFldInfoRec; Hdr : PPxHeader; function FileFormatIsOK : boolean; begin FileFormatIsOK := (Hdr^.maxTableSize >= 1) and (Hdr^.maxTableSize <= 4) end; function FileIsEncrypted : boolean; begin If (Hdr^.fileVersionID <= 4) or not (Hdr^.fileType in [0,2,3,5]) then FileIsEncrypted := (Hdr^.encryption1 <> 0) else FileIsEncrypted := (Hdr^.encryption2 <> 0) end; begin New(Hdr); S.Seek(0); S.Read(Hdr^, sizeof(Hdr^)); If (S.Status = stOK) and FileFormatIsOK then begin { assign the header's fldInfoPtr field } If (Hdr^.fileVersionID <= 4) or not (Hdr^.fileType in [0,2,3,5]) then Hdr^.fldInfoPtr := addr(Hdr^.fieldInfo35) else Hdr^.fldInfoPtr := addr(Hdr^.fieldInfo); If FileIsEncrypted then writeln('This file is encrypted.') else begin New(Block); num := 0; While (S.Status = stOK) and (num < Hdr^.fileBlocks) do begin SeekBlock(S, Hdr, num); ReadBlock(S, Hdr, Block^); If (S.Status = stOK) and (Block^.addDataSize >= 0) then begin z := 0; For i := 0 to (Block^.addDataSize div Hdr^.recordSize) do begin ConvertPxRecord(Hdr, addr(Block^.fileData[z])); WritePxRecord(Hdr, addr(Block^.fileData[z])); Inc(z, Hdr^.recordSize); end; end; Inc(num); end; Dispose(Block); end; end; end; var Stream : TBufStream; Begin Assign(Output, ''); Rewrite(Output); Stream.Init(paramstr(1), stOpenRead, 4096); ReadAllRecords(Stream); If Stream.Status <> stOK then writeln(^M^J'Error: Status=', Stream.Status, '; Error=', Stream.ErrorInfo); Stream.Done; End.